Multiple allowed signer files in `ssh-keygen -Y verify`
Damien Miller
djm at mindrot.org
Tue Apr 29 12:58:53 AEST 2025
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" | \
More information about the openssh-unix-dev
mailing list