commit: f1d02fbf01683c42ddb0cdfbfe7815c5ff37e035 Author: Fabian Groffen <grobian <AT> gentoo <DOT> org> AuthorDate: Fri May 24 11:58:26 2019 +0000 Commit: Fabian Groffen <grobian <AT> gentoo <DOT> org> CommitDate: Fri May 24 11:58:26 2019 +0000 URL: https://gitweb.gentoo.org/proj/portage-utils.git/commit/?id=f1d02fbf
qmanifest: allow GPG-signing top-level Manifest Signed-off-by: Fabian Groffen <grobian <AT> gentoo.org> man/include/qmanifest-01-generation.include | 17 ++ man/include/qmanifest.optdesc.yaml | 8 + man/qmanifest.1 | 30 ++- qmanifest.c | 214 ++++++++++++++++----- tests/qmanifest/dotest | 16 +- tests/qmanifest/manifest04.good | 3 +- tests/qmanifest/manifest07.good | 11 +- .../1F0A2C7F1E80A6EEEA3B9C30068FB3349702B3A7.key | Bin 0 -> 1171 bytes .../E37F9F3C8E4A940C625EC65B7070255F4AAA55F9.key | Bin 0 -> 1155 bytes tests/qmanifest/root/.gnupg/pubring.kbx | Bin 0 -> 1435 bytes tests/qmanifest/root/.gnupg/random_seed | Bin 0 -> 600 bytes tests/qmanifest/root/.gnupg/trustdb.gpg | Bin 0 -> 1280 bytes 12 files changed, 233 insertions(+), 66 deletions(-) diff --git a/man/include/qmanifest-01-generation.include b/man/include/qmanifest-01-generation.include new file mode 100644 index 0000000..5a24a02 --- /dev/null +++ b/man/include/qmanifest-01-generation.include @@ -0,0 +1,17 @@ +.SH "GENERATING A SIGNED TREE" +.PP +By default, \fBqmanifest\fR will not try to sign the top-level Manifest +when it generating thick Manifests. A tree as such isn't completely +valid (as it misses the final signature), but still correct. To sign +the top-level Manifest, the \fB-s\fR flag needs to be used to provide +the GPG keyid to sign with. The passphrase is requested by \fBgpg\fR(1) +itself, unless the \fB-p\fR flag is given, in which case \fBqmanifest\fR +attempts to read the passphrase from \fIstdin\fR and then pass that +passphrase onto \fBgpg\fR. This is useful for scenarios in which the +signing of a tree is scripted. +.PP +To generate a tree signed by GPG keyid \fI0x123567ABC\fR using +passphrase \fImypasswd\fR, one could use: +.nf\fI + $ echo mypasswd | qmanifest -g -s 0x123567ABC -p /path/to/tree +.fi diff --git a/man/include/qmanifest.optdesc.yaml b/man/include/qmanifest.optdesc.yaml new file mode 100644 index 0000000..8bf1ce7 --- /dev/null +++ b/man/include/qmanifest.optdesc.yaml @@ -0,0 +1,8 @@ +signas: | + Sign generated Manifest using GPG key. This key must exist in your + keyring and be valid for signing. +passphrase: | + Ask for GPG key password (instead of relying on gpg-agent). While + this option is not very useful compared to gpg's ways of gathering a + password, it is mainly intended for automated setups where the + password is piped in using \fIstdin\fR. diff --git a/man/qmanifest.1 b/man/qmanifest.1 index e223122..15027f6 100644 --- a/man/qmanifest.1 +++ b/man/qmanifest.1 @@ -38,7 +38,17 @@ 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. +Generate thick Manifests. +.TP +\fB\-s\fR \fI<arg>\fR, \fB\-\-signas\fR \fI<arg>\fR +Sign generated Manifest using GPG key. This key must exist in your +keyring and be valid for signing. +.TP +\fB\-p\fR, \fB\-\-passphrase\fR +Ask for GPG key password (instead of relying on gpg-agent). While +this option is not very useful compared to gpg's ways of gathering a +password, it is mainly intended for automated setups where the +password is piped in using \fIstdin\fR. .TP \fB\-d\fR, \fB\-\-dir\fR Treat arguments as directories. @@ -63,7 +73,23 @@ Print this help and exit. .TP \fB\-V\fR, \fB\-\-version\fR Print version and exit. - +.SH "GENERATING A SIGNED TREE" +.PP +By default, \fBqmanifest\fR will not try to sign the top-level Manifest +when it generating thick Manifests. A tree as such isn't completely +valid (as it misses the final signature), but still correct. To sign +the top-level Manifest, the \fB-s\fR flag needs to be used to provide +the GPG keyid to sign with. The passphrase is requested by \fBgpg\fR(1) +itself, unless the \fB-p\fR flag is given, in which case \fBqmanifest\fR +attempts to read the passphrase from \fIstdin\fR and then pass that +passphrase onto \fBgpg\fR. This is useful for scenarios in which the +signing of a tree is scripted. +.PP +To generate a tree signed by GPG keyid \fI0x123567ABC\fR using +passphrase \fImypasswd\fR, one could use: +.nf\fI + $ echo mypasswd | qmanifest -g -s 0x123567ABC -p /path/to/tree +.fi .SH "REPORTING BUGS" Please report bugs via http://bugs.gentoo.org/ .br diff --git a/qmanifest.c b/qmanifest.c index 88352fa..ed203a6 100644 --- a/qmanifest.c +++ b/qmanifest.c @@ -39,15 +39,19 @@ #include "eat_file.h" #include "hash.h" -#define QMANIFEST_FLAGS "gdo" COMMON_FLAGS +#define QMANIFEST_FLAGS "gs:pdo" COMMON_FLAGS static struct option const qmanifest_long_opts[] = { {"generate", no_argument, NULL, 'g'}, + {"signas", a_argument, NULL, 's'}, + {"passphrase", no_argument, NULL, 'p'}, {"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", + "Generate thick Manifests", + "Sign generated Manifest using GPG key", + "Ask for GPG key password (instead of relying on gpg-agent)", "Treat arguments as directories", "Treat arguments as overlay names", COMMON_OPTS_HELP @@ -55,6 +59,8 @@ static const char * const qmanifest_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; +static char *gpg_sign_key = NULL; +static bool gpg_get_password = false; /* linked list structure to hold verification complaints */ typedef struct verify_msg { @@ -688,37 +694,124 @@ generate_dir(const char *dir, enum type_manifest mtype) } } +static gpgme_error_t +gpgme_pw_cb(void *opaque, const char *uid_hint, const char *pw_info, + int last_was_bad, int fd) +{ + char *pass = (char *)opaque; + size_t passlen = strlen(pass); + ssize_t ret; + + (void)uid_hint; + (void)pw_info; + (void)last_was_bad; + + do { + ret = write(fd, pass, passlen); + if (ret > 0) { + pass += ret; + passlen -= ret; + } + } while (passlen > 0 && ret > 0); + + return passlen == 0 ? GPG_ERR_NO_ERROR : gpgme_error_from_errno(errno); +} + static const char * -process_dir_gen(const char *dir) +process_dir_gen(void) { char path[_Q_PATH_MAX]; int newhashes; - int curdirfd; + struct termios termio; + char *gpg_pass; - snprintf(path, sizeof(path), "%s%s/metadata/layout.conf", portroot, dir); - if ((newhashes = parse_layout_conf(path)) != 0) { + if ((newhashes = parse_layout_conf("metadata/layout.conf")) != 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)); - } - snprintf(path, sizeof(path), "%s%s", portroot, dir); - if (chdir(path) != 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 */ - if (fchdir(curdirfd) != 0 && verbose > 1) - warn("could not move back to original directory"); - close(curdirfd); + if (gpg_sign_key != NULL) { + gpgme_ctx_t gctx; + gpgme_error_t gerr; + gpgme_key_t gkey; + gpgme_data_t manifest; + gpgme_data_t out; + FILE *f; + size_t dlen; + + gerr = gpgme_new(&gctx); + if (gerr != GPG_ERR_NO_ERROR) + return "GPG setup failed"; + + gerr = gpgme_get_key(gctx, gpg_sign_key, &gkey, 0); + if (gerr != GPG_ERR_NO_ERROR) + return "failed to get GPG key"; + gerr = gpgme_signers_add(gctx, gkey); + if (gerr != GPG_ERR_NO_ERROR) + return "failed to add GPG key to sign list, is it a suitable key?"; + + gpg_pass = NULL; + if (gpg_get_password) { + if (isatty(fileno(stdin))) { + /* disable terminal echo; the printing of what you type */ + tcgetattr(fileno(stdin), &termio); + termio.c_lflag &= ~ECHO; + tcsetattr(fileno(stdin), TCSANOW, &termio); + + printf("Password for GPG-key %s: ", gpg_sign_key); + } + + gpg_pass = fgets(path, sizeof(path), stdin); + + if (isatty(fileno(stdin))) { + printf("\n"); + /* restore echoing, for what it's worth */ + termio.c_lflag |= ECHO; + tcsetattr(fileno(stdin), TCSANOW, &termio); + } + + if (gpg_pass == NULL || *gpg_pass == '\0') + warn("no GPG password given, gpg might ask for it again"); + /* continue for the case where gpg-agent holds the pass */ + else { + gpgme_set_pinentry_mode(gctx, GPGME_PINENTRY_MODE_LOOPBACK); + gpgme_set_passphrase_cb(gctx, gpgme_pw_cb, gpg_pass); + } + } + + if ((f = fopen(str_manifest, "r+")) == NULL) + return "could not open top-level Manifest file"; + + /* finally, sign the Manifest */ + if (gpgme_data_new_from_stream(&manifest, f) != GPG_ERR_NO_ERROR) + return "failed to create GPG data from Manifest"; + + if (gpgme_data_new(&out) != GPG_ERR_NO_ERROR) + return "failed to create GPG output buffer"; + + gerr = gpgme_op_sign(gctx, manifest, out, GPGME_SIG_MODE_CLEAR); + if (gerr != GPG_ERR_NO_ERROR) { + warn("%s: %s", gpgme_strsource(gerr), gpgme_strerror(gerr)); + return "failed to GPG sign Manifest"; + } + + /* write back signed Manifest */ + rewind(f); + gpgme_data_seek(out, 0, SEEK_SET); + do { + dlen = gpgme_data_read(out, path, sizeof(path)); + fwrite(path, dlen, 1, f); + } while (dlen == sizeof(path)); + fclose(f); + + gpgme_data_release(out); + gpgme_data_release(manifest); + gpgme_release(gctx); + } return NULL; } @@ -854,13 +947,19 @@ verify_gpg_sig(const char *path, verify_msg **msgs) "used to verify the signature has been revoked"); break; case GPG_ERR_BAD_SIGNATURE: + free(ret); + ret = NULL; printf("the signature is invalid\n"); break; case GPG_ERR_NO_PUBKEY: + free(ret); + ret = NULL; printf("the signature could not be verified due to a " "missing key\n"); break; default: + free(ret); + ret = NULL; printf("there was some other error which prevented the " "signature verification\n"); break; @@ -1414,7 +1513,7 @@ format_line(const char *pfx, const char *msg, int twidth) } static const char * -process_dir_vrfy(const char *dir) +process_dir_vrfy(void) { char buf[8192]; int newhashes; @@ -1422,7 +1521,6 @@ process_dir_vrfy(const char *dir) struct timeval startt; struct timeval finisht; double etime; - int curdirfd; char *timestamp; verify_msg topmsg; verify_msg *walk = &topmsg; @@ -1436,35 +1534,25 @@ process_dir_vrfy(const char *dir) gettimeofday(&startt, NULL); - snprintf(buf, sizeof(buf), "%s%s/metadata/layout.conf", portroot, dir); + snprintf(buf, sizeof(buf), "metadata/layout.conf"); 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)); - } - snprintf(buf, sizeof(buf), "%s%s", portroot, dir); - if (chdir(buf) != 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" + " %s%s%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, + DKBLUE, gs->signer, NORM, gs->pkfingerprint, gs->algo, gs->fingerprint); if (!gs->isgood) @@ -1575,11 +1663,6 @@ process_dir_vrfy(const char *dir) gettimeofday(&finisht, NULL); - /* return to where we were before we called this function */ - if (fchdir(curdirfd) != 0 && verbose > 1) - warn("could not move back to original directory"); - 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", @@ -1591,15 +1674,17 @@ int qmanifest_main(int argc, char **argv) { char *prog; - const char *(*runfunc)(const char *); + const char *(*runfunc)(void); int ret; const char *rsn; bool isdir = false; bool isoverlay = false; char *overlay; char path[_Q_PATH_MAX]; + char path2[_Q_PATH_MAX]; size_t n; int i; + int curdirfd; if ((prog = strrchr(argv[0], '/')) == NULL) { prog = argv[0]; @@ -1620,6 +1705,8 @@ qmanifest_main(int argc, char **argv) switch (ret) { COMMON_GETOPTS_CASES(qmanifest) case 'g': runfunc = process_dir_gen; break; + case 's': gpg_sign_key = optarg; break; + case 'p': gpg_get_password = true; break; case 'd': isdir = true; break; case 'o': isoverlay = true; break; } @@ -1653,6 +1740,9 @@ qmanifest_main(int argc, char **argv) } } + if ((curdirfd = open(".", O_RDONLY)) < 0) + warn("cannot open current directory?!? %s\n", strerror(errno)); + ret = EXIT_SUCCESS; argc -= optind; argv += optind; @@ -1679,20 +1769,27 @@ qmanifest_main(int argc, char **argv) if (isdir || (!isoverlay && overlay == NULL)) /* !isdir && !isoverlay */ overlay = argv[i]; - if (runfunc == process_dir_vrfy) - printf("verifying %s%s%s...\n", BOLD, overlay, NORM); - if (*overlay != '/') { if (portroot[1] == '\0') { /* resolve the path */ + (void)fchdir(curdirfd); (void)realpath(overlay, path); } else { snprintf(path, sizeof(path), "./%s", overlay); } - overlay = path; } - rsn = runfunc(overlay); + snprintf(path2, sizeof(path2), "%s%s", portroot, path); + if (chdir(path2) != 0) { + warn("cannot change directory to %s: %s", overlay, strerror(errno)); + ret |= 1; + continue; + } + + if (runfunc == process_dir_vrfy) + printf("verifying %s%s%s...\n", BOLD, overlay, NORM); + + rsn = runfunc(); if (rsn != NULL) { printf("%s%s%s\n", RED, rsn, NORM); ret |= 2; @@ -1700,15 +1797,28 @@ qmanifest_main(int argc, char **argv) } if (i == 0) { - if (runfunc == process_dir_vrfy) - printf("verifying %s%s%s...\n", BOLD, main_overlay, NORM); - rsn = runfunc(main_overlay); - if (rsn != NULL) { - printf("%s%s%s\n", RED, rsn, NORM); - ret |= 2; + snprintf(path, sizeof(path), "%s%s", portroot, main_overlay); + if (chdir(path) != 0) { + warn("cannot change directory to %s: %s", + main_overlay, strerror(errno)); + ret |= 1; + } else { + if (runfunc == process_dir_vrfy) + printf("verifying %s%s%s...\n", BOLD, main_overlay, NORM); + + rsn = runfunc(); + if (rsn != NULL) { + printf("%s%s%s\n", RED, rsn, NORM); + ret |= 2; + } } } + /* return to where we were before we called this function */ + if (fchdir(curdirfd) != 0 && verbose > 1) + warn("could not move back to original directory"); + close(curdirfd); + return ret; } diff --git a/tests/qmanifest/dotest b/tests/qmanifest/dotest index 636a723..fb2aa22 100755 --- a/tests/qmanifest/dotest +++ b/tests/qmanifest/dotest @@ -36,7 +36,7 @@ test 02 2 "qmanifest not_a_tree" test 03 2 "qmanifest notatree" # dir test -test 04 2 "qmanifest -d not_a_tree" +test 04 1 "qmanifest -d not_a_tree" # overlay test test 05 1 "qmanifest -o notatree" @@ -44,11 +44,19 @@ test 05 1 "qmanifest -o notatree" # generate a valid tree rm -Rf testtree cp -r "${ROOT}/simpletree" testtree || echo try it anyway +# make it a fully valid tree +export HOME=${ROOT} # for gnupg home +rm testtree/my-cat/mypackage/unrecorded-file unset ROOT PORTAGE_CONFIGROOT -test 06 0 "qmanifest -g testtree" +SIGNAS=0x3D695C8C0F87966B62DC5AFCDCFABA8E07F52261 +KEYPASS=qmanifest +test 06 0 "echo ${KEYPASS} | qmanifest -g -s ${SIGNAS} -p testtree" -# validate the just generated tree (doesn't do GPG signing hence fails) -test 07 0 "qmanifest testtree | sed '/Manifest timestamp/d'" +# validate the just generated tree +test 07 0 "qmanifest testtree | sed -e '/Manifest timestamp/d' -e 's/made .* UTC by/made by/'" + +# shut down agents and whatnot +gpgconf --kill all cleantmpdir diff --git a/tests/qmanifest/manifest04.good b/tests/qmanifest/manifest04.good index 17c6a1f..4831674 100644 --- a/tests/qmanifest/manifest04.good +++ b/tests/qmanifest/manifest04.good @@ -1,2 +1 @@ -verifying not_a_tree... -verification must be done on a full tree +manifest: cannot change directory to not_a_tree: No such file or directory diff --git a/tests/qmanifest/manifest07.good b/tests/qmanifest/manifest07.good index 67176c5..6347806 100644 --- a/tests/qmanifest/manifest07.good +++ b/tests/qmanifest/manifest07.good @@ -1,7 +1,6 @@ verifying testtree... -Manifest: -- failed to verify signature -my-cat/mypackage/Manifest: -- file not listed: unrecorded-file -checked 5 Manifests, 9 files, 1 failures -manifest verification failed +good signature made by + Qmanifest Test Key +primary key fingerprint 3D69 5C8C 0F87 966B 62DC 5AFC DCFA BA8E 07F5 2261 + RSA subkey fingerprint 3D69 5C8C 0F87 966B 62DC 5AFC DCFA BA8E 07F5 2261 +checked 5 Manifests, 9 files, 0 failures diff --git a/tests/qmanifest/root/.gnupg/private-keys-v1.d/1F0A2C7F1E80A6EEEA3B9C30068FB3349702B3A7.key b/tests/qmanifest/root/.gnupg/private-keys-v1.d/1F0A2C7F1E80A6EEEA3B9C30068FB3349702B3A7.key new file mode 100644 index 0000000..b4ed767 Binary files /dev/null and b/tests/qmanifest/root/.gnupg/private-keys-v1.d/1F0A2C7F1E80A6EEEA3B9C30068FB3349702B3A7.key differ diff --git a/tests/qmanifest/root/.gnupg/private-keys-v1.d/E37F9F3C8E4A940C625EC65B7070255F4AAA55F9.key b/tests/qmanifest/root/.gnupg/private-keys-v1.d/E37F9F3C8E4A940C625EC65B7070255F4AAA55F9.key new file mode 100644 index 0000000..4b07401 Binary files /dev/null and b/tests/qmanifest/root/.gnupg/private-keys-v1.d/E37F9F3C8E4A940C625EC65B7070255F4AAA55F9.key differ diff --git a/tests/qmanifest/root/.gnupg/pubring.kbx b/tests/qmanifest/root/.gnupg/pubring.kbx new file mode 100644 index 0000000..848dc93 Binary files /dev/null and b/tests/qmanifest/root/.gnupg/pubring.kbx differ diff --git a/tests/qmanifest/root/.gnupg/random_seed b/tests/qmanifest/root/.gnupg/random_seed new file mode 100644 index 0000000..d32d054 Binary files /dev/null and b/tests/qmanifest/root/.gnupg/random_seed differ diff --git a/tests/qmanifest/root/.gnupg/trustdb.gpg b/tests/qmanifest/root/.gnupg/trustdb.gpg new file mode 100644 index 0000000..78308c6 Binary files /dev/null and b/tests/qmanifest/root/.gnupg/trustdb.gpg differ
