Wiktor Kwapisiewicz
2025-Apr-23 11:39 UTC
Multiple allowed signer files in `ssh-keygen -Y verify`
Hello, I'm currently evaluating using `ssh-keygen -Y verify` to check OS artifacts (e.g. packages) and I noticed that the `-f allowed_signers_file` option can be passed only once. A side remark: technically it can be passed multiple times without a warning but the last invocation overrides all previous ones. Tested using: $ ssh-keygen -Y verify -f allowed_signers -f /dev/null -n file -s statement.txt.sig -I wiktor at metacode.biz < statement.txt Could not verify signature. While this works (note the order of -f's): $ ssh-keygen -Y verify -f /dev/null -f allowed_signers -n file -s statement.txt.sig -I wiktor at metacode.biz < statement.txt Good "file" signature for wiktor at metacode.biz with RSA key SHA256:xb+QgBmoSdveobEdwKqUb3BCk9SLJVxq3Ltu2o/FK7U This is a little bit limiting since it doesn't allow splitting the signers file into multiple locations that may be managed independently. For example: a distro's keys file would be managed by a system package while additional user/local keys could be in a separate one, managed by the system administrator / end user. Of course, this could be workarounded by careful concatenation of files before passing them to "verify" (inserting newlines between files etc.). Just for comparison the Stateless OpenPGP CLI spec allows passing multiple CERTS files [0] directly in the command-line. A similar problem appears in the "File Hierarchy for the Verification of OS Artifacts (VOA)" draft specification [1] which suggests putting each key in a separate file (CC'ing David, who is leading this). In my opinion allowing multiple "-f" files would cleanly solve all these issues but I'd like to hear what you think about it and if there are any (potentially better) alternatives? Thanks for your time! Kind regards, Wiktor [0]: https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli#name-verify-verify-detached-sign [1]: https://github.com/uapi-group/specifications/pull/134/files#diff-c79d1da1ef2fcfffc28ac7308505535eac0942d086d54a990553374ac81fed00R383
Damien Miller
2025-Apr-29 02:58 UTC
Multiple allowed signer files in `ssh-keygen -Y verify`
On Wed, 23 Apr 2025, Wiktor Kwapisiewicz via openssh-unix-dev wrote:> Hello, > > I'm currently evaluating using `ssh-keygen -Y verify` to check OS artifacts > (e.g. packages) and I noticed that the `-f allowed_signers_file` option can be > passed only once. A side remark: technically it can be passed multiple times > without a warning but the last invocation overrides all previous ones. Tested > using: > > $ ssh-keygen -Y verify -f allowed_signers -f /dev/null -n file -s > statement.txt.sig -I wiktor at metacode.biz < statement.txt > Could not verify signature. > > While this works (note the order of -f's): > > $ ssh-keygen -Y verify -f /dev/null -f allowed_signers -n file -s > statement.txt.sig -I wiktor at metacode.biz < statement.txt > Good "file" signature for wiktor at metacode.biz with RSA key > SHA256:xb+QgBmoSdveobEdwKqUb3BCk9SLJVxq3Ltu2o/FK7U > > This is a little bit limiting since it doesn't allow splitting the signers > file into multiple locations that may be managed independently. For example: a > distro's keys file would be managed by a system package while additional > user/local keys could be in a separate one, managed by the system > administrator / end user. > > Of course, this could be workarounded by careful concatenation of files before > passing them to "verify" (inserting newlines between files etc.). > > Just for comparison the Stateless OpenPGP CLI spec allows passing multiple > CERTS files [0] directly in the command-line. > > A similar problem appears in the "File Hierarchy for the Verification of OS > Artifacts (VOA)" draft specification [1] which suggests putting each key in a > separate file (CC'ing David, who is leading this). > > In my opinion allowing multiple "-f" files would cleanly solve all these > issues but I'd like to hear what you think about it and if there are any > (potentially better) alternatives?I think it's reasonable to allow multiple allowed signers files. These patches implement this. Please take a look to see if I've missed any test cases. -d -------------- next part -------------- diff --git a/ssh-keygen.c b/ssh-keygen.c index 9aac967..0b0587d 100644 --- a/ssh-keygen.c +++ b/ssh-keygen.c @@ -91,6 +91,10 @@ static int fingerprint_hash = SSH_FP_HASH_DEFAULT; static char identity_file[PATH_MAX]; static int have_identity = 0; +/* Some operations accept multiple files */ +static char **identity_files; +static size_t nidentity_files; + /* This is set to the passphrase if given on the command line. */ static char *identity_passphrase = NULL; @@ -2803,16 +2807,17 @@ done: static int sig_verify(const char *signature, const char *sig_namespace, - const char *principal, const char *allowed_keys, const char *revoked_keys, - char * const *opts, size_t nopts) + const char *principal, char **allowed_keys, size_t nallowed_keys, + const char *revoked_keys, char * const *opts, size_t nopts) { - int r, ret = -1; + int r, ret = -1, matched = 0; int print_pubkey = 0; struct sshbuf *sigbuf = NULL, *abuf = NULL; struct sshkey *sign_key = NULL; char *fp = NULL; struct sshkey_sig_details *sig_details = NULL; uint64_t verify_time = 0; + size_t i; if (sig_process_opts(opts, nopts, NULL, &verify_time, &print_pubkey) != 0) @@ -2850,9 +2855,23 @@ sig_verify(const char *signature, const char *sig_namespace, } } - if (allowed_keys != NULL && (r = sshsig_check_allowed_keys(allowed_keys, - sign_key, principal, sig_namespace, verify_time)) != 0) { - debug3_fr(r, "sshsig_check_allowed_keys"); + for (i = 0; i < nallowed_keys; i++) { + if ((r = sshsig_check_allowed_keys(allowed_keys[i], sign_key, + principal, sig_namespace, verify_time)) != 0) { + /* don't attempt other files on hard errors */ + if (r != SSH_ERR_KEY_NOT_FOUND) { + error_fr(r, "check allowed keys in %s", + allowed_keys[i]); + goto done; + } + debug3_fr(r, "sshsig_check_allowed_keys in %s", + allowed_keys[i]); + continue; + } + matched = 1; + } + if (!matched && nallowed_keys != 0) { + error_f("No key matched in allowed signers file(s)"); goto done; } /* success */ @@ -2894,14 +2913,15 @@ done: } static int -sig_find_principals(const char *signature, const char *allowed_keys, - char * const *opts, size_t nopts) +sig_find_principals(const char *signature, char **allowed_keys_files, + size_t nallowed_keys_files, char * const *opts, size_t nopts) { int r, ret = -1; struct sshbuf *sigbuf = NULL, *abuf = NULL; struct sshkey *sign_key = NULL; - char *principals = NULL, *cp, *tmp; + char *principals = NULL, *output = NULL, *cp, *tmp; uint64_t verify_time = 0; + size_t i; if (sig_process_opts(opts, nopts, NULL, &verify_time, NULL) != 0) goto done; /* error already logged */ @@ -2918,53 +2938,76 @@ sig_find_principals(const char *signature, const char *allowed_keys, error_fr(r, "sshsig_get_pubkey"); goto done; } - if ((r = sshsig_find_principals(allowed_keys, sign_key, - verify_time, &principals)) != 0) { - if (r != SSH_ERR_KEY_NOT_FOUND) - error_fr(r, "sshsig_find_principal"); - goto done; - } - ret = 0; -done: - if (ret == 0 ) { - /* Emit matching principals one per line */ + + for (i = 0; i < nallowed_keys_files; i++) { + if ((r = sshsig_find_principals(allowed_keys_files[i], sign_key, + verify_time, &principals)) != 0) { + /* don't attempt other files on hard errors */ + if (r != SSH_ERR_KEY_NOT_FOUND) { + error_fr(r, "find principals in %s", + allowed_keys_files[i]); + goto done; + } + debug_fr(r, "find principals in %s", + allowed_keys_files[i]); + continue; + } + /* Record matching principals one per line */ tmp = principals; while ((cp = strsep(&tmp, ",")) != NULL && *cp != '\0') - puts(cp); - } else { - fprintf(stderr, "No principal matched.\n"); + xextendf(&output, "\n", "%s", cp); + free(principals); } + if (output != NULL) { + printf("%s\n", output); + ret = 0; + } else + fprintf(stderr, "No principal matched.\n"); +done: sshbuf_free(sigbuf); sshbuf_free(abuf); sshkey_free(sign_key); - free(principals); + free(output); return ret; } static int -sig_match_principals(const char *allowed_keys, char *principal, - char * const *opts, size_t nopts) +sig_match_principals(char **allowed_keys, size_t nallowed_keys, + char *principal, char * const *opts, size_t nopts) { - int r; - char **principals = NULL; - size_t i, nprincipals = 0; + int r, ret = -1; + char **principals = NULL, *output = NULL; + size_t i, j, nprincipals = 0; if ((r = sig_process_opts(opts, nopts, NULL, NULL, NULL)) != 0) return r; /* error already logged */ - if ((r = sshsig_match_principals(allowed_keys, principal, - &principals, &nprincipals)) != 0) { - debug_f("match: %s", ssh_err(r)); + for (i = 0; i < nallowed_keys; i++) { + if ((r = sshsig_match_principals(allowed_keys[i], principal, + &principals, &nprincipals)) != 0) { + /* don't attempt other files on hard errors */ + if (r != SSH_ERR_KEY_NOT_FOUND) { + error_fr(r, "match principals in %s", + allowed_keys[i]); + goto done; + } + debug_fr(r, "match in %s", allowed_keys[i]); + continue; + } + for (j = 0; j < nprincipals; j++) { + xextendf(&output, "\n", "%s", principals[j]); + free(principals[j]); + } + free(principals); + } + if (output != NULL) { + printf("%s\n", output); + ret = 0; + } else fprintf(stderr, "No principal matched.\n"); - return r; - } - for (i = 0; i < nprincipals; i++) { - printf("%s\n", principals[i]); - free(principals[i]); - } - free(principals); - - return 0; + done: + free(output); + return ret; } static void @@ -3478,6 +3521,11 @@ main(int argc, char **argv) sizeof(identity_file)) >= sizeof(identity_file)) fatal("Identity filename too long"); have_identity = 1; + /* Some operations accept multiple filenames */ + identity_files = xrecallocarray(identity_files, + nidentity_files, nidentity_files + 1, + sizeof(*identity_files)); + identity_files[nidentity_files++] = xstrdup(optarg); break; case 'g': print_generic = 1; @@ -3608,17 +3656,17 @@ main(int argc, char **argv) if (sign_op != NULL) { if (strncmp(sign_op, "find-principals", 15) == 0) { if (ca_key_path == NULL) { - error("Too few arguments for find-principals:" + fatal("Too few arguments for find-principals:" "missing signature file"); exit(1); } if (!have_identity) { - error("Too few arguments for find-principals:" + fatal("Too few arguments for find-principals:" "missing allowed keys file"); exit(1); } - return sig_find_principals(ca_key_path, identity_file, - opts, nopts); + return sig_find_principals(ca_key_path, identity_files, + nidentity_files, opts, nopts); } else if (strncmp(sign_op, "match-principals", 16) == 0) { if (!have_identity) { error("Too few arguments for match-principals:" @@ -3630,8 +3678,8 @@ main(int argc, char **argv) "missing principal ID"); exit(1); } - return sig_match_principals(identity_file, cert_key_id, - opts, nopts); + return sig_match_principals(identity_files, + nidentity_files, cert_key_id, opts, nopts); } else if (strncmp(sign_op, "sign", 4) == 0) { /* NB. cert_principals is actually namespace, via -n */ if (cert_principals == NULL || @@ -3645,6 +3693,10 @@ main(int argc, char **argv) "missing key"); exit(1); } + if (nidentity_files > 1) { + error("Too many keys specified for sign"); + exit(1); + } return sig_sign(identity_file, cert_principals, prefer_agent, argc, argv, opts, nopts); } else if (strncmp(sign_op, "check-novalidate", 16) == 0) { @@ -3660,8 +3712,13 @@ main(int argc, char **argv) "missing signature file"); exit(1); } + if (nidentity_files > 0) { + error("Too many keys specified " + "for check-novalidate"); + exit(1); + } return sig_verify(ca_key_path, cert_principals, - NULL, NULL, NULL, opts, nopts); + NULL, NULL, 0, NULL, opts, nopts); } else if (strncmp(sign_op, "verify", 6) == 0) { /* NB. cert_principals is actually namespace, via -n */ if (cert_principals == NULL || @@ -3676,7 +3733,7 @@ main(int argc, char **argv) exit(1); } if (!have_identity) { - error("Too few arguments for sign: " + error("Too few arguments for verify: " "missing allowed keys file"); exit(1); } @@ -3686,14 +3743,19 @@ main(int argc, char **argv) exit(1); } return sig_verify(ca_key_path, cert_principals, - cert_key_id, identity_file, rr_hostname, - opts, nopts); + cert_key_id, identity_files, nidentity_files, + rr_hostname, opts, nopts); } error("Unsupported operation for -Y: \"%s\"", sign_op); usage(); /* NOTREACHED */ } + /* All other operations accept only a single keyfile */ + if (nidentity_files > 1) { + error("Too many keys specified on commandline"); + exit(1); + } if (ca_key_path != NULL) { if (argc < 1 && !gen_krl) { error("Too few arguments."); -------------- next part -------------- diff --git a/sshsig.sh b/sshsig.sh index dae0370..1a31ac7 100644 --- a/sshsig.sh +++ b/sshsig.sh @@ -6,7 +6,7 @@ tid="sshsig" DATA2=$OBJ/${DATANAME}.2 cat ${DATA} ${DATA} > ${DATA2} -rm -f $OBJ/sshsig-*.sig $OBJ/wrong-key* $OBJ/sigca-key* +rm -f $OBJ/sshsig-*.sig $OBJ/wrong-key* $OBJ/sigca-key* $OBJ/allowed_signers* sig_namespace="test-$$" sig_principal="user-$$@example.com" @@ -66,11 +66,22 @@ for t in $SIGNKEYS; do $hashalg_arg < $DATA > $sigfile 2>/dev/null || \ fail "sign using $t / $h failed" (printf "$sig_principal " ; cat $pubkey) > $OBJ/allowed_signers + echo "" > $OBJ/allowed_signers.empty trace "$tid: key type $t verify with hash $h" ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ -I $sig_principal -f $OBJ/allowed_signers \ < $DATA >/dev/null 2>&1 || \ fail "failed signature for $t / $h key" + # Multiple allowed_signers files + ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ + -I $sig_principal -f $OBJ/allowed_signers.empty \ + -f $OBJ/allowed_signers < $DATA >/dev/null 2>&1 || \ + fail "failed signature for $t / $h key (multifile)" + # Opposite order + ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ + -I $sig_principal -f $OBJ/allowed_signers \ + -f $OBJ/allowed_signers.empty < $DATA >/dev/null 2>&1 || \ + fail "failed signature for $t / $h key (multifile2)" done trace "$tid: key type $t verify with limited namespace" @@ -104,10 +115,29 @@ for t in $SIGNKEYS; do # Wrong key trusted. trace "$tid: key type $t verify with wrong key" (printf "$sig_principal " ; cat $WRONG) > $OBJ/allowed_signers + (printf "$sig_principal " ; cat $WRONG) > $OBJ/allowed_signers.2 ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ -I $sig_principal -f $OBJ/allowed_signers \ < $DATA >/dev/null 2>&1 && \ fail "accepted signature for $t key with wrong key trusted" + # Multiple files. + ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ + -I $sig_principal -f $OBJ/allowed_signers \ + -f $OBJ/allowed_signers.2 < $DATA >/dev/null 2>&1 && \ + fail "accepted signature for $t key with wrong key trusted2" + + # Wrong key in one file shouldn't stop correct key in other working. + trace "$tid: key type $t verify with right and wrong keys" + (printf "$sig_principal " ; cat $WRONG) > $OBJ/allowed_signers + (printf "$sig_principal " ; cat $pubkey) > $OBJ/allowed_signers.2 + ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ + -I $sig_principal -f $OBJ/allowed_signers \ + -f $OBJ/allowed_signers.2 < $DATA >/dev/null 2>&1 || \ + fail "failed signature for $t key with right+wrong key trusted" + ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ + -I $sig_principal -f $OBJ/allowed_signers \ + -f $OBJ/allowed_signers.2 < $DATA >/dev/null 2>&1 || \ + fail "failed signature for $t key with right+wrong key trusted2" # incorrect data trace "$tid: key type $t verify with wrong data" @@ -120,10 +150,37 @@ for t in $SIGNKEYS; do # wrong principal in signers trace "$tid: key type $t verify with wrong principal" (printf "josef.k at example.com " ; cat $pubkey) > $OBJ/allowed_signers + (printf "gregor.s at example.com " ; cat $pubkey) > $OBJ/allowed_signers.2 ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ -I $sig_principal -f $OBJ/allowed_signers \ < $DATA >/dev/null 2>&1 && \ fail "accepted signature for $t key with wrong principal" + # Multiple files. + ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ + -I $sig_principal -f $OBJ/allowed_signers \ + -f $OBJ/allowed_signers.2 < $DATA >/dev/null 2>&1 && \ + fail "accepted signature for $t key with wrong principal2" + + # Right and wrong principals in same file. + trace "$tid: key type $t verify with right and wrong principal" + (printf "josef.k at example.com " ; cat $pubkey) > $OBJ/allowed_signers + (printf "$sig_principal " ; cat $pubkey) >> $OBJ/allowed_signers + ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ + -I $sig_principal -f $OBJ/allowed_signers \ + < $DATA >/dev/null 2>&1 || \ + fail "failed signature for $t key with right/wrong principal" + # Same but across different files. + (printf "josef.k at example.com " ; cat $pubkey) > $OBJ/allowed_signers + (printf "$sig_principal " ; cat $pubkey) > $OBJ/allowed_signers.2 + ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ + -I $sig_principal -f $OBJ/allowed_signers \ + -f $OBJ/allowed_signers.2 < $DATA >/dev/null 2>&1 || \ + fail "failed signature for $t key with right/wrong principal2" + # Opposite ordering. + ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \ + -I $sig_principal -f $OBJ/allowed_signers.2 \ + -f $OBJ/allowed_signers < $DATA >/dev/null 2>&1 || \ + fail "failed signature for $t key with right/wrong principal3" # wrong namespace trace "$tid: key type $t verify with wrong namespace" @@ -174,19 +231,46 @@ for t in $SIGNKEYS; do < $DATA >/dev/null 2>&1 && \ fail "failed signature for $t with expired key" - # key lifespan valid + ( printf "$sig_principal " ; + printf "valid-after=\"19800101\",valid-before=\"19900101\" " ; + cat $pubkey) > $OBJ/allowed_signers + echo "" > $OBJ/allowed_signers.2 + ( printf "$sig_principal " ; + printf "valid-after=\"20000101\",valid-before=\"20100101\" " ; + cat $pubkey) > $OBJ/allowed_signers.3 + + # find-principals: principal not found + trace "$tid: key type $t find-principals missing" + ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ + -Overify-time="19990101" \ + -f $OBJ/allowed_signers.2 >/dev/null 2>&1 && \ + fail "passed find-principals for $t missing" + + # find-principals: key lifespan valid trace "$tid: key type $t find-principals with valid lifespan" ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ -Overify-time="19850101" \ -f $OBJ/allowed_signers >/dev/null 2>&1 || \ - fail "failed find-principals for $t key with valid expiry interval" - # key not yet valid + fail "failed find-principals for $t key valid expiry interval" + # same but multiple files. + ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ + -Overify-time="19850101" -f $OBJ/allowed_signers \ + -f $OBJ/allowed_signers.2 >/dev/null 2>&1 || \ + fail "failed find-principals for $t key valid expiry interval2" + # opposite order. + ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ + -Overify-time="19850101" -f $OBJ/allowed_signers.2 \ + -f $OBJ/allowed_signers >/dev/null 2>&1 || \ + fail "failed find-principals for $t key valid expiry interval2" + + # find-principals: key not yet valid trace "$tid: key type $t find principals with not-yet-valid lifespan" ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ -Overify-time="19790101" \ -f $OBJ/allowed_signers >/dev/null 2>&1 && \ fail "failed find-principals for $t not-yet-valid key" - # key expired + + # find-principals: key expired trace "$tid: key type $t find-principals with expired lifespan" ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ -Overify-time="19990101" \ @@ -197,6 +281,33 @@ for t in $SIGNKEYS; do ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ -f $OBJ/allowed_signers >/dev/null 2>&1 && \ fail "failed find-principals for $t with expired key" + # Multiple files + ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ + -Overify-time="19990101" -f $OBJ/allowed_signers \ + -f $OBJ/allowed_signers.2 >/dev/null 2>&1 && \ + fail "failed find-principals for $t with expired key2" + # opposite order. + ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ + -Overify-time="19990101" -f $OBJ/allowed_signers.2 \ + -f $OBJ/allowed_signers >/dev/null 2>&1 && \ + fail "failed find-principals for $t with expired key3" + + # find-principals: key expired in one file but not the other + trace "$tid: key type $t find-principals with expired lifespan" + ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ + -Overify-time="19990101" -f $OBJ/allowed_signers \ + -f $OBJ/allowed_signers.3 >/dev/null 2>&1 && \ + fail "failed find-principals for $t with expired key (multi)" + # Valid date + ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ + -Overify-time="20050101" -f $OBJ/allowed_signers \ + -f $OBJ/allowed_signers.3 >/dev/null 2>&1 || \ + fail "failed find-principals for $t with expired key (multi2)" + # Opposite order. + ${SSHKEYGEN} -vvv -Y find-principals -s $sigfile \ + -Overify-time="20050101" -f $OBJ/allowed_signers.3 \ + -f $OBJ/allowed_signers >/dev/null 2>&1 || \ + fail "failed find-principals for $t with expired key (multi2)" # public key in revoked keys file trace "$tid: key type $t verify with revoked key" @@ -513,11 +624,22 @@ done printf "princi* " ; cat $pubkey; printf "unique " ; cat $pubkey; ) > $OBJ/allowed_signers +echo > $OBJ/allowed_signers.2 verbose "$tid: match principals" ${SSHKEYGEN} -Y match-principals -f $OBJ/allowed_signers -I "unique" | \ fgrep "unique" >/dev/null || \ fail "failed to match static principal" +# Multiple files +${SSHKEYGEN} -Y match-principals -f $OBJ/allowed_signers \ + -f $OBJ/allowed_signers.2 -I "unique" | \ + fgrep "unique" >/dev/null || \ + fail "failed to match static principal (multi)" +# Opposite order +${SSHKEYGEN} -Y match-principals -f $OBJ/allowed_signers.2 \ + -f $OBJ/allowed_signers -I "unique" | \ + fgrep "unique" >/dev/null || \ + fail "failed to match static principal (multi2)" trace "$tid: match principals wildcard" ${SSHKEYGEN} -Y match-principals -f $OBJ/allowed_signers -I "princip" | \
Maybe Matching Threads
- Multiple allowed signer files in `ssh-keygen -Y verify`
- Question about ssh-keygen -Y find-principals
- [klibc:update-dash] expand: 'nolog' and 'debug' options cause "$-" to wreak havoc
- [klibc:update-dash] dash: expand: 'nolog' and 'debug' options cause "$-" to wreak havoc
- [PATCH 06/10] builder: split Index_parser.index in an own module