Skip to content

Commit

Permalink
gpg-interface: add minTrustLevel as a configuration option
Browse files Browse the repository at this point in the history
Previously, signature verification for merge and pull operations checked
if the key had a trust-level of either TRUST_NEVER or TRUST_UNDEFINED in
verify_merge_signature().  If that was the case, the process die()d.

The other code paths that did signature verification relied entirely on
the return code from check_commit_signature().  And signatures made with
a good key, irregardless of its trust level, was considered valid by
check_commit_signature().

This difference in behavior might induce users to erroneously assume
that the trust level of a key in their keyring is always considered by
Git, even for operations where it is not (e.g. during a verify-commit or
verify-tag).

The way it worked was by gpg-interface.c storing the result from the
key/signature status *and* the lowest-two trust levels in the `result`
member of the signature_check structure (the last of these status lines
that were encountered got written to `result`).  These are documented in
GPG under the subsection `General status codes` and `Key related`,
respectively [1].

The GPG documentation says the following on the TRUST_ status codes [1]:

    """
    These are several similar status codes:

    - TRUST_UNDEFINED <error_token>
    - TRUST_NEVER     <error_token>
    - TRUST_MARGINAL  [0  [<validation_model>]]
    - TRUST_FULLY     [0  [<validation_model>]]
    - TRUST_ULTIMATE  [0  [<validation_model>]]

    For good signatures one of these status lines are emitted to
    indicate the validity of the key used to create the signature.
    The error token values are currently only emitted by gpgsm.
    """

My interpretation is that the trust level is conceptionally different
from the validity of the key and/or signature.  That seems to also have
been the assumption of the old code in check_signature() where a result
of 'G' (as in GOODSIG) and 'U' (as in TRUST_NEVER or TRUST_UNDEFINED)
were both considered a success.

The two cases where a result of 'U' had special meaning were in
verify_merge_signature() (where this caused git to die()) and in
format_commit_one() (where it affected the output of the %G? format
specifier).

I think it makes sense to refactor the processing of TRUST_ status lines
such that users can configure a minimum trust level that is enforced
globally, rather than have individual parts of git (e.g. merge) do it
themselves (except for a grace period with backward compatibility).

I also think it makes sense to not store the trust level in the same
struct member as the key/signature status.  While the presence of a
TRUST_ status code does imply that the signature is good (see the first
paragraph in the included snippet above), as far as I can tell, the
order of the status lines from GPG isn't well-defined; thus it would
seem plausible that the trust level could be overwritten with the
key/signature status if they were stored in the same member of the
signature_check structure.

This patch introduces a new configuration option: gpg.minTrustLevel.  It
consolidates trust-level verification to gpg-interface.c and adds a new
`trust_level` member to the signature_check structure.

Backward-compatibility is maintained by introducing a special case in
verify_merge_signature() such that if no user-configurable
gpg.minTrustLevel is set, then the old behavior of rejecting
TRUST_UNDEFINED and TRUST_NEVER is enforced.  If, on the other hand,
gpg.minTrustLevel is set, then that value overrides the old behavior.

Similarly, the %G? format specifier will continue show 'U' for
signatures made with a key that has a trust level of TRUST_UNDEFINED or
TRUST_NEVER, even though the 'U' character no longer exist in the
`result` member of the signature_check structure.  A new format
specifier, %GT, is also introduced for users that want to show all
possible trust levels for a signature.

Another approach would have been to simply drop the trust-level
requirement in verify_merge_signature().  This would also have made the
behavior consistent with other parts of git that perform signature
verification.  However, requiring a minimum trust level for signing keys
does seem to have a real-world use-case.  For example, the build system
used by the Qubes OS project currently parses the raw output from
verify-tag in order to assert a minimum trust level for keys used to
sign git tags [2].

[1] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/doc/DETAILS;h=bd00006e933ac56719b1edd2478ecd79273eae72;hb=refs/heads/master
[2] https://github.com/QubesOS/qubes-builder/blob/9674c1991deef45b1a1b1c71fddfab14ba50dccf/scripts/verify-git-tag#L43

Signed-off-by: Hans Jerry Illikainen <hji@dyntopia.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
  • Loading branch information
illikainen authored and gitster committed Jan 15, 2020
1 parent c58ae96 commit 54887b4
Show file tree
Hide file tree
Showing 13 changed files with 319 additions and 23 deletions.
15 changes: 15 additions & 0 deletions Documentation/config/gpg.txt
Expand Up @@ -18,3 +18,18 @@ gpg.<format>.program::
chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
be used as a legacy synonym for `gpg.openpgp.program`. The default
value for `gpg.x509.program` is "gpgsm".

gpg.minTrustLevel::
Specifies a minimum trust level for signature verification. If
this option is unset, then signature verification for merge
operations require a key with at least `marginal` trust. Other
operations that perform signature verification require a key
with at least `undefined` trust. Setting this option overrides
the required trust-level for all operations. Supported values,
in increasing order of significance:
+
* `undefined`
* `never`
* `marginal`
* `fully`
* `ultimate`
1 change: 1 addition & 0 deletions Documentation/pretty-formats.txt
Expand Up @@ -213,6 +213,7 @@ endif::git-rev-list[]
'%GF':: show the fingerprint of the key used to sign a signed commit
'%GP':: show the fingerprint of the primary key whose subkey was used
to sign a signed commit
'%GT':: show the trust level for the key used to sign a signed commit
'%gD':: reflog selector, e.g., `refs/stash@{1}` or `refs/stash@{2
minutes ago}`; the format follows the rules described for the
`-g` option. The portion before the `@` is the refname as
Expand Down
9 changes: 7 additions & 2 deletions builtin/merge.c
Expand Up @@ -62,6 +62,7 @@ static int show_diffstat = 1, shortlog_len = -1, squash;
static int option_commit = -1;
static int option_edit = -1;
static int allow_trivial = 1, have_message, verify_signatures;
static int check_trust_level = 1;
static int overwrite_ignore = 1;
static struct strbuf merge_msg = STRBUF_INIT;
static struct strategy **use_strategies;
Expand Down Expand Up @@ -631,6 +632,8 @@ static int git_merge_config(const char *k, const char *v, void *cb)
} else if (!strcmp(k, "commit.gpgsign")) {
sign_commit = git_config_bool(k, v) ? "" : NULL;
return 0;
} else if (!strcmp(k, "gpg.mintrustlevel")) {
check_trust_level = 0;
}

status = fmt_merge_msg_config(k, v, cb);
Expand Down Expand Up @@ -1397,7 +1400,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
die(_("Can merge only exactly one commit into empty head"));

if (verify_signatures)
verify_merge_signature(remoteheads->item, verbosity);
verify_merge_signature(remoteheads->item, verbosity,
check_trust_level);

remote_head_oid = &remoteheads->item->object.oid;
read_empty(remote_head_oid, 0);
Expand All @@ -1420,7 +1424,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)

if (verify_signatures) {
for (p = remoteheads; p; p = p->next) {
verify_merge_signature(p->item, verbosity);
verify_merge_signature(p->item, verbosity,
check_trust_level);
}
}

Expand Down
13 changes: 12 additions & 1 deletion builtin/pull.c
Expand Up @@ -107,6 +107,7 @@ static char *opt_ff;
static char *opt_verify_signatures;
static int opt_autostash = -1;
static int config_autostash;
static int check_trust_level = 1;
static struct argv_array opt_strategies = ARGV_ARRAY_INIT;
static struct argv_array opt_strategy_opts = ARGV_ARRAY_INIT;
static char *opt_gpg_sign;
Expand Down Expand Up @@ -355,14 +356,23 @@ static enum rebase_type config_get_rebase(void)
*/
static int git_pull_config(const char *var, const char *value, void *cb)
{
int status;

if (!strcmp(var, "rebase.autostash")) {
config_autostash = git_config_bool(var, value);
return 0;
} else if (!strcmp(var, "submodule.recurse")) {
recurse_submodules = git_config_bool(var, value) ?
RECURSE_SUBMODULES_ON : RECURSE_SUBMODULES_OFF;
return 0;
} else if (!strcmp(var, "gpg.mintrustlevel")) {
check_trust_level = 0;
}

status = git_gpg_config(var, value, cb);
if (status)
return status;

return git_default_config(var, value, cb);
}

Expand Down Expand Up @@ -587,7 +597,8 @@ static int pull_into_void(const struct object_id *merge_head,
die(_("unable to access commit %s"),
oid_to_hex(merge_head));

verify_merge_signature(commit, opt_verbosity);
verify_merge_signature(commit, opt_verbosity,
check_trust_level);
}

/*
Expand Down
12 changes: 7 additions & 5 deletions commit.c
Expand Up @@ -1136,21 +1136,23 @@ int check_commit_signature(const struct commit *commit, struct signature_check *
return ret;
}

void verify_merge_signature(struct commit *commit, int verbosity)
void verify_merge_signature(struct commit *commit, int verbosity,
int check_trust)
{
char hex[GIT_MAX_HEXSZ + 1];
struct signature_check signature_check;
int ret;
memset(&signature_check, 0, sizeof(signature_check));

check_commit_signature(commit, &signature_check);
ret = check_commit_signature(commit, &signature_check);

find_unique_abbrev_r(hex, &commit->object.oid, DEFAULT_ABBREV);
switch (signature_check.result) {
case 'G':
if (ret || (check_trust && signature_check.trust_level < TRUST_MARGINAL))
die(_("Commit %s has an untrusted GPG signature, "
"allegedly by %s."), hex, signature_check.signer);
break;
case 'U':
die(_("Commit %s has an untrusted GPG signature, "
"allegedly by %s."), hex, signature_check.signer);
case 'B':
die(_("Commit %s has a bad GPG signature "
"allegedly by %s."), hex, signature_check.signer);
Expand Down
12 changes: 11 additions & 1 deletion commit.h
Expand Up @@ -383,8 +383,18 @@ int compare_commits_by_author_date(const void *a_, const void *b_, void *unused)
* Verify a single commit with check_commit_signature() and die() if it is not
* a good signature. This isn't really suitable for general use, but is a
* helper to implement consistent logic for pull/merge --verify-signatures.
*
* The check_trust parameter is meant for backward-compatibility. The GPG
* interface verifies key trust with a default trust level that is below the
* default trust level for merge operations. Its value should be non-zero if
* the user hasn't set a minimum trust level explicitly in their configuration.
*
* If the user has set a minimum trust level, then that value should be obeyed
* and check_trust should be zero, even if the configured trust level is below
* the default trust level for merges.
*/
void verify_merge_signature(struct commit *commit, int verbose);
void verify_merge_signature(struct commit *commit, int verbose,
int check_trust);

int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused);
int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);
Expand Down
91 changes: 79 additions & 12 deletions gpg-interface.c
Expand Up @@ -7,6 +7,8 @@
#include "tempfile.h"

static char *configured_signing_key;
static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;

struct gpg_format {
const char *name;
const char *program;
Expand Down Expand Up @@ -85,6 +87,8 @@ void signature_check_clear(struct signature_check *sigc)
#define GPG_STATUS_UID (1<<2)
/* The status includes key fingerprints */
#define GPG_STATUS_FINGERPRINT (1<<3)
/* The status includes trust level */
#define GPG_STATUS_TRUST_LEVEL (1<<4)

/* Short-hand for standard exclusive *SIG status with keyid & UID */
#define GPG_STATUS_STDSIG (GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID|GPG_STATUS_UID)
Expand All @@ -96,13 +100,23 @@ static struct {
} sigcheck_gpg_status[] = {
{ 'G', "GOODSIG ", GPG_STATUS_STDSIG },
{ 'B', "BADSIG ", GPG_STATUS_STDSIG },
{ 'U', "TRUST_NEVER", 0 },
{ 'U', "TRUST_UNDEFINED", 0 },
{ 'E', "ERRSIG ", GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID },
{ 'X', "EXPSIG ", GPG_STATUS_STDSIG },
{ 'Y', "EXPKEYSIG ", GPG_STATUS_STDSIG },
{ 'R', "REVKEYSIG ", GPG_STATUS_STDSIG },
{ 0, "VALIDSIG ", GPG_STATUS_FINGERPRINT },
{ 0, "TRUST_", GPG_STATUS_TRUST_LEVEL },
};

static struct {
const char *key;
enum signature_trust_level value;
} sigcheck_gpg_trust_level[] = {
{ "UNDEFINED", TRUST_UNDEFINED },
{ "NEVER", TRUST_NEVER },
{ "MARGINAL", TRUST_MARGINAL },
{ "FULLY", TRUST_FULLY },
{ "ULTIMATE", TRUST_ULTIMATE },
};

static void replace_cstring(char **field, const char *line, const char *next)
Expand All @@ -115,6 +129,20 @@ static void replace_cstring(char **field, const char *line, const char *next)
*field = NULL;
}

static int parse_gpg_trust_level(const char *level,
enum signature_trust_level *res)
{
size_t i;

for (i = 0; i < ARRAY_SIZE(sigcheck_gpg_trust_level); i++) {
if (!strcmp(sigcheck_gpg_trust_level[i].key, level)) {
*res = sigcheck_gpg_trust_level[i].value;
return 0;
}
}
return 1;
}

static void parse_gpg_output(struct signature_check *sigc)
{
const char *buf = sigc->gpg_status;
Expand All @@ -136,9 +164,18 @@ static void parse_gpg_output(struct signature_check *sigc)
/* Iterate over all search strings */
for (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {
if (skip_prefix(line, sigcheck_gpg_status[i].check, &line)) {
/*
* GOODSIG, BADSIG etc. can occur only once for
* each signature. Therefore, if we had more
* than one then we're dealing with multiple
* signatures. We don't support them
* currently, and they're rather hard to
* create, so something is likely fishy and we
* should reject them altogether.
*/
if (sigcheck_gpg_status[i].flags & GPG_STATUS_EXCLUSIVE) {
if (seen_exclusive_status++)
goto found_duplicate_status;
goto error;
}

if (sigcheck_gpg_status[i].result)
Expand All @@ -154,6 +191,25 @@ static void parse_gpg_output(struct signature_check *sigc)
replace_cstring(&sigc->signer, line, next);
}
}

/* Do we have trust level? */
if (sigcheck_gpg_status[i].flags & GPG_STATUS_TRUST_LEVEL) {
/*
* GPG v1 and v2 differs in how the
* TRUST_ lines are written. Some
* trust lines contain no additional
* space-separated information for v1.
*/
size_t trust_size = strcspn(line, " \n");
char *trust = xmemdupz(line, trust_size);

if (parse_gpg_trust_level(trust, &sigc->trust_level)) {
free(trust);
goto error;
}
free(trust);
}

/* Do we have fingerprint? */
if (sigcheck_gpg_status[i].flags & GPG_STATUS_FINGERPRINT) {
const char *limit;
Expand Down Expand Up @@ -191,14 +247,7 @@ static void parse_gpg_output(struct signature_check *sigc)
}
return;

found_duplicate_status:
/*
* GOODSIG, BADSIG etc. can occur only once for each signature.
* Therefore, if we had more than one then we're dealing with multiple
* signatures. We don't support them currently, and they're rather
* hard to create, so something is likely fishy and we should reject
* them altogether.
*/
error:
sigc->result = 'E';
/* Clear partial data to avoid confusion */
FREE_AND_NULL(sigc->primary_key_fingerprint);
Expand All @@ -215,6 +264,7 @@ int check_signature(const char *payload, size_t plen, const char *signature,
int status;

sigc->result = 'N';
sigc->trust_level = -1;

status = verify_signed_buffer(payload, plen, signature, slen,
&gpg_output, &gpg_status);
Expand All @@ -224,7 +274,8 @@ int check_signature(const char *payload, size_t plen, const char *signature,
sigc->gpg_output = strbuf_detach(&gpg_output, NULL);
sigc->gpg_status = strbuf_detach(&gpg_status, NULL);
parse_gpg_output(sigc);
status |= sigc->result != 'G' && sigc->result != 'U';
status |= sigc->result != 'G';
status |= sigc->trust_level < configured_min_trust_level;

out:
strbuf_release(&gpg_status);
Expand Down Expand Up @@ -271,6 +322,8 @@ int git_gpg_config(const char *var, const char *value, void *cb)
{
struct gpg_format *fmt = NULL;
char *fmtname = NULL;
char *trust;
int ret;

if (!strcmp(var, "user.signingkey")) {
if (!value)
Expand All @@ -290,6 +343,20 @@ int git_gpg_config(const char *var, const char *value, void *cb)
return 0;
}

if (!strcmp(var, "gpg.mintrustlevel")) {
if (!value)
return config_error_nonbool(var);

trust = xstrdup_toupper(value);
ret = parse_gpg_trust_level(trust, &configured_min_trust_level);
free(trust);

if (ret)
return error("unsupported value for %s: %s", var,
value);
return 0;
}

if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program"))
fmtname = "openpgp";

Expand Down
10 changes: 9 additions & 1 deletion gpg-interface.h
Expand Up @@ -7,6 +7,14 @@ struct strbuf;
#define GPG_VERIFY_RAW 2
#define GPG_VERIFY_OMIT_STATUS 4

enum signature_trust_level {
TRUST_UNDEFINED,
TRUST_NEVER,
TRUST_MARGINAL,
TRUST_FULLY,
TRUST_ULTIMATE,
};

struct signature_check {
char *payload;
char *gpg_output;
Expand All @@ -16,7 +24,6 @@ struct signature_check {
* possible "result":
* 0 (not checked)
* N (checked but no further result)
* U (untrusted good)
* G (good)
* B (bad)
*/
Expand All @@ -25,6 +32,7 @@ struct signature_check {
char *key;
char *fingerprint;
char *primary_key_fingerprint;
enum signature_trust_level trust_level;
};

void signature_check_clear(struct signature_check *sigc);
Expand Down

0 comments on commit 54887b4

Please sign in to comment.