* [gentoo-commits] proj/portage-utils:master commit in: man/, man/include/, libq/, /
@ 2019-05-21 14:11 Fabian Groffen
0 siblings, 0 replies; only message in thread
From: Fabian Groffen @ 2019-05-21 14:11 UTC (permalink / raw
To: gentoo-commits
commit: d16f69568285d76dfa8a367f2458bc16f9e049e4
Author: Fabian Groffen <grobian <AT> gentoo <DOT> org>
AuthorDate: Mon May 20 17:53:48 2019 +0000
Commit: Fabian Groffen <grobian <AT> gentoo <DOT> org>
CommitDate: Mon May 20 17:53:48 2019 +0000
URL: https://gitweb.gentoo.org/proj/portage-utils.git/commit/?id=d16f6956
qmanifest: new applet to verify and generate thick Manifests
This incorporates https://github.com/grobian/hashgen into portage-utils
as qmanifest.
Signed-off-by: Fabian Groffen <grobian <AT> gentoo.org>
Makefile.am | 10 +
applets.h | 24 +-
configure.ac | 73 ++
libq/Makefile.am | 2 +
libq/hash.c | 196 +++++
libq/hash.h | 26 +
man/include/qmanifest.desc | 29 +
man/qmanifest.1 | 94 +++
qmanifest.c | 1695 ++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 2140 insertions(+), 9 deletions(-)
diff --git a/Makefile.am b/Makefile.am
index b36173c..f28a073 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -17,6 +17,7 @@ APPLETS = \
qkeyword \
qlist \
qlop \
+ qmanifest \
qmerge \
qpkg \
qsearch \
@@ -37,6 +38,7 @@ dist_man_MANS = \
man/qkeyword.1 \
man/qlist.1 \
man/qlop.1 \
+ man/qmanifest.1 \
man/qmerge.1 \
man/qpkg.1 \
man/qsearch.1 \
@@ -59,6 +61,7 @@ q_SOURCES = \
qkeyword.c \
qlist.c \
qlop.c \
+ qmanifest.c \
qmerge.c \
qpkg.c \
qsearch.c \
@@ -69,14 +72,21 @@ q_SOURCES = \
qxpak.c \
$(NULL)
q_CPPFLAGS = \
+ $(OPENMP_CFLAGS) \
+ $(GPGME_CFLAGS) \
-I$(top_srcdir)/libq \
-I$(top_builddir)/autotools/gnulib \
-I$(top_srcdir)/autotools/gnulib \
$(NULL)
q_LDADD = \
+ $(OPENMP_CFLAGS) \
$(top_builddir)/libq/libq.la \
$(top_builddir)/autotools/gnulib/libgnu.a \
-liniparser \
+ $(LIBSSL) \
+ $(LIBBL2) \
+ $(LIBZ) \
+ $(GPGME_LIBS) \
$(LIB_CLOCK_GETTIME) \
$(LIB_EACCESS) \
$(NULL)
diff --git a/applets.h b/applets.h
index fbb4e8c..902e664 100644
--- a/applets.h
+++ b/applets.h
@@ -38,23 +38,24 @@ typedef int (*APPLET)(int, char **);
#define DECLARE_APPLET(applet) \
extern int applet##_main(int, char **) __attribute__((weak));
DECLARE_APPLET(q)
+DECLARE_APPLET(qatom)
DECLARE_APPLET(qcheck)
DECLARE_APPLET(qdepends)
DECLARE_APPLET(qfile)
+/*DECLARE_APPLET(qglsa) disable */
+DECLARE_APPLET(qgrep)
+DECLARE_APPLET(qkeyword)
DECLARE_APPLET(qlist)
DECLARE_APPLET(qlop)
+DECLARE_APPLET(qmanifest)
+DECLARE_APPLET(qmerge)
+DECLARE_APPLET(qpkg)
DECLARE_APPLET(qsearch)
DECLARE_APPLET(qsize)
DECLARE_APPLET(qtbz2)
+DECLARE_APPLET(qtegrity)
DECLARE_APPLET(quse)
DECLARE_APPLET(qxpak)
-DECLARE_APPLET(qpkg)
-DECLARE_APPLET(qgrep)
-DECLARE_APPLET(qatom)
-DECLARE_APPLET(qmerge)
-DECLARE_APPLET(qkeyword)
-/*DECLARE_APPLET(qglsa) disable */
-DECLARE_APPLET(qtegrity)
#undef DECLARE_APPLET
static const struct applet_t {
@@ -66,7 +67,6 @@ static const struct applet_t {
/* q must always be the first applet */
{"q", q_main, "<applet> <args>", "virtual applet"},
{"qatom", qatom_main, "<pkg>", "split atom strings"},
- {"qkeyword", qkeyword_main, "<action> <args>", "list packages based on keywords"},
{"qcheck", qcheck_main, "<pkgname>", "verify integrity of installed packages"},
{"qdepends", qdepends_main, "<pkgname>", "show dependency info"},
{"qfile", qfile_main, "<filename>", "list all pkgs owning files"},
@@ -74,16 +74,18 @@ static const struct applet_t {
{"qglsa", qglsa_main, "<action> <list>", "check GLSAs against system"},
*/
{"qgrep", qgrep_main, "<expr> [pkg ...]", "grep in ebuilds"},
+ {"qkeyword", qkeyword_main, "<action> <args>", "list packages based on keywords"},
{"qlist", qlist_main, "<pkgname>", "list files owned by pkgname"},
{"qlop", qlop_main, "<pkgname>", "emerge log analyzer"},
+ {"qmanifest", qmanifest_main, "<misc args>", "verify or generate thick Manifest files"},
{"qmerge", qmerge_main, "<pkgnames>", "fetch and merge binary package"},
{"qpkg", qpkg_main, "<misc args>", "manipulate Gentoo binpkgs"},
{"qsearch", qsearch_main, "<regex>", "search pkgname/desc"},
{"qsize", qsize_main, "<pkgname>", "calculate size usage"},
{"qtbz2", qtbz2_main, "<misc args>", "manipulate tbz2 packages"},
+ {"qtegrity", qtegrity_main, "<misc args>", "verify files with IMA"},
{"quse", quse_main, "<useflag>", "find pkgs using useflags"},
{"qxpak", qxpak_main, "<misc args>", "manipulate xpak archives"},
- {"qtegrity", qtegrity_main, "<misc args>", "verify files with IMA"},
/* aliases for equery compatibility */
{"belongs", qfile_main, NULL, NULL},
@@ -107,6 +109,10 @@ static const struct applet_t {
/* alias for qtegrity */
{"integrity", qtegrity_main, NULL, NULL},
+ /* old hashgen */
+ {"hashgen", qmanifest_main, NULL, NULL},
+ {"hashverify",qmanifest_main, NULL, NULL},
+
{NULL, NULL, NULL, NULL}
};
diff --git a/configure.ac b/configure.ac
index 43a4609..2a39df8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -8,6 +8,7 @@ AC_CONFIG_MACRO_DIR([autotools/m4])
AC_PROG_CC_C99
AM_PROG_CC_C_O
+AC_OPENMP
AC_USE_SYSTEM_EXTENSIONS
gl_EARLY
@@ -31,6 +32,78 @@ AC_DEFINE_UNQUOTED([CONFIG_EPREFIX], ["$with_eprefix"],
[Gentoo Prefix offset path])
AC_SUBST([CONFIG_EPREFIX], ["$with_eprefix"])
+AC_ARG_ENABLE([qmanifest], [AS_HELP_STRING([--enable-qmanifest],
+ [support qmanifest applet])],
+ [], [enable_qmanifest=auto])
+LIBSSL=
+LIBBL2=
+LIBZ=
+HASGPGME=
+AS_IF([test "x$enable_qmanifest" != xno],
+ [AC_CHECK_HEADERS([openssl/err.h \
+ openssl/ssl.h], [], [LIBSSL=_missing_header])
+ AC_CHECK_LIB([ssl${LIBSSL}], [SSL_connect],
+ [LIBSSL="-lssl"
+ AC_DEFINE([HAVE_SSL], [1], [Define if you have ssl])
+ AC_CHECK_LIB([crypto],
+ [ERR_reason_error_string],
+ [LIBSSL="${LIBSSL} -lcrypto"],
+ [])
+ AC_SUBST([LIBSSL], ["${LIBSSL}"])
+ ],
+ [if test "x$enable_qmanifest" != xauto; then
+ AC_MSG_FAILURE(
+ [--enable-qmanifest was given, but test for ssl failed])
+ fi
+ LIBSSL=
+ ])
+ AC_CHECK_HEADERS([blake2.h], [], [LIBBL2=_missing_header])
+ AC_CHECK_LIB([b2${LIBBL2}], [blake2b_update],
+ [LIBBL2="-lb2"
+ AC_DEFINE([HAVE_BLAKE2B], [1],
+ [Define if you have blake2b])
+ AC_SUBST([LIBBL2], ["${LIBBL2}"])
+ ],
+ [if test "x$enable_qmanifest" != xauto; then
+ AC_MSG_FAILURE(
+ [--enable-qmanifest was given, but test for blake2b failed])
+ fi
+ LIBBL2=
+ ])
+ AC_CHECK_HEADERS([zlib.h], [], [LIBZ=_missing_header])
+ AC_CHECK_LIB([z${LIBZ}], [gzopen],
+ [LIBZ="-lz"
+ AC_DEFINE([HAVE_LIBZ], [1],
+ [Define if you have zlib])
+ AC_SUBST([LIBZ], ["${LIBZ}"])
+ ],
+ [if test "x$enable_qmanifest" != xauto; then
+ AC_MSG_FAILURE(
+ [--enable-qmanifest was given, but test for libz failed])
+ fi
+ LIBZ=
+ ])
+ AM_PATH_GPGME([], [HASGPGME=yes])
+ AC_MSG_CHECKING([whether to enable qmanifest])
+ case "x${LIBSSL}${LIBBL2}${LIBZ}-${HASGPGME}" in
+ "x-lssl"*"-lb2-lz-yes")
+ AC_MSG_RESULT([yes])
+ ;;
+ *)
+ enable_qmanifest=no
+ AC_MSG_RESULT([no: missing dependencies])
+ ;;
+ esac
+ if test "x$enable_qmanifest" != xno ; then
+ AC_DEFINE([ENABLE_QMANIFEST], [1],
+ [Define if qmanifest should be compiled])
+ fi
+ ],
+ [
+ AC_MSG_CHECKING([whether to enable qmanifest])
+ AC_MSG_RESULT([no: disabled by configure argument])
+ ])
+
AX_CFLAGS_WARN_ALL
AC_DEFUN([PT_CHECK_CFLAG],[AX_CHECK_COMPILER_FLAGS([$1],[CFLAGS="$CFLAGS $1"])])
m4_foreach_w([flag], [
diff --git a/libq/Makefile.am b/libq/Makefile.am
index 62ffb83..9cb19a2 100644
--- a/libq/Makefile.am
+++ b/libq/Makefile.am
@@ -8,6 +8,7 @@ QFILES = \
copy_file.c copy_file.h \
dep.c dep.h \
eat_file.c eat_file.h \
+ hash.c hash.h \
hash_fd.c hash_fd.h \
human_readable.c human_readable.h \
i18n.h \
@@ -31,5 +32,6 @@ QFILES = \
noinst_LTLIBRARIES = libq.la
libq_la_SOURCES = $(QFILES)
libq_la_CPPFLAGS = \
+ $(OPENMP_CFLAGS) \
-I$(top_builddir)/autotools/gnulib \
-I$(top_srcdir)/autotools/gnulib
diff --git a/libq/hash.c b/libq/hash.c
new file mode 100644
index 0000000..5b13d49
--- /dev/null
+++ b/libq/hash.c
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2018-2019 Gentoo Foundation
+ * Distributed under the terms of the GNU General Public License v2
+ *
+ * Copyright 2018- Fabian Groffen - <grobian@gentoo.org>
+ *
+ * The contents of this file was taken from:
+ * https://github.com/grobian/hashgen
+ * which was discontinued at the time the sources were incorporated into
+ * portage-utils as qmanifest.
+ */
+
+#include "main.h"
+
+#ifdef HAVE_SSL
+#include <openssl/sha.h>
+#include <openssl/whrlpool.h>
+#endif
+#ifdef HAVE_BLAKE2B
+#include <blake2.h>
+#endif
+
+#include "hash.h"
+
+static int hashes = HASH_DEFAULT;
+
+void
+hash_hex(char *out, const unsigned char *buf, const int length)
+{
+ switch (length) {
+ /* SHA256_DIGEST_LENGTH */
+ case 32:
+ snprintf(out, 64 + 1,
+ "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
+ "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
+ "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
+ "%02x%02x",
+ buf[ 0], buf[ 1], buf[ 2], buf[ 3], buf[ 4],
+ buf[ 5], buf[ 6], buf[ 7], buf[ 8], buf[ 9],
+ buf[10], buf[11], buf[12], buf[13], buf[14],
+ buf[15], buf[16], buf[17], buf[18], buf[19],
+ buf[20], buf[21], buf[22], buf[23], buf[24],
+ buf[25], buf[26], buf[27], buf[28], buf[29],
+ buf[30], buf[31]
+ );
+ break;
+ /* SHA512_DIGEST_LENGTH, WHIRLPOOL_DIGEST_LENGTH, BLAKE2B_OUTBYTES */
+ case 64:
+ snprintf(out, 128 + 1,
+ "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
+ "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
+ "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
+ "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
+ "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
+ "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
+ "%02x%02x%02x%02x",
+ buf[ 0], buf[ 1], buf[ 2], buf[ 3], buf[ 4],
+ buf[ 5], buf[ 6], buf[ 7], buf[ 8], buf[ 9],
+ buf[10], buf[11], buf[12], buf[13], buf[14],
+ buf[15], buf[16], buf[17], buf[18], buf[19],
+ buf[20], buf[21], buf[22], buf[23], buf[24],
+ buf[25], buf[26], buf[27], buf[28], buf[29],
+ buf[30], buf[31], buf[32], buf[33], buf[34],
+ buf[35], buf[36], buf[37], buf[38], buf[39],
+ buf[40], buf[41], buf[42], buf[43], buf[44],
+ buf[45], buf[46], buf[47], buf[48], buf[49],
+ buf[50], buf[51], buf[52], buf[53], buf[54],
+ buf[55], buf[56], buf[57], buf[58], buf[59],
+ buf[60], buf[61], buf[62], buf[63]
+ );
+ break;
+ /* fallback case, should never be necessary */
+ default:
+ {
+ int i;
+ for (i = 0; i < length; i++) {
+ snprintf(&out[i * 2], 3, "%02x", buf[i]);
+ }
+ }
+ break;
+ }
+}
+
+/**
+ * Computes the hashes for file fname and writes the hex-representation
+ * for those hashes into the address space pointed to by the return
+ * pointers for these hashes. The caller should ensure enough space is
+ * available. Only those hashes which are in the global hashes variable
+ * are computed, the address space pointed to for non-used hashes are
+ * left untouched, e.g. they can be NULL. The number of bytes read from
+ * the file pointed to by fname is returned in the flen argument.
+ */
+void
+hash_compute_file(
+ const char *fname,
+ char *sha256,
+ char *sha512,
+ char *whrlpl,
+ char *blak2b,
+ size_t *flen)
+{
+ FILE *f;
+ char data[8192];
+ size_t len;
+#ifdef HAVE_SSL
+ SHA256_CTX s256;
+ SHA512_CTX s512;
+ WHIRLPOOL_CTX whrl;
+#endif
+#ifdef HAVE_BLAKE2B
+ blake2b_state bl2b;
+#endif
+
+ if ((f = fopen(fname, "r")) == NULL)
+ return;
+
+#ifdef HAVE_SSL
+ SHA256_Init(&s256);
+ SHA512_Init(&s512);
+ WHIRLPOOL_Init(&whrl);
+#endif
+#ifdef HAVE_BLAKE2B
+ blake2b_init(&bl2b, BLAKE2B_OUTBYTES);
+#endif
+
+ while ((len = fread(data, 1, sizeof(data), f)) > 0) {
+ *flen += len;
+#pragma omp parallel sections
+ {
+#ifdef HAVE_SSL
+#pragma omp section
+ {
+ if (hashes & HASH_SHA256)
+ SHA256_Update(&s256, data, len);
+ }
+#pragma omp section
+ {
+ if (hashes & HASH_SHA512)
+ SHA512_Update(&s512, data, len);
+ }
+#pragma omp section
+ {
+ if (hashes & HASH_WHIRLPOOL)
+ WHIRLPOOL_Update(&whrl, data, len);
+ }
+#endif
+#ifdef HAVE_BLAKE2B
+#pragma omp section
+ {
+ if (hashes & HASH_BLAKE2B)
+ blake2b_update(&bl2b, (unsigned char *)data, len);
+ }
+#endif
+ }
+ }
+ fclose(f);
+
+#pragma omp parallel sections
+ {
+#ifdef HAVE_SSL
+ {
+ if (hashes & HASH_SHA256) {
+ unsigned char sha256buf[SHA256_DIGEST_LENGTH];
+ SHA256_Final(sha256buf, &s256);
+ hash_hex(sha256, sha256buf, SHA256_DIGEST_LENGTH);
+ }
+ }
+#pragma omp section
+ {
+ if (hashes & HASH_SHA512) {
+ unsigned char sha512buf[SHA512_DIGEST_LENGTH];
+ SHA512_Final(sha512buf, &s512);
+ hash_hex(sha512, sha512buf, SHA512_DIGEST_LENGTH);
+ }
+ }
+#pragma omp section
+ {
+ if (hashes & HASH_WHIRLPOOL) {
+ unsigned char whrlplbuf[WHIRLPOOL_DIGEST_LENGTH];
+ WHIRLPOOL_Final(whrlplbuf, &whrl);
+ hash_hex(whrlpl, whrlplbuf, WHIRLPOOL_DIGEST_LENGTH);
+ }
+ }
+#endif
+#ifdef HAVE_BLAKE2B
+#pragma omp section
+ {
+ if (hashes & HASH_BLAKE2B) {
+ unsigned char blak2bbuf[BLAKE2B_OUTBYTES];
+ blake2b_final(&bl2b, blak2bbuf, BLAKE2B_OUTBYTES);
+ hash_hex(blak2b, blak2bbuf, BLAKE2B_OUTBYTES);
+ }
+ }
+#endif
+ }
+}
diff --git a/libq/hash.h b/libq/hash.h
new file mode 100644
index 0000000..157c454
--- /dev/null
+++ b/libq/hash.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2018-2019 Gentoo Foundation
+ * Distributed under the terms of the GNU General Public License v2
+ *
+ * Copyright 2018- Fabian Groffen - <grobian@gentoo.org>
+ */
+
+#ifndef _HASH_H
+#define _HASH_H 1
+
+enum hash_impls {
+ HASH_SHA256 = 1<<0,
+ HASH_SHA512 = 1<<1,
+ HASH_WHIRLPOOL = 1<<2,
+ HASH_BLAKE2B = 1<<3
+};
+
+/* default changed from sha256, sha512, whirlpool
+ * to blake2b, sha512 on 2017-11-21 */
+#define HASH_DEFAULT (HASH_BLAKE2B | HASH_SHA512);
+
+void hash_hex(char *out, const unsigned char *buf, const int length);
+void hash_compute_file(const char *fname, char *sha256, char *sha512,
+ char *whrlpl, char *blak2b, size_t *flen);
+
+#endif
diff --git a/man/include/qmanifest.desc b/man/include/qmanifest.desc
new file mode 100644
index 0000000..b873419
--- /dev/null
+++ b/man/include/qmanifest.desc
@@ -0,0 +1,29 @@
+\fIqmanifest\fR allows to verify or generate thick signed Manifests in
+an ebuild tree. By default, \fIqmanifest\fR will verify the main tree
+as specified by Portage's configuration (one can check which using
+\fIq\fR \fB-o\fR). In this mode, it will output some information about
+the GPG signature of the top-level Manifest file, and further reports on
+any problems it encounters.
+.P
+This applet was originally a standalone program \fIhashgen\fR and its
+alias \fIhashverify\fR. Aliases for these names are still available for
+historical reasons. With the incorporation of \fIhashgen\fR in
+\fBportage-utils\fR, development on the former has stopped in favour of
+the latter.
+.P
+The arguments to \fIqmanifest\fR can be directories or names of
+overlays. By default, each argument is attempted to be matched against
+all overlay names, and if that fails, treated as directory to presume a
+tree is in. This behaviour can be overridden with the \fB-d\fR and
+\fB-o\fR flags to force treating the arguments as directories or
+overlay names respectively. Note that overlay names are those as
+defined in \fIrepos.conf\fR from Portage's configuration. The
+\fIrepo_name\fR files from the overlays themselves (if present) are
+ignored.
+.P
+This applet does similar things as \fIapp-portage/gemato\fR. However,
+the output and implemented strategies are completely different. When
+compiled with \fBUSE=openmp\fR, this applet will exploit parallelism
+where possible to traverse a tree. Should you want to limit the number
+of parallel threads, export \fBOMP_NUM_THREADS\fR in your environment
+with the desired maximum amount of threads in use by \fIqmanifest\fR.
diff --git a/man/qmanifest.1 b/man/qmanifest.1
new file mode 100644
index 0000000..e223122
--- /dev/null
+++ b/man/qmanifest.1
@@ -0,0 +1,94 @@
+.\" generated by mkman.py, please do NOT edit!
+.TH qmanifest "1" "May 2019" "Gentoo Foundation" "qmanifest"
+.SH NAME
+qmanifest \- verify or generate thick Manifest files
+.SH SYNOPSIS
+.B qmanifest
+\fI[opts] <misc args>\fR
+.SH DESCRIPTION
+\fIqmanifest\fR allows to verify or generate thick signed Manifests in
+an ebuild tree. By default, \fIqmanifest\fR will verify the main tree
+as specified by Portage's configuration (one can check which using
+\fIq\fR \fB-o\fR). In this mode, it will output some information about
+the GPG signature of the top-level Manifest file, and further reports on
+any problems it encounters.
+.P
+This applet was originally a standalone program \fIhashgen\fR and its
+alias \fIhashverify\fR. Aliases for these names are still available for
+historical reasons. With the incorporation of \fIhashgen\fR in
+\fBportage-utils\fR, development on the former has stopped in favour of
+the latter.
+.P
+The arguments to \fIqmanifest\fR can be directories or names of
+overlays. By default, each argument is attempted to be matched against
+all overlay names, and if that fails, treated as directory to presume a
+tree is in. This behaviour can be overridden with the \fB-d\fR and
+\fB-o\fR flags to force treating the arguments as directories or
+overlay names respectively. Note that overlay names are those as
+defined in \fIrepos.conf\fR from Portage's configuration. The
+\fIrepo_name\fR files from the overlays themselves (if present) are
+ignored.
+.P
+This applet does similar things as \fIapp-portage/gemato\fR. However,
+the output and implemented strategies are completely different. When
+compiled with \fBUSE=openmp\fR, this applet will exploit parallelism
+where possible to traverse a tree. Should you want to limit the number
+of parallel threads, export \fBOMP_NUM_THREADS\fR in your environment
+with the desired maximum amount of threads in use by \fIqmanifest\fR.
+.SH OPTIONS
+.TP
+\fB\-g\fR, \fB\-\-generate\fR
+Generate thick Manifests and sign.
+.TP
+\fB\-d\fR, \fB\-\-dir\fR
+Treat arguments as directories.
+.TP
+\fB\-o\fR, \fB\-\-overlay\fR
+Treat arguments as overlay names.
+.TP
+\fB\-\-root\fR \fI<arg>\fR
+Set the ROOT env var.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Make a lot of noise.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+Tighter output; suppress warnings.
+.TP
+\fB\-C\fR, \fB\-\-nocolor\fR
+Don't output color.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+Print this help and exit.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+Print version and exit.
+
+.SH "REPORTING BUGS"
+Please report bugs via http://bugs.gentoo.org/
+.br
+Product: Portage Development; Component: Tools
+.SH AUTHORS
+.nf
+Ned Ludd <solar@gentoo.org>
+Mike Frysinger <vapier@gentoo.org>
+Fabian Groffen <grobian@gentoo.org>
+.fi
+.SH "SEE ALSO"
+.BR q (1),
+.BR qatom (1),
+.BR qcheck (1),
+.BR qdepends (1),
+.BR qfile (1),
+.BR qgrep (1),
+.BR qkeyword (1),
+.BR qlist (1),
+.BR qlop (1),
+.BR qmerge (1),
+.BR qpkg (1),
+.BR qsearch (1),
+.BR qsize (1),
+.BR qtbz2 (1),
+.BR qtegrity (1),
+.BR quse (1),
+.BR qxpak (1)
diff --git a/qmanifest.c b/qmanifest.c
new file mode 100644
index 0000000..09e7881
--- /dev/null
+++ b/qmanifest.c
@@ -0,0 +1,1695 @@
+/*
+ * Copyright 2018-2019 Gentoo Foundation
+ * Distributed under the terms of the GNU General Public License v2
+ *
+ * Copyright 2018- Fabian Groffen - <grobian@gentoo.org>
+ *
+ * The contents of this file was taken from:
+ * https://github.com/grobian/hashgen
+ * which was discontinued at the time the sources were incorporated into
+ * portage-utils as qmanifest.
+ */
+
+#include "main.h"
+
+#ifdef ENABLE_QMANIFEST
+
+#include "applets.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <fcntl.h>
+#include <ctype.h>
+#include <dirent.h>
+#include <time.h>
+#include <errno.h>
+#include <termios.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/time.h>
+#include <sys/ioctl.h>
+#include <openssl/sha.h>
+#include <openssl/whrlpool.h>
+#include <blake2.h>
+#include <zlib.h>
+#include <gpgme.h>
+
+#include "eat_file.h"
+#include "hash.h"
+
+#define QMANIFEST_FLAGS "gdo" COMMON_FLAGS
+static struct option const qmanifest_long_opts[] = {
+ {"generate", no_argument, NULL, 'g'},
+ {"dir", no_argument, NULL, 'd'},
+ {"overlay", no_argument, NULL, 'o'},
+ COMMON_LONG_OPTS
+};
+static const char * const qmanifest_opts_help[] = {
+ "Generate thick Manifests and sign",
+ "Treat arguments as directories",
+ "Treat arguments as overlay names",
+ COMMON_OPTS_HELP
+};
+#define qmanifest_usage(ret) usage(ret, QMANIFEST_FLAGS, qmanifest_long_opts, qmanifest_opts_help, NULL, lookup_applet_idx("qmanifest"))
+
+static int hashes = HASH_DEFAULT;
+
+/* linked list structure to hold verification complaints */
+typedef struct verify_msg {
+ char *msg;
+ struct verify_msg *next;
+} verify_msg;
+
+typedef struct _gpg_signature {
+ char *algo;
+ char *fingerprint;
+ char isgood:1;
+ char *timestamp;
+ char *signer;
+ char *pkfingerprint;
+ char *reason;
+} gpg_sig;
+
+gpg_sig *verify_gpg_sig(const char *path, verify_msg **msgs);
+char *verify_timestamp(const char *ts);
+char verify_manifest(const char *dir, const char *manifest, verify_msg **msgs);
+
+/* Generate thick Manifests based on thin Manifests, or verify a tree. */
+
+/* In order to build this program, the following packages are required:
+ * - sys-libs/zlib (for compressing/decompressing Manifest files)
+ * - app-crypt/gpgme (for signing/verifying the top level manifest)
+ */
+
+static inline void
+update_times(struct timeval *tv, struct stat *s)
+{
+#ifdef __MACH__
+# define st_mtim st_mtimespec
+# define st_atim st_atimespec
+#endif
+ if (tv[1].tv_sec < s->st_mtim.tv_sec ||
+ (tv[1].tv_sec == s->st_mtim.tv_sec &&
+ tv[1].tv_usec < s->st_mtim.tv_nsec / 1000))
+ {
+ tv[0].tv_sec = s->st_atim.tv_sec;
+ tv[0].tv_usec = s->st_atim.tv_nsec / 1000;
+ tv[1].tv_sec = s->st_mtim.tv_sec;
+ tv[1].tv_usec = s->st_mtim.tv_nsec / 1000;
+ }
+}
+
+#define LISTSZ 64
+
+/**
+ * qsort comparator which runs strcmp.
+ */
+static int
+compare_strings(const void *l, const void *r)
+{
+ const char **strl = (const char **)l;
+ const char **strr = (const char **)r;
+ return strcmp(*strl, *strr);
+}
+
+/**
+ * Return a sorted list of entries in the given directory. All entries
+ * starting with a dot are ignored, and not present in the returned
+ * list. The list and all entries are allocated using xmalloc() and need
+ * to be freed.
+ * This function returns 0 when everything is fine, non-zero otherwise.
+ */
+static char
+list_dir(char ***retlist, size_t *retcnt, const char *path)
+{
+ DIR *d;
+ struct dirent *e;
+ size_t rlen = 0;
+ size_t rsize = 0;
+ char **rlist = NULL;
+
+ if ((d = opendir(path)) != NULL) {
+ while ((e = readdir(d)) != NULL) {
+ /* skip all dotfiles */
+ if (e->d_name[0] == '.')
+ continue;
+
+ if (rlen == rsize) {
+ rsize += LISTSZ;
+ rlist = realloc(rlist,
+ rsize * sizeof(rlist[0]));
+ if (rlist == NULL) {
+ fprintf(stderr, "out of memory\n");
+ return 1;
+ }
+ }
+ rlist[rlen] = xstrdup(e->d_name);
+ if (rlist[rlen] == NULL) {
+ fprintf(stderr, "out of memory\n");
+ return 1;
+ }
+ rlen++;
+ }
+ closedir(d);
+
+ qsort(rlist, rlen, sizeof(rlist[0]), compare_strings);
+
+ *retlist = rlist;
+ *retcnt = rlen;
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+/**
+ * Write hashes in Manifest format to the file open for writing m, or
+ * gzipped file open for writing gm. The hashes written are for a file
+ * in root found by name. The Manifest entry will be using type as
+ * first component.
+ */
+static void
+write_hashes(
+ struct timeval *tv,
+ const char *root,
+ const char *name,
+ const char *type,
+ FILE *m,
+ gzFile gm)
+{
+ size_t flen = 0;
+ char sha256[(SHA256_DIGEST_LENGTH * 2) + 1];
+ char sha512[(SHA512_DIGEST_LENGTH * 2) + 1];
+ char whrlpl[(WHIRLPOOL_DIGEST_LENGTH * 2) + 1];
+ char blak2b[(BLAKE2B_OUTBYTES * 2) + 1];
+ char data[8192];
+ char fname[8192];
+ size_t len;
+ struct stat s;
+
+ snprintf(fname, sizeof(fname), "%s/%s", root, name);
+
+ if (stat(fname, &s) != 0)
+ return;
+
+ update_times(tv, &s);
+
+ hash_compute_file(fname, sha256, sha512, whrlpl, blak2b, &flen);
+
+ len = snprintf(data, sizeof(data), "%s %s %zd", type, name, flen);
+ if (hashes & HASH_BLAKE2B)
+ len += snprintf(data + len, sizeof(data) - len,
+ " BLAKE2B %s", blak2b);
+ if (hashes & HASH_SHA256)
+ len += snprintf(data + len, sizeof(data) - len,
+ " SHA256 %s", sha256);
+ if (hashes & HASH_SHA512)
+ len += snprintf(data + len, sizeof(data) - len,
+ " SHA512 %s", sha512);
+ if (hashes & HASH_WHIRLPOOL)
+ len += snprintf(data + len, sizeof(data) - len,
+ " WHIRLPOOL %s", whrlpl);
+ len += snprintf(data + len, sizeof(data) - len, "\n");
+
+ if (m != NULL)
+ fwrite(data, len, 1, m);
+ if (gm != NULL)
+ gzwrite(gm, data, len);
+}
+
+/**
+ * Walk through a directory recursively and write hashes for each file
+ * found to the gzipped open stream for writing zm. The Manifest
+ * entries generated will all be of DATA type.
+ */
+static char
+write_hashes_dir(
+ struct timeval *tv,
+ const char *root,
+ const char *name,
+ gzFile zm)
+{
+ char path[8192];
+ char **dentries;
+ size_t dentrieslen;
+ size_t i;
+
+ snprintf(path, sizeof(path), "%s/%s", root, name);
+ if (list_dir(&dentries, &dentrieslen, path) == 0) {
+ for (i = 0; i < dentrieslen; i++) {
+ snprintf(path, sizeof(path), "%s/%s", name, dentries[i]);
+ free(dentries[i]);
+ if (write_hashes_dir(tv, root, path, zm) == 0)
+ continue;
+ /* regular file */
+ write_hashes(tv, root, path, "DATA", NULL, zm);
+ }
+ free(dentries);
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+/**
+ * Walk through directory recursively and write hashes for each file
+ * found to the open stream for writing m. All files will not use the
+ * "files/" prefix and Manifest entries will be of AUX type.
+ */
+static char
+process_files(struct timeval *tv, const char *dir, const char *off, FILE *m)
+{
+ char path[8192];
+ char **dentries;
+ size_t dentrieslen;
+ size_t i;
+
+ snprintf(path, sizeof(path), "%s/%s", dir, off);
+ if (list_dir(&dentries, &dentrieslen, path) == 0) {
+ for (i = 0; i < dentrieslen; i++) {
+ snprintf(path, sizeof(path), "%s%s%s",
+ off, *off == '\0' ? "" : "/", dentries[i]);
+ free(dentries[i]);
+ if (process_files(tv, dir, path, m) == 0)
+ continue;
+ /* regular file */
+ write_hashes(tv, dir, path, "AUX", m, NULL);
+ }
+ free(dentries);
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+/**
+ * Read layout.conf file specified by path and extract the
+ * manifest-hashes property from this file. The hash set specified for
+ * this property will be returned. When the property isn't found the
+ * returned hash set will be the HASH_DEFAULT set. When the file isn't
+ * found, 0 will be returned (which means /no/ hashes).
+ */
+static int
+parse_layout_conf(const char *path)
+{
+ FILE *f;
+ char buf[8192];
+ size_t len = 0;
+ size_t sz;
+ char *p;
+ char *q;
+ char *tok;
+ char *last_nl;
+ char *start;
+ int ret = 0;
+
+ if ((f = fopen(path, "r")) == NULL)
+ return 0;
+
+ /* read file, examine lines after encountering a newline, that is,
+ * if the file doesn't end with a newline, the final bit is ignored */
+ while ((sz = fread(buf + len, 1, sizeof(buf) - len, f)) > 0) {
+ len += sz;
+ start = buf;
+ last_nl = NULL;
+ for (p = buf; (size_t)(p - buf) < len; p++) {
+ if (*p == '\n') {
+ if (last_nl != NULL)
+ start = last_nl + 1;
+ last_nl = p;
+ do {
+ sz = strlen("manifest-hashes");
+ if (strncmp(start, "manifest-hashes", sz))
+ break;
+ if ((q = strchr(start + sz, '=')) == NULL)
+ break;
+ q++;
+ while (isspace((int)*q))
+ q++;
+ /* parse the tokens, whitespace separated */
+ tok = q;
+ do {
+ while (!isspace((int)*q))
+ q++;
+ sz = q - tok;
+ if (strncmp(tok, "SHA256", sz) == 0) {
+ ret |= HASH_SHA256;
+ } else if (strncmp(tok, "SHA512", sz) == 0) {
+ ret |= HASH_SHA512;
+ } else if (strncmp(tok, "WHIRLPOOL", sz) == 0) {
+ ret |= HASH_WHIRLPOOL;
+ } else if (strncmp(tok, "BLAKE2B", sz) == 0) {
+ ret |= HASH_BLAKE2B;
+ } else {
+ fprintf(stderr, "warning: unsupported hash from "
+ "layout.conf: %.*s\n", (int)sz, tok);
+ }
+ while (isspace((int)*q) && *q != '\n')
+ q++;
+ tok = q;
+ } while (*q != '\n');
+ /* got it, expect only once, so stop processing */
+ fclose(f);
+ return ret;
+ } while (0);
+ }
+ }
+ if (last_nl != NULL) {
+ last_nl++; /* skip \n */
+ len = last_nl - buf;
+ memmove(buf, last_nl, len);
+ last_nl = buf;
+ } else {
+ /* skip too long line */
+ len = 0;
+ }
+ }
+
+ fclose(f);
+ /* if we didn't find anything, return the default set */
+ return HASH_DEFAULT;
+}
+
+static const char *str_manifest = "Manifest";
+static const char *str_manifest_gz = "Manifest.gz";
+static const char *str_manifest_files_gz = "Manifest.files.gz";
+enum type_manifest {
+ GLOBAL_MANIFEST, /* Manifest.files.gz + Manifest */
+ SUBTREE_MANIFEST, /* Manifest.gz for recursive list of files */
+ EBUILD_MANIFEST, /* Manifest thick from thin */
+ CATEGORY_MANIFEST /* Manifest.gz with Manifest entries */
+};
+static const char *
+generate_dir(const char *dir, enum type_manifest mtype)
+{
+ FILE *f;
+ char path[8192];
+ struct stat s;
+ struct timeval tv[2];
+ char **dentries;
+ size_t dentrieslen;
+ size_t i;
+
+ /* our timestamp strategy is as follows:
+ * - when a Manifest exists, use its timestamp
+ * - when a meta-Manifest is written (non-ebuilds) use the timestamp
+ * of the latest Manifest referenced
+ * - when a Manifest is written for something like eclasses, use the
+ * timestamp of the latest file in the dir
+ * this way we should keep updates limited to where changes are, and
+ * also get reproducible mtimes. */
+ tv[0].tv_sec = 0;
+ tv[0].tv_usec = 0;
+ tv[1].tv_sec = 0;
+ tv[1].tv_usec = 0;
+
+ if (mtype == GLOBAL_MANIFEST) {
+ const char *mfest;
+ size_t len;
+ gzFile mf;
+ time_t rtime;
+
+ snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_files_gz);
+ if ((mf = gzopen(path, "wb9")) == NULL) {
+ fprintf(stderr, "failed to open file '%s' for writing: %s\n",
+ path, strerror(errno));
+ return NULL;
+ }
+
+ /* These "IGNORE" entries are taken from gx86, there is no
+ * standardisation on this, on purpose, apparently. */
+ len = snprintf(path, sizeof(path),
+ "IGNORE distfiles\n"
+ "IGNORE local\n"
+ "IGNORE lost+found\n"
+ "IGNORE packages\n"
+ "IGNORE snapshots\n");
+ gzwrite(mf, path, len);
+
+ if (list_dir(&dentries, &dentrieslen, dir) != 0)
+ return NULL;
+
+ for (i = 0; i < dentrieslen; i++) {
+ /* ignore existing Manifests */
+ if (strcmp(dentries[i], str_manifest_files_gz) == 0 ||
+ strcmp(dentries[i], str_manifest) == 0)
+ {
+ free(dentries[i]);
+ continue;
+ }
+
+ snprintf(path, sizeof(path), "%s/%s", dir, dentries[i]);
+
+ mfest = NULL;
+ if (!stat(path, &s)) {
+ if (s.st_mode & S_IFDIR) {
+ if (
+ strcmp(dentries[i], "eclass") == 0 ||
+ strcmp(dentries[i], "licenses") == 0 ||
+ strcmp(dentries[i], "metadata") == 0 ||
+ strcmp(dentries[i], "profiles") == 0 ||
+ strcmp(dentries[i], "scripts") == 0
+ )
+ {
+ mfest = generate_dir(path, SUBTREE_MANIFEST);
+ } else {
+ mfest = generate_dir(path, CATEGORY_MANIFEST);
+ }
+
+ if (mfest == NULL) {
+ fprintf(stderr, "generating Manifest for %s failed!\n",
+ path);
+ gzclose(mf);
+ return NULL;
+ }
+
+ snprintf(path, sizeof(path), "%s/%s",
+ dentries[i], mfest);
+ write_hashes(tv, dir, path, "MANIFEST", NULL, mf);
+ } else if (s.st_mode & S_IFREG) {
+ write_hashes(tv, dir, dentries[i], "DATA", NULL, mf);
+ } /* ignore other "things" (like symlinks) as they
+ don't belong in a tree */
+ } else {
+ fprintf(stderr, "stat(%s) failed: %s\n",
+ path, strerror(errno));
+ }
+ free(dentries[i]);
+ }
+ free(dentries);
+ gzclose(mf);
+
+ if (tv[0].tv_sec != 0) {
+ snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_files_gz);
+ utimes(path, tv);
+ }
+
+ /* create global Manifest */
+ snprintf(path, sizeof(path), "%s/%s", dir, str_manifest);
+ if ((f = fopen(path, "w")) == NULL) {
+ fprintf(stderr, "failed to open file '%s' for writing: %s\n",
+ path, strerror(errno));
+ return NULL;
+ }
+
+ write_hashes(tv, dir, str_manifest_files_gz, "MANIFEST", f, NULL);
+ time(&rtime);
+ len = strftime(path, sizeof(path),
+ "TIMESTAMP %Y-%m-%dT%H:%M:%SZ\n", gmtime(&rtime));
+ fwrite(path, len, 1, f);
+ fflush(f);
+ fclose(f);
+
+ /* because we write a timestamp in Manifest, we don't mess with
+ * its mtime, else it would obviously lie */
+ return str_manifest_files_gz;
+ } else if (mtype == SUBTREE_MANIFEST) {
+ const char *ldir;
+ gzFile mf;
+
+ snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_gz);
+ if ((mf = gzopen(path, "wb9")) == NULL) {
+ fprintf(stderr, "failed to open file '%s' for writing: %s\n",
+ path, strerror(errno));
+ return NULL;
+ }
+
+ ldir = strrchr(dir, '/');
+ if (ldir == NULL)
+ ldir = dir;
+ if (strcmp(ldir, "metadata") == 0) {
+ size_t len;
+ len = snprintf(path, sizeof(path),
+ "IGNORE timestamp\n"
+ "IGNORE timestamp.chk\n"
+ "IGNORE timestamp.commit\n"
+ "IGNORE timestamp.x\n");
+ gzwrite(mf, path, len);
+ }
+
+ if (list_dir(&dentries, &dentrieslen, dir) != 0)
+ return NULL;
+
+ for (i = 0; i < dentrieslen; i++) {
+ /* ignore existing Manifests */
+ if (strcmp(dentries[i], str_manifest_gz) == 0) {
+ free(dentries[i]);
+ continue;
+ }
+
+ if (write_hashes_dir(tv, dir, dentries[i], mf) != 0)
+ write_hashes(tv, dir, dentries[i], "DATA", NULL, mf);
+ free(dentries[i]);
+ }
+
+ free(dentries);
+ gzclose(mf);
+
+ if (tv[0].tv_sec != 0) {
+ /* set Manifest and dir mtime to most recent file found */
+ snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_gz);
+ utimes(path, tv);
+ utimes(dir, tv);
+ }
+
+ return str_manifest_gz;
+ } else if (mtype == CATEGORY_MANIFEST) {
+ const char *mfest;
+ gzFile mf;
+
+ snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_gz);
+ if ((mf = gzopen(path, "wb9")) == NULL) {
+ fprintf(stderr, "failed to open file '%s' for writing: %s\n",
+ path, strerror(errno));
+ return NULL;
+ }
+
+ if (list_dir(&dentries, &dentrieslen, dir) != 0)
+ return NULL;
+
+ for (i = 0; i < dentrieslen; i++) {
+ /* ignore existing Manifests */
+ if (strcmp(dentries[i], str_manifest_gz) == 0) {
+ free(dentries[i]);
+ continue;
+ }
+
+ snprintf(path, sizeof(path), "%s/%s", dir, dentries[i]);
+ if (!stat(path, &s)) {
+ if (s.st_mode & S_IFDIR) {
+ mfest = generate_dir(path, EBUILD_MANIFEST);
+
+ if (mfest == NULL) {
+ fprintf(stderr, "generating Manifest for %s failed!\n",
+ path);
+ gzclose(mf);
+ return NULL;
+ }
+
+ snprintf(path, sizeof(path), "%s/%s",
+ dentries[i], mfest);
+ write_hashes(tv, dir, path, "MANIFEST", NULL, mf);
+ } else if (s.st_mode & S_IFREG) {
+ write_hashes(tv, dir, dentries[i], "DATA", NULL, mf);
+ } /* ignore other "things" (like symlinks) as they
+ don't belong in a tree */
+ } else {
+ fprintf(stderr, "stat(%s) failed: %s\n",
+ path, strerror(errno));
+ }
+ free(dentries[i]);
+ }
+
+ free(dentries);
+ gzclose(mf);
+
+ if (tv[0].tv_sec != 0) {
+ /* set Manifest and dir mtime to most ebuild dir found */
+ snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_gz);
+ utimes(path, tv);
+ utimes(dir, tv);
+ }
+
+ return str_manifest_gz;
+ } else if (mtype == EBUILD_MANIFEST) {
+ char newmanifest[8192];
+ FILE *m;
+
+ snprintf(newmanifest, sizeof(newmanifest), "%s/.Manifest.new", dir);
+ if ((m = fopen(newmanifest, "w")) == NULL) {
+ fprintf(stderr, "failed to open file '%s' for writing: %s\n",
+ newmanifest, strerror(errno));
+ return NULL;
+ }
+
+ /* we know the Manifest is sorted, and stuff in files/ is
+ * prefixed with AUX, hence, if it exists, we need to do it
+ * first */
+ snprintf(path, sizeof(path), "%s/files", dir);
+ process_files(tv, path, "", m);
+
+ /* the Manifest file may be missing in case there are no DIST
+ * entries to be stored */
+ snprintf(path, sizeof(path), "%s/%s", dir, str_manifest);
+ if (!stat(path, &s))
+ update_times(tv, &s);
+ f = fopen(path, "r");
+ if (f != NULL) {
+ /* copy the DIST entries, we could do it unconditional, but this
+ * way we can re-run without producing invalid Manifests */
+ while (fgets(path, sizeof(path), f) != NULL) {
+ if (strncmp(path, "DIST ", 5) == 0)
+ if (fwrite(path, strlen(path), 1, m) != 1) {
+ fprintf(stderr, "failed to write to "
+ "%s/.Manifest.new: %s\n",
+ dir, strerror(errno));
+ fclose(f);
+ fclose(m);
+ return NULL;
+ }
+ }
+ fclose(f);
+ }
+
+ if (list_dir(&dentries, &dentrieslen, dir) == 0) {
+ for (i = 0; i < dentrieslen; i++) {
+ if (strcmp(dentries[i] + strlen(dentries[i]) - 7,
+ ".ebuild") != 0)
+ {
+ free(dentries[i]);
+ continue;
+ }
+ write_hashes(tv, dir, dentries[i], "EBUILD", m, NULL);
+ free(dentries[i]);
+ }
+ free(dentries);
+ }
+
+ write_hashes(tv, dir, "ChangeLog", "MISC", m, NULL);
+ write_hashes(tv, dir, "metadata.xml", "MISC", m, NULL);
+
+ fflush(m);
+ fclose(m);
+
+ snprintf(path, sizeof(path), "%s/%s", dir, str_manifest);
+ rename(newmanifest, path);
+
+ if (tv[0].tv_sec != 0) {
+ /* set Manifest and dir mtime to most recent file we found */
+ utimes(path, tv);
+ utimes(dir, tv);
+ }
+
+ return str_manifest;
+ } else {
+ return NULL;
+ }
+}
+
+static const char *
+process_dir_gen(const char *dir)
+{
+ char path[8192];
+ int newhashes;
+ int curdirfd;
+
+ snprintf(path, sizeof(path), "%s/metadata/layout.conf", dir);
+ if ((newhashes = parse_layout_conf(path)) != 0) {
+ hashes = newhashes;
+ } else {
+ return "generation must be done on a full tree";
+ }
+
+ if ((curdirfd = open(".", O_RDONLY)) < 0) {
+ fprintf(stderr, "cannot open current directory?!? %s\n",
+ strerror(errno));
+ }
+ if (chdir(dir) != 0) {
+ fprintf(stderr, "cannot chdir() to %s: %s\n", dir, strerror(errno));
+ return "not a directory";
+ }
+
+ if (generate_dir(".\0", GLOBAL_MANIFEST) == NULL)
+ return "generation failed";
+
+ /* return to where we were before we called this function */
+ fchdir(curdirfd);
+ close(curdirfd);
+
+ return NULL;
+}
+
+static void
+msgs_add(
+ verify_msg **msgs,
+ const char *manifest,
+ const char *ebuild,
+ const char *fmt, ...)
+{
+ char buf[4096];
+ int len;
+ va_list ap;
+ verify_msg *msg;
+
+ if (msgs == NULL || *msgs == NULL)
+ return;
+
+ msg = (*msgs)->next = xmalloc(sizeof(verify_msg));
+
+ len = snprintf(buf, sizeof(buf), "%s:%s:",
+ manifest ? manifest : "",
+ ebuild ? ebuild : "");
+
+ va_start(ap, fmt);
+ vsnprintf(buf + len, sizeof(buf) - len, fmt, ap);
+ va_end(ap);
+
+ msg->msg = xstrdup(buf);
+ msg->next = NULL;
+ *msgs = msg;
+}
+
+gpg_sig *
+verify_gpg_sig(const char *path, verify_msg **msgs)
+{
+ gpgme_ctx_t g_ctx;
+ gpgme_data_t manifest;
+ gpgme_data_t out;
+ gpgme_verify_result_t vres;
+ gpgme_signature_t sig;
+ gpgme_key_t key;
+ char buf[64];
+ FILE *f;
+ struct tm *ctime;
+ gpg_sig *ret = NULL;
+
+ if ((f = fopen(path, "r")) == NULL) {
+ msgs_add(msgs, path, NULL, "failed to open: %s", strerror(errno));
+ return NULL;
+ }
+
+ if (gpgme_new(&g_ctx) != GPG_ERR_NO_ERROR) {
+ msgs_add(msgs, path, NULL, "failed to create gpgme context");
+ return NULL;
+ }
+
+ if (gpgme_data_new(&out) != GPG_ERR_NO_ERROR) {
+ msgs_add(msgs, path, NULL, "failed to create gpgme data");
+ return NULL;
+ }
+
+ if (gpgme_data_new_from_stream(&manifest, f) != GPG_ERR_NO_ERROR) {
+ msgs_add(msgs, path, NULL,
+ "failed to create new gpgme data from stream");
+ return NULL;
+ }
+
+ if (gpgme_op_verify(g_ctx, manifest, NULL, out) != GPG_ERR_NO_ERROR) {
+ msgs_add(msgs, path, NULL, "failed to verify signature");
+ return NULL;
+ }
+
+ vres = gpgme_op_verify_result(g_ctx);
+ fclose(f);
+
+ if (vres == NULL || vres->signatures == NULL) {
+ msgs_add(msgs, path, NULL,
+ "verification failed due to a missing gpg keyring");
+ return NULL;
+ }
+
+ /* we only check/return the first signature */
+ if ((sig = vres->signatures) != NULL) {
+ ret = xmalloc(sizeof(gpg_sig));
+
+ if (sig->status != GPG_ERR_NO_PUBKEY) {
+ ret->algo = xstrdup(gpgme_pubkey_algo_name(sig->pubkey_algo));
+ snprintf(buf, sizeof(buf),
+ "%.4s %.4s %.4s %.4s %.4s %.4s %.4s %.4s %.4s %.4s",
+ sig->fpr + 0, sig->fpr + 4, sig->fpr + 8, sig->fpr + 12,
+ sig->fpr + 16, sig->fpr + 20, sig->fpr + 24, sig->fpr + 28,
+ sig->fpr + 32, sig->fpr + 36);
+ ret->fingerprint = xstrdup(buf);
+ ret->isgood = sig->status == GPG_ERR_NO_ERROR ? 1 : 0;
+ ctime = gmtime((time_t *)&sig->timestamp);
+ strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S UTC", ctime);
+ ret->timestamp = xstrdup(buf);
+
+ if (gpgme_get_key(g_ctx, sig->fpr, &key, 0) == GPG_ERR_NO_ERROR) {
+ if (key->uids != NULL)
+ ret->signer = xstrdup(key->uids->uid);
+ if (key->subkeys != NULL) {
+ snprintf(buf, sizeof(buf),
+ "%.4s %.4s %.4s %.4s %.4s "
+ "%.4s %.4s %.4s %.4s %.4s",
+ key->subkeys->fpr + 0, key->subkeys->fpr + 4,
+ key->subkeys->fpr + 8, key->subkeys->fpr + 12,
+ key->subkeys->fpr + 16, key->subkeys->fpr + 20,
+ key->subkeys->fpr + 24, key->subkeys->fpr + 28,
+ key->subkeys->fpr + 32, key->subkeys->fpr + 36);
+ ret->pkfingerprint = xstrdup(buf);
+ }
+ gpgme_key_release(key);
+ }
+ }
+
+ switch (sig->status) {
+ case GPG_ERR_NO_ERROR:
+ /* nothing */
+ ret->reason = NULL;
+ break;
+ case GPG_ERR_SIG_EXPIRED:
+ ret->reason = xstrdup("the signature is valid but expired");
+ break;
+ case GPG_ERR_KEY_EXPIRED:
+ ret->reason = xstrdup("the signature is valid but the key "
+ "used to verify the signature has expired");
+ break;
+ case GPG_ERR_CERT_REVOKED:
+ ret->reason = xstrdup("the signature is valid but the key "
+ "used to verify the signature has been revoked");
+ break;
+ case GPG_ERR_BAD_SIGNATURE:
+ printf("the signature is invalid\n");
+ break;
+ case GPG_ERR_NO_PUBKEY:
+ printf("the signature could not be verified due to a "
+ "missing key\n");
+ break;
+ default:
+ printf("there was some other error which prevented the "
+ "signature verification\n");
+ break;
+ }
+ }
+
+ gpgme_release(g_ctx);
+
+ return ret;
+}
+
+static size_t checked_manifests = 0;
+static size_t checked_files = 0;
+static size_t failed_files = 0;
+static char strict = 0;
+
+static char
+verify_file(const char *dir, char *mfline, const char *mfest, verify_msg **msgs)
+{
+ char *path;
+ char *size;
+ long long int fsize;
+ char *hashtype;
+ char *hash;
+ char *p;
+ char buf[8192];
+ size_t flen = 0;
+ char sha256[(SHA256_DIGEST_LENGTH * 2) + 1];
+ char sha512[(SHA512_DIGEST_LENGTH * 2) + 1];
+ char whrlpl[(WHIRLPOOL_DIGEST_LENGTH * 2) + 1];
+ char blak2b[(BLAKE2B_OUTBYTES * 2) + 1];
+ char ret = 0;
+
+ /* mfline is a Manifest file line with type and leading path
+ * stripped, something like:
+ * file <SIZE> <HASHTYPE HASH ...>
+ * we parse this, and verify the size and hashes */
+
+ path = mfline;
+ p = strchr(path, ' ');
+ if (p == NULL) {
+ msgs_add(msgs, mfest, NULL, "corrupt manifest line: %s", path);
+ return 1;
+ }
+ *p++ = '\0';
+
+ size = p;
+ p = strchr(size, ' ');
+ if (p == NULL) {
+ msgs_add(msgs, mfest, NULL, "corrupt manifest line, need size");
+ return 1;
+ }
+ *p++ = '\0';
+ fsize = strtoll(size, NULL, 10);
+ if (fsize == 0 && errno == EINVAL) {
+ msgs_add(msgs, mfest, NULL, "corrupt manifest line, "
+ "size is not a number: %s", size);
+ return 1;
+ }
+
+ sha256[0] = sha512[0] = whrlpl[0] = blak2b[0] = '\0';
+ snprintf(buf, sizeof(buf), "%s/%s", dir, path);
+ hash_compute_file(buf, sha256, sha512, whrlpl, blak2b, &flen);
+
+ if (flen == 0) {
+ msgs_add(msgs, mfest, path, "cannot open file!");
+ return 1;
+ }
+
+ checked_files++;
+
+ if (flen != (size_t)fsize) {
+ msgs_add(msgs, mfest, path,
+ "file size mismatch\n"
+ " got: %zd\n"
+ "expected: %lld",
+ flen, fsize);
+ failed_files++;
+ return 1;
+ }
+
+ /* now we are in free territory, we read TYPE HASH pairs until we
+ * drained the string, and match them against what we computed */
+ while (p != NULL && *p != '\0') {
+ hashtype = p;
+ p = strchr(hashtype, ' ');
+ if (p == NULL) {
+ msgs_add(msgs, mfest, path,
+ "corrupt manifest line, missing hash type");
+ return 1;
+ }
+ *p++ = '\0';
+
+ hash = p;
+ p = strchr(hash, ' ');
+ if (p != NULL)
+ *p++ = '\0';
+
+ if (strcmp(hashtype, "SHA256") == 0) {
+ if (!(hashes & HASH_SHA256)) {
+ if (strict)
+ msgs_add(msgs, mfest, path,
+ "hash SHA256 is not "
+ "enabled for this repository");
+ } else if (strcmp(hash, sha256) != 0) {
+ msgs_add(msgs, mfest, path,
+ "SHA256 hash mismatch\n"
+ "computed: '%s'\n"
+ "Manifest: '%s'",
+ sha256, hash);
+ ret = 1;
+ }
+ sha256[0] = '\0';
+ } else if (strcmp(hashtype, "SHA512") == 0) {
+ if (!(hashes & HASH_SHA512)) {
+ if (strict)
+ msgs_add(msgs, mfest, path,
+ "hash SHA512 is not "
+ "enabled for this repository");
+ } else if (strcmp(hash, sha512) != 0) {
+ msgs_add(msgs, mfest, path,
+ "SHA512 hash mismatch\n"
+ "computed: '%s'\n"
+ "Manifest: '%s'",
+ sha512, hash);
+ ret = 1;
+ }
+ sha512[0] = '\0';
+ } else if (strcmp(hashtype, "WHIRLPOOL") == 0) {
+ if (!(hashes & HASH_WHIRLPOOL)) {
+ if (strict)
+ msgs_add(msgs, mfest, path,
+ "hash WHIRLPOOL is not "
+ "enabled for this repository");
+ } else if (strcmp(hash, whrlpl) != 0) {
+ msgs_add(msgs, mfest, path,
+ "WHIRLPOOL hash mismatch\n"
+ "computed: '%s'\n"
+ "Manifest: '%s'",
+ whrlpl, hash);
+ ret = 1;
+ }
+ whrlpl[0] = '\0';
+ } else if (strcmp(hashtype, "BLAKE2B") == 0) {
+ if (!(hashes & HASH_BLAKE2B)) {
+ if (strict)
+ msgs_add(msgs, mfest, path,
+ "hash BLAKE2B is not "
+ "enabled for this repository");
+ } else if (strcmp(hash, blak2b) != 0) {
+ msgs_add(msgs, mfest, path,
+ "BLAKE2B hash mismatch\n"
+ "computed: '%s'\n"
+ "Manifest: '%s'",
+ blak2b, hash);
+ ret = 1;
+ }
+ blak2b[0] = '\0';
+ } else {
+ msgs_add(msgs, mfest, path, "unsupported hash: %s", hashtype);
+ ret = 1;
+ }
+ }
+
+ if (sha256[0] != '\0') {
+ msgs_add(msgs, mfest, path, "missing hash: SHA256");
+ ret = 1;
+ }
+ if (sha512[0] != '\0') {
+ msgs_add(msgs, mfest, path, "missing hash: SHA512");
+ ret = 1;
+ }
+ if (whrlpl[0] != '\0') {
+ msgs_add(msgs, mfest, path, "missing hash: WHIRLPOOL");
+ ret = 1;
+ }
+ if (blak2b[0] != '\0') {
+ msgs_add(msgs, mfest, path, "missing hash: BLAKE2B");
+ ret = 1;
+ }
+
+ failed_files += ret;
+ return ret;
+}
+
+static int
+compare_elems(const void *l, const void *r)
+{
+ const char *strl = *((const char **)l) + 2;
+ const char *strr = *((const char **)r) + 2;
+ unsigned char cl;
+ unsigned char cr;
+ /* compare treating / as end of string */
+ while ((cl = *strl++) == (cr = *strr++))
+ if (cl == '\0')
+ return 0;
+ if (cl == '/')
+ cl = '\0';
+ if (cr == '/')
+ cr = '\0';
+ return cl - cr;
+}
+
+struct subdir_workload {
+ size_t subdirlen;
+ size_t elemslen;
+ char **elems;
+};
+
+static char
+verify_dir(
+ const char *dir,
+ char **elems,
+ size_t elemslen,
+ size_t skippath,
+ const char *mfest,
+ verify_msg **msgs)
+{
+ char **dentries = NULL;
+ size_t dentrieslen = 0;
+ size_t curelem = 0;
+ size_t curdentry = 0;
+ char *entry;
+ char *slash;
+ char etpe;
+ char ret = 0;
+ int cmp;
+ struct subdir_workload **subdir = NULL;
+ size_t subdirsize = 0;
+ size_t subdirlen = 0;
+ size_t elem;
+
+ /* shortcut a single Manifest entry pointing to the same dir
+ * (happens at top-level) */
+ if (elemslen == 1 && skippath == 0 &&
+ **elems == 'M' && strchr(*elems + 2, '/') == NULL)
+ {
+ if ((ret = verify_file(dir, *elems + 2, mfest, msgs)) == 0) {
+ slash = strchr(*elems + 2, ' ');
+ if (slash != NULL)
+ *slash = '\0';
+ /* else, verify_manifest will fail, so ret will be handled */
+ ret = verify_manifest(dir, *elems + 2, msgs);
+ }
+ return ret;
+ }
+
+ /*
+ * We have a list of entries from the manifest just read, now we
+ * need to match these onto the directory layout. From what we got
+ * - we can ignore TIMESTAMP and DIST entries
+ * - IGNOREs need to be handled separate (shortcut)
+ * - MANIFESTs need to be handled on their own, for memory
+ * consumption reasons, we defer them to until we've verified
+ * what's left, we treat the path the Manifest refers to as IGNORE
+ * - DATAs, EBUILDs and MISCs needs verifying
+ * - AUXs need verifying, but in files/ subdir
+ * If we sort both lists, we should be able to do a merge-join, to
+ * easily flag missing entries in either list without hashing or
+ * anything.
+ */
+ if (list_dir(&dentries, &dentrieslen, dir) == 0) {
+ while (curdentry < dentrieslen) {
+ if (strcmp(dentries[curdentry], str_manifest) == 0 ||
+ strcmp(dentries[curdentry], str_manifest_gz) == 0 ||
+ strcmp(dentries[curdentry], str_manifest_files_gz) == 0)
+ {
+ curdentry++;
+ continue;
+ }
+
+ if (curelem < elemslen) {
+ entry = elems[curelem] + 2 + skippath;
+ etpe = *elems[curelem];
+ } else {
+ entry = (char *)"";
+ etpe = 'I';
+ }
+
+ /* handle subdirs first */
+ if ((slash = strchr(entry, '/')) != NULL) {
+ size_t sublen = slash - entry;
+ int elemstart = curelem;
+ char **subelems = &elems[curelem];
+
+ /* collect all entries like this one (same subdir) into
+ * a sub-list that we can verify */
+ curelem++;
+ while (curelem < elemslen &&
+ strncmp(entry, elems[curelem] + 2 + skippath,
+ sublen + 1) == 0)
+ curelem++;
+
+ if (subdirlen == subdirsize) {
+ subdirsize += LISTSZ;
+ subdir = realloc(subdir,
+ subdirsize * sizeof(subdir[0]));
+ if (subdir == NULL) {
+ msgs_add(msgs, mfest, NULL, "out of memory allocating "
+ "sublist for %.*s", (int)sublen, entry);
+ return 1;
+ }
+ }
+ subdir[subdirlen] = xmalloc(sizeof(struct subdir_workload));
+ subdir[subdirlen]->subdirlen = sublen;
+ subdir[subdirlen]->elemslen = curelem - elemstart;
+ subdir[subdirlen]->elems = subelems;
+ subdirlen++;
+
+ curelem--; /* move back, see below */
+
+ /* modify the last entry to be the subdir, such that we
+ * can let the code below synchronise with dentries */
+ elems[curelem][2 + skippath + sublen] = ' ';
+ entry = elems[curelem] + 2 + skippath;
+ etpe = 'S'; /* flag this was a subdir */
+ }
+
+ /* does this entry exist in list? */
+ if (*entry == '\0') {
+ /* end of list reached, force dir to catch up */
+ cmp = 1;
+ } else {
+ slash = strchr(entry, ' ');
+ if (slash != NULL)
+ *slash = '\0';
+ cmp = strcmp(entry, dentries[curdentry]);
+ if (slash != NULL)
+ *slash = ' ';
+ }
+ if (cmp == 0) {
+ /* equal, so yay */
+ if (etpe == 'D') {
+ ret |= verify_file(dir, entry, mfest, msgs);
+ }
+ /* else this is I(GNORE) or S(ubdir), which means it is
+ * ok in any way (M shouldn't happen) */
+ curelem++;
+ curdentry++;
+ } else if (cmp < 0) {
+ /* entry is missing from dir */
+ if (etpe == 'I') {
+ /* right, we can ignore this */
+ } else {
+ ret |= 1;
+ slash = strchr(entry, ' ');
+ if (slash != NULL)
+ *slash = '\0';
+ msgs_add(msgs, mfest, entry, "%s file listed in Manifest, "
+ "but not found", etpe == 'M' ? "MANIFEST" : "DATA");
+ if (slash != NULL)
+ *slash = ' ';
+ failed_files++;
+ }
+ curelem++;
+ } else if (cmp > 0) {
+ /* dir has extra element */
+ ret |= 1;
+ msgs_add(msgs, mfest, NULL,
+ "file not listed: %s", dentries[curdentry]);
+ curdentry++;
+ failed_files++;
+ }
+ }
+
+ while (dentrieslen-- > 0)
+ free(dentries[dentrieslen]);
+ free(dentries);
+
+#pragma omp parallel for shared(ret) private(entry, etpe, slash)
+ for (elem = 0; elem < subdirlen; elem++) {
+ char ndir[8192];
+
+ entry = subdir[elem]->elems[0] + 2 + skippath;
+ etpe = subdir[elem]->elems[0][0];
+
+ /* restore original entry format */
+ subdir[elem]->elems[subdir[elem]->elemslen - 1]
+ [2 + skippath + subdir[elem]->subdirlen] = '/';
+
+ if (etpe == 'M') {
+ size_t skiplen = strlen(dir) + 1 + subdir[elem]->subdirlen;
+ /* sub-Manifest, we need to do a proper recurse */
+ slash = strrchr(entry, '/'); /* cannot be NULL */
+ snprintf(ndir, sizeof(ndir), "%s/%s", dir, entry);
+ ndir[skiplen] = '\0';
+ slash = strchr(ndir + skiplen + 1, ' ');
+ if (slash != NULL) /* path should fit in ndir ... */
+ *slash = '\0';
+ if (verify_file(dir, entry, mfest, msgs) != 0 ||
+ verify_manifest(ndir, ndir + skiplen + 1, msgs) != 0)
+ ret |= 1;
+ } else {
+ snprintf(ndir, sizeof(ndir), "%s/%.*s", dir,
+ (int)subdir[elem]->subdirlen, entry);
+ ret |= verify_dir(ndir, subdir[elem]->elems,
+ subdir[elem]->elemslen,
+ skippath + subdir[elem]->subdirlen + 1, mfest, msgs);
+ }
+
+ free(subdir[elem]);
+ }
+
+ if (subdir)
+ free(subdir);
+
+ return ret;
+ } else {
+ return 1;
+ }
+}
+
+char
+verify_manifest(
+ const char *dir,
+ const char *manifest,
+ verify_msg **msgs)
+{
+ char buf[8192];
+ FILE *f;
+ gzFile mf;
+ char ret = 0;
+
+ size_t elemssize = 0;
+ size_t elemslen = 0;
+ char **elems = NULL;
+#define append_list(STR) \
+ if (strncmp(STR, "TIMESTAMP ", 10) != 0 || strncmp(STR, "DIST ", 5) != 0) {\
+ char *endp = STR + strlen(STR) - 1;\
+ while (isspace(*endp))\
+ *endp-- = '\0';\
+ if (elemslen == elemssize) {\
+ elemssize += LISTSZ;\
+ elems = xrealloc(elems, elemssize * sizeof(elems[0]));\
+ }\
+ if (strncmp(STR, "IGNORE ", 7) == 0) {\
+ STR[5] = 'I';\
+ elems[elemslen] = xstrdup(STR + 5);\
+ elemslen++;\
+ } else if (strncmp(STR, "MANIFEST ", 9) == 0) {\
+ STR[7] = 'M';\
+ elems[elemslen] = xstrdup(STR + 7);\
+ elemslen++;\
+ } else if (strncmp(STR, "DATA ", 5) == 0 ||\
+ strncmp(STR, "MISC ", 5) == 0 ||\
+ strncmp(STR, "EBUILD ", 7) == 0)\
+ {\
+ if (*STR == 'E') {\
+ STR[5] = 'D';\
+ elems[elemslen] = xstrdup(STR + 5);\
+ } else {\
+ STR[3] = 'D';\
+ elems[elemslen] = xstrdup(STR + 3);\
+ }\
+ elemslen++;\
+ } else if (strncmp(STR, "AUX ", 4) == 0) {\
+ /* translate directly into what it is: DATA in files/ */\
+ size_t slen = strlen(STR + 2) + sizeof("files/");\
+ elems[elemslen] = xmalloc(slen);\
+ snprintf(elems[elemslen], slen, "D files/%s", STR + 4);\
+ elemslen++;\
+ }\
+ }
+
+ snprintf(buf, sizeof(buf), "%s/%s", dir, manifest);
+ if (strcmp(manifest, str_manifest) == 0) {
+ if ((f = fopen(buf, "r")) == NULL) {
+ msgs_add(msgs, buf, NULL, "failed to open %s: %s\n",
+ manifest, strerror(errno));
+ return 1;
+ }
+ while (fgets(buf, sizeof(buf), f) != NULL) {
+ append_list(buf);
+ }
+ fclose(f);
+ } else if (strcmp(manifest, str_manifest_files_gz) == 0 ||
+ strcmp(manifest, str_manifest_gz) == 0)
+ {
+ if ((mf = gzopen(buf, "rb9")) == NULL) {
+ msgs_add(msgs, buf, NULL, "failed to open %s: %s\n",
+ manifest, strerror(errno));
+ return 1;
+ }
+ while (gzgets(mf, buf, sizeof(buf)) != NULL) {
+ append_list(buf);
+ }
+ gzclose(mf);
+ }
+
+ /* The idea:
+ * - Manifest without MANIFEST entries, we need to scan the entire
+ * subtree
+ * - Manifest with MANIFEST entries, assume they are just one level
+ * deeper, thus ignore that subdir, further like above
+ * - Manifest at top-level, needs to be igored as it only points to
+ * the larger Manifest.files.gz
+ */
+ qsort(elems, elemslen, sizeof(elems[0]), compare_elems);
+ snprintf(buf, sizeof(buf), "%s/%s", dir, manifest);
+ ret = verify_dir(dir, elems, elemslen, 0, buf + 2, msgs);
+ checked_manifests++;
+
+ while (elemslen-- > 0)
+ free(elems[elemslen]);
+ free(elems);
+
+ return ret;
+}
+
+char *
+verify_timestamp(const char *ts)
+{
+ char buf[8192];
+ FILE *f;
+ char *ret = NULL;
+
+ if ((f = fopen(ts, "r")) != NULL) {
+ while (fgets(buf, sizeof(buf), f) != NULL) {
+ if (strncmp(buf, "TIMESTAMP ", 10) == 0) {
+ char *endp = buf + strlen(buf) - 1;
+ while (isspace(*endp))
+ *endp-- = '\0';
+ ret = xstrdup(buf + 10);
+ break;
+ }
+ }
+ fclose(f);
+ }
+
+ return ret;
+}
+
+static void
+format_line(const char *pfx, const char *msg, int twidth)
+{
+ size_t msglen = strlen(pfx) + strlen(msg);
+
+ if (*pfx == '-') {
+ fprintf(stdout, "%s%s%s%s\n", pfx, RED, msg, NORM);
+ } else {
+ if (!verbose && msglen > (size_t)twidth) {
+ int to_remove = 3 + (msglen - twidth);
+ int first_half = msglen / 2 - to_remove / 2;
+ int remainder = msglen / 2 + (to_remove + 1) / 2;
+ fprintf(stdout, "%s%.*s...%s\n",
+ pfx, first_half, msg, msg + remainder);
+ } else {
+ fprintf(stdout, "%s%s\n", pfx, msg);
+ }
+ }
+}
+
+static const char *
+process_dir_vrfy(const char *dir)
+{
+ char buf[8192];
+ int newhashes;
+ const char *ret = NULL;
+ struct timeval startt;
+ struct timeval finisht;
+ double etime;
+ int curdirfd;
+ char *timestamp;
+ verify_msg topmsg;
+ verify_msg *walk = &topmsg;
+ gpg_sig *gs;
+ struct winsize winsz;
+ int twidth = 80;
+
+ ioctl(0, TIOCGWINSZ, &winsz);
+ if (winsz.ws_col > 0)
+ twidth = (int)winsz.ws_col;
+
+ gettimeofday(&startt, NULL);
+
+ fprintf(stdout, "verifying %s...\n", dir);
+ snprintf(buf, sizeof(buf), "%s/metadata/layout.conf", dir);
+ if ((newhashes = parse_layout_conf(buf)) != 0) {
+ hashes = newhashes;
+ } else {
+ return "verification must be done on a full tree";
+ }
+
+ if ((curdirfd = open(".", O_RDONLY)) < 0) {
+ fprintf(stderr, "cannot open current directory?!? %s\n",
+ strerror(errno));
+ }
+ if (chdir(dir) != 0) {
+ fprintf(stderr, "cannot chdir() to %s: %s\n", dir, strerror(errno));
+ return "not a directory";
+ }
+
+ if ((gs = verify_gpg_sig(str_manifest, &walk)) == NULL) {
+ ret = "gpg signature invalid";
+ } else {
+ fprintf(stdout,
+ "%s%s%s signature made %s by\n"
+ "%s\n"
+ "primary key fingerprint %s\n"
+ "%4s subkey fingerprint %s\n",
+ gs->isgood ? GREEN : RED,
+ gs->isgood ? "good": "BAD",
+ NORM, gs->timestamp,
+ gs->signer,
+ gs->pkfingerprint,
+ gs->algo, gs->fingerprint);
+ if (!gs->isgood)
+ fprintf(stdout, "reason: %s%s%s\n", RED, gs->reason, NORM);
+ free(gs->algo);
+ free(gs->fingerprint);
+ free(gs->timestamp);
+ free(gs->signer);
+ free(gs->pkfingerprint);
+ if (!gs->isgood)
+ free(gs->reason);
+ free(gs);
+ }
+
+ if ((timestamp = verify_timestamp(str_manifest)) != NULL) {
+ fprintf(stdout, "%s timestamp: %s\n", str_manifest, timestamp);
+ free(timestamp);
+ } else {
+ ret = "manifest timestamp entry missing";
+ }
+
+ /* verification goes like this:
+ * - verify the signature of the top-level Manifest file (done
+ * above)
+ * - read the contents of the Manifest file, and process the
+ * entries - verify them, check there are no files which shouldn't
+ * be there
+ * - recurse into directories for which Manifest files are defined
+ */
+ walk->next = NULL;
+ if (verify_manifest(".\0", str_manifest, &walk) != 0)
+ ret = "manifest verification failed";
+
+ {
+ char *mfest;
+ char *ebuild;
+ char *msg;
+ char *lastmfest = (char *)"-";
+ char *lastebuild = (char *)"-";
+ char *msgline;
+ const char *pfx;
+ verify_msg *next;
+
+ for (walk = topmsg.next; walk != NULL; walk = walk->next) {
+ mfest = walk->msg;
+ ebuild = strchr(mfest, ':');
+ if (ebuild != NULL) {
+ *ebuild++ = '\0';
+ msg = strchr(ebuild, ':');
+ if (msg != NULL)
+ *msg++ = '\0';
+ }
+ if (ebuild != NULL && msg != NULL) {
+ if (strcmp(mfest, lastmfest) != 0 ||
+ strcmp(ebuild, lastebuild) != 0)
+ {
+ char *mycat = mfest;
+ char *mypkg = NULL;
+
+ if ((mfest = strchr(mycat, '/')) != NULL) {
+ *mfest++ = '\0';
+ mypkg = mfest;
+ if ((mfest = strchr(mypkg, '/')) != NULL) {
+ *mfest++ = '\0';
+ } else {
+ mfest = mypkg;
+ mypkg = NULL;
+ }
+ } else {
+ mfest = mycat;
+ mycat = NULL;
+ }
+
+ fprintf(stdout, "%s%s%s" "%s%s%s%s" "%s%s" "%s%s%s%s\n",
+ mycat == NULL ? "" : BOLD,
+ mycat == NULL ? "" : mycat,
+ mycat == NULL ? "" : "/",
+ mypkg == NULL ? "" : BLUE,
+ mypkg == NULL ? "" : mypkg,
+ mypkg == NULL ? "" : NORM,
+ mypkg == NULL ? "" : "/",
+ mfest, *ebuild == '\0' ? ":" : "::",
+ CYAN, ebuild, NORM, *ebuild == '\0' ? "" : ":");
+ }
+
+ lastmfest = mfest;
+ lastebuild = ebuild;
+
+ pfx = "- ";
+ msgline = msg;
+ while ((msgline = strchr(msgline, '\n')) != NULL) {
+ *msgline++ = '\0';
+ format_line(pfx, msg, twidth);
+ pfx = " ";
+ msg = msgline;
+ }
+ format_line(pfx, msg, twidth);
+ }
+ }
+
+ walk = topmsg.next;
+ while (walk != NULL) {
+ next = walk->next;
+ free(walk);
+ walk = next;
+ }
+ }
+
+ gettimeofday(&finisht, NULL);
+
+ /* return to where we were before we called this function */
+ fchdir(curdirfd);
+ close(curdirfd);
+
+ etime = ((double)((finisht.tv_sec - startt.tv_sec) * 1000000 +
+ finisht.tv_usec) - (double)startt.tv_usec) / 1000000.0;
+ printf("checked %zd Manifests, %zd files, %zd failures in %.02fs\n",
+ checked_manifests, checked_files, failed_files, etime);
+ return ret;
+}
+
+int
+qmanifest_main(int argc, char **argv)
+{
+ char *prog;
+ const char *(*runfunc)(const char *);
+ int ret = 0;
+ const char *rsn;
+ bool isdir = false;
+ bool isoverlay = false;
+ char *overlay;
+ size_t n;
+ int i;
+
+ if ((prog = strrchr(argv[0], '/')) == NULL) {
+ prog = argv[0];
+ } else {
+ prog++;
+ }
+ if (*prog == 'q')
+ prog++;
+
+ runfunc = NULL;
+ if (strcmp(prog, "hashverify") == 0) {
+ runfunc = process_dir_vrfy;
+ } else if (strcmp(prog, "hashgen") == 0) {
+ runfunc = process_dir_gen;
+ }
+
+ while ((ret = GETOPT_LONG(QMANIFEST, qmanifest, "")) != -1) {
+ switch (ret) {
+ COMMON_GETOPTS_CASES(qmanifest)
+ case 'g': runfunc = process_dir_gen; break;
+ case 'd': isdir = true; break;
+ case 'o': isoverlay = true; break;
+ }
+ }
+
+ if (isdir && isoverlay) {
+ warn("cannot specify both directories (-d) and overlays (-o), "
+ "continuing using overlays only");
+ isdir = false;
+ }
+
+ if (runfunc == NULL)
+ /* default mode: verify */
+ runfunc = process_dir_vrfy;
+
+ gpgme_check_version(NULL);
+
+ if (isoverlay || (!isdir && !isoverlay)) {
+ char buf[_Q_PATH_MAX];
+ char *repo;
+ size_t repolen;
+
+ array_for_each(overlays, n, overlay) {
+ repo = xarrayget(overlay_names, n);
+ if (strcmp(repo, "<PORTDIR>") == 0) {
+ snprintf(buf, sizeof(buf), "%s/profiles/repo_name", overlay);
+ if (eat_file(buf, &repo, &repolen)) {
+ free(array_get_elem(overlays, n));
+ array_get_elem(overlays, n) = repo;
+ }
+ }
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+ for (i = 0; i < argc; i++) {
+ array_for_each(overlay_names, n, overlay) {
+ if (strcmp(overlay, argv[i]) == 0) {
+ overlay = xarrayget(overlays, n);
+ break;
+ }
+ overlay = NULL;
+ }
+
+ /* behaviour:
+ * if isdir is set: treat argument as directory
+ * if isoverlay is set: treat argument as overlay (look it up)
+ * if neither is set: treat argument as overlay if it exists,
+ * else as directory */
+
+ if (isoverlay && overlay == NULL) {
+ warn("no such overlay: %s", argv[i]);
+ continue;
+ }
+ if (isdir || (!isoverlay && overlay == NULL)) /* !isdir && !isoverlay */
+ overlay = argv[i];
+
+ rsn = runfunc(overlay);
+ if (rsn != NULL) {
+ printf("%s%s%s\n", RED, rsn, NORM);
+ ret |= 1;
+ }
+ }
+
+ if (i == 0) {
+ rsn = runfunc(main_overlay);
+ if (rsn != NULL) {
+ printf("%s%s%s\n", RED, rsn, NORM);
+ ret |= 1;
+ }
+ }
+
+ return ret;
+}
+
+#endif
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2019-05-21 14:12 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2019-05-21 14:11 [gentoo-commits] proj/portage-utils:master commit in: man/, man/include/, libq/, / Fabian Groffen
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox