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@xxxxxxxxxxxx < 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@xxxxxxxxxxxx < statement.txt > Good "file" signature for wiktor@xxxxxxxxxxxx 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
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.");
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@xxxxxxxxxxx " ; cat $pubkey) > $OBJ/allowed_signers + (printf "gregor.s@xxxxxxxxxxx " ; 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@xxxxxxxxxxx " ; 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@xxxxxxxxxxx " ; 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" | \
_______________________________________________ openssh-unix-dev mailing list openssh-unix-dev@xxxxxxxxxxx https://lists.mindrot.org/mailman/listinfo/openssh-unix-dev