git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
* [PATCH 0/1] gpg-interface: add minTrustLevel as a configuration option
@ 2019-12-16 15:32 Hans Jerry Illikainen
  2019-12-16 15:32 ` [PATCH 1/1] " Hans Jerry Illikainen
                   ` (2 more replies)
  0 siblings, 3 replies; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-16 15:32 UTC (permalink / raw)
  To: git; +Cc: Hans Jerry Illikainen

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.

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.

Unfortunately, it breaks backward-compatibility in two ways:

1. The default trust level is TRUST_UNDEFINED.  This is compatible with
   the old behavior of every code path *except* for
   verify_merge_signature() (since, again, it used to die()s on trust
   levels below TRUST_MARGINAL).

2. The %G? format specifier no longer includes 'U' for signatures made
   with a key that is either TRUST_UNDEFINED or TRUST_NEVER.  Instead, a
   new %GT format specifier is introduced that outputs the trust level
   (as a complete string to avoid ambiguity with TRUST_UNDEFINED and
   TRUST_ULTIMATE).

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 (and it would also have broken backward-compatibility #1).
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

Hans Jerry Illikainen (1):
  gpg-interface: add minTrustLevel as a configuration option

 Documentation/config/gpg.txt       | 11 ++++
 Documentation/pretty-formats.txt   |  2 +-
 commit.c                           |  9 ++--
 gpg-interface.c                    | 85 +++++++++++++++++++++++++-----
 gpg-interface.h                    | 10 +++-
 pretty.c                           | 20 ++++++-
 t/t5573-pull-verify-signatures.sh  |  7 +++
 t/t7510-signed-commit.sh           | 19 ++++++-
 t/t7612-merge-verify-signatures.sh | 15 ++++++
 9 files changed, 157 insertions(+), 21 deletions(-)

-- 
2.24.1.485.gad05a3d8e5.dirty


^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-16 15:32 [PATCH 0/1] gpg-interface: add minTrustLevel as a configuration option Hans Jerry Illikainen
@ 2019-12-16 15:32 ` Hans Jerry Illikainen
  2019-12-20 22:57   ` SZEDER Gábor
  2019-12-16 20:58 ` [PATCH 0/1] " Junio C Hamano
  2019-12-19  0:01 ` [PATCH v1 " Hans Jerry Illikainen
  2 siblings, 1 reply; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-16 15:32 UTC (permalink / raw)
  To: git; +Cc: Hans Jerry Illikainen

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.

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.

Unfortunately, it breaks backward-compatibility in two ways:

1. The default trust level is TRUST_UNDEFINED.  This is compatible with
   the old behavior of every code path *except* for
   verify_merge_signature() (since, again, it used to die()s on trust
   levels below TRUST_MARGINAL).

2. The %G? format specifier no longer includes 'U' for signatures made
   with a key that is either TRUST_UNDEFINED or TRUST_NEVER.  Instead, a
   new %GT format specifier is introduced that outputs the trust level
   (as a complete string to avoid ambiguity with TRUST_UNDEFINED and
   TRUST_ULTIMATE).

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 (and it would also have broken backward-compatibility #1).
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>
---
 Documentation/config/gpg.txt       | 11 ++++
 Documentation/pretty-formats.txt   |  2 +-
 commit.c                           |  9 ++--
 gpg-interface.c                    | 85 +++++++++++++++++++++++++-----
 gpg-interface.h                    | 10 +++-
 pretty.c                           | 20 ++++++-
 t/t5573-pull-verify-signatures.sh  |  7 +++
 t/t7510-signed-commit.sh           | 19 ++++++-
 t/t7612-merge-verify-signatures.sh | 15 ++++++
 9 files changed, 157 insertions(+), 21 deletions(-)

diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index cce2c89245..030311fce3 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -18,3 +18,14 @@ 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.  The
+	default value is "undefined".  Supported values (in increasing
+	order of significance):
++
+* `undefined`
+* `never`
+* `marginal`
+* `fully`
+* `ultimate`
diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt
index 1a7212ce5a..f2e74241fe 100644
--- a/Documentation/pretty-formats.txt
+++ b/Documentation/pretty-formats.txt
@@ -215,7 +215,6 @@ endif::git-rev-list[]
 '%GG':: raw verification message from GPG for a signed commit
 '%G?':: show "G" for a good (valid) signature,
 	"B" for a bad signature,
-	"U" for a good signature with unknown validity,
 	"X" for a good signature that has expired,
 	"Y" for a good signature made by an expired key,
 	"R" for a good signature made by a revoked key,
@@ -226,6 +225,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
diff --git a/commit.c b/commit.c
index 434ec030d6..f6d3ce4a6e 100644
--- a/commit.c
+++ b/commit.c
@@ -1140,17 +1140,18 @@ void verify_merge_signature(struct commit *commit, int verbosity)
 {
 	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)
+			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);
diff --git a/gpg-interface.c b/gpg-interface.c
index 5134ce2780..f7b11480fb 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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;
@@ -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)
@@ -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)
@@ -115,10 +129,25 @@ 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;
 	const char *line, *next;
+	char *trust;
 	int i, j;
 	int seen_exclusive_status = 0;
 
@@ -136,9 +165,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)
@@ -154,6 +192,18 @@ 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) {
+					next = strchrnul(line, ' ');
+					trust = xmemdupz(line, next - line);
+					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;
@@ -191,14 +241,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);
@@ -264,6 +307,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);
@@ -273,7 +317,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);
@@ -320,6 +365,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)
@@ -339,6 +386,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";
 
diff --git a/gpg-interface.h b/gpg-interface.h
index 93cc3aff5c..f4e9b4f371 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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;
@@ -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)
 	 */
@@ -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);
diff --git a/pretty.c b/pretty.c
index 305e903192..0afe05714d 100644
--- a/pretty.c
+++ b/pretty.c
@@ -1313,7 +1313,6 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 			case 'G':
 			case 'B':
 			case 'E':
-			case 'U':
 			case 'N':
 			case 'X':
 			case 'Y':
@@ -1337,6 +1336,25 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 			if (c->signature_check.primary_key_fingerprint)
 				strbuf_addstr(sb, c->signature_check.primary_key_fingerprint);
 			break;
+		case 'T':
+			switch (c->signature_check.trust_level) {
+			case TRUST_UNDEFINED:
+				strbuf_addstr(sb, "undefined");
+				break;
+			case TRUST_NEVER:
+				strbuf_addstr(sb, "never");
+				break;
+			case TRUST_MARGINAL:
+				strbuf_addstr(sb, "marginal");
+				break;
+			case TRUST_FULLY:
+				strbuf_addstr(sb, "fully");
+				break;
+			case TRUST_ULTIMATE:
+				strbuf_addstr(sb, "ultimate");
+				break;
+			}
+			break;
 		default:
 			return 0;
 		}
diff --git a/t/t5573-pull-verify-signatures.sh b/t/t5573-pull-verify-signatures.sh
index 3e9876e197..d7d46d9382 100755
--- a/t/t5573-pull-verify-signatures.sh
+++ b/t/t5573-pull-verify-signatures.sh
@@ -56,6 +56,13 @@ test_expect_success GPG 'pull commit with bad signature with --verify-signatures
 
 test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures' '
 	test_when_finished "git reset --hard && git checkout initial" &&
+	git pull --ff-only --verify-signatures untrusted >pulloutput &&
+	test_i18ngrep "has a good GPG signature" pulloutput
+'
+
+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel marginal &&
 	test_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&
 	test_i18ngrep "has an untrusted GPG signature" pullerror
 '
diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh
index 682b23a068..8ab29e80ce 100755
--- a/t/t7510-signed-commit.sh
+++ b/t/t7510-signed-commit.sh
@@ -109,6 +109,21 @@ test_expect_success GPG 'verify-commit exits success on untrusted signature' '
 	grep "not certified" actual
 '
 
+test_expect_success GPG 'verify-commit exits success with matching minTrustLevel' '
+	test_config gpg.minTrustLevel ultimate &&
+	git verify-commit sixth-signed
+'
+
+test_expect_success GPG 'verify-commit exits success with low minTrustLevel' '
+	test_config gpg.minTrustLevel fully &&
+	git verify-commit sixth-signed
+'
+
+test_expect_success GPG 'verify-commit exits failure with high minTrustLevel' '
+	test_config gpg.minTrustLevel ultimate &&
+	test_must_fail git verify-commit eighth-signed-alt
+'
+
 test_expect_success GPG 'verify signatures with --raw' '
 	(
 		for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed
@@ -209,13 +224,13 @@ test_expect_success GPG 'show bad signature with custom format' '
 
 test_expect_success GPG 'show untrusted signature with custom format' '
 	cat >expect <<-\EOF &&
-	U
+	undefined
 	65A0EEA02E30CAD7
 	Eris Discordia <discord@example.net>
 	F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7
 	D4BE22311AD3131E5EDA29A461092E85B7227189
 	EOF
-	git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" eighth-signed-alt >actual &&
+	git log -1 --format="%GT%n%GK%n%GS%n%GF%n%GP" eighth-signed-alt >actual &&
 	test_cmp expect actual
 '
 
diff --git a/t/t7612-merge-verify-signatures.sh b/t/t7612-merge-verify-signatures.sh
index d99218a725..5a8e9afd8e 100755
--- a/t/t7612-merge-verify-signatures.sh
+++ b/t/t7612-merge-verify-signatures.sh
@@ -62,6 +62,13 @@ test_expect_success GPG 'merge commit with bad signature with merge.verifySignat
 
 test_expect_success GPG 'merge commit with untrusted signature with verification' '
 	test_when_finished "git reset --hard && git checkout initial" &&
+	git merge --ff-only --verify-signatures side-untrusted >mergeoutput &&
+	test_i18ngrep "has a good GPG signature" mergeoutput
+'
+
+test_expect_success GPG 'merge commit with untrusted signature with verification and minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel marginal &&
 	test_must_fail git merge --ff-only --verify-signatures side-untrusted 2>mergeerror &&
 	test_i18ngrep "has an untrusted GPG signature" mergeerror
 '
@@ -69,6 +76,14 @@ test_expect_success GPG 'merge commit with untrusted signature with verification
 test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true' '
 	test_when_finished "git reset --hard && git checkout initial" &&
 	test_config merge.verifySignatures true &&
+	git merge --ff-only side-untrusted >mergeoutput &&
+	test_i18ngrep "has a good GPG signature" mergeoutput
+'
+
+test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true and minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config merge.verifySignatures true &&
+	test_config gpg.minTrustLevel marginal &&
 	test_must_fail git merge --ff-only side-untrusted 2>mergeerror &&
 	test_i18ngrep "has an untrusted GPG signature" mergeerror
 '
-- 
2.24.1.485.gad05a3d8e5.dirty


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* Re: [PATCH 0/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-16 15:32 [PATCH 0/1] gpg-interface: add minTrustLevel as a configuration option Hans Jerry Illikainen
  2019-12-16 15:32 ` [PATCH 1/1] " Hans Jerry Illikainen
@ 2019-12-16 20:58 ` Junio C Hamano
  2019-12-18 23:59   ` Hans Jerry Illikainen
  2019-12-19  0:01 ` [PATCH v1 " Hans Jerry Illikainen
  2 siblings, 1 reply; 19+ messages in thread
From: Junio C Hamano @ 2019-12-16 20:58 UTC (permalink / raw)
  To: Hans Jerry Illikainen; +Cc: git

Hans Jerry Illikainen <hji@dyntopia.com> writes:

> 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.

OK.

> 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.

I agree that this is a good move---ever since gpg-interface.c was
written, I have found it disturbing that the order of the lines in
the output can affect the result we store and return to our callers

> 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.

Nice.

> Unfortunately, it breaks backward-compatibility in two ways:
>
> 1. The default trust level is TRUST_UNDEFINED.  This is compatible with
>    the old behavior of every code path *except* for
>    verify_merge_signature() (since, again, it used to die()s on trust
>    levels below TRUST_MARGINAL).

This might be a bit problematic.  If we can keep the default
behaviour identical to the code before this patch, while allowing
the configuration to tweak the behaviour, that would have been
more easily acceptable.

That is, can we rearrange this patch so that each user of the
verification code define its default minimum trust level (and
verify-merge-signature would have a bit higher standard than
everybody else), so that the uneven trust requirement is kept by
default?  And when the user explicitly sets the gpg.minTrustLevel
configuration, these uneven default would all set to the given
value.  Later when the code gets a bit more mature, we would declare
that we'd break the backward compatibility and set the default trust
level for all codepaths even (either by raising everybody to
marginal-or-better, or lowering everybody to undefined).

> 2. The %G? format specifier no longer includes 'U' for signatures made
>    with a key that is either TRUST_UNDEFINED or TRUST_NEVER.

Hmm, I can sort-of-see why you want to introduce a new placeholder
"%GT" to disambiguate two sources of 'U', but why would this change
to "%G?" necessary?

>    Instead, a
>    new %GT format specifier is introduced that outputs the trust level
>    (as a complete string to avoid ambiguity with TRUST_UNDEFINED and
>    TRUST_ULTIMATE).


^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH 0/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-16 20:58 ` [PATCH 0/1] " Junio C Hamano
@ 2019-12-18 23:59   ` Hans Jerry Illikainen
  0 siblings, 0 replies; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-18 23:59 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git

On Mon, Dec 16 2019, Junio C Hamano wrote:
>> Unfortunately, it breaks backward-compatibility in two ways:
>>
>> 1. The default trust level is TRUST_UNDEFINED.  This is compatible with
>>    the old behavior of every code path *except* for
>>    verify_merge_signature() (since, again, it used to die()s on trust
>>    levels below TRUST_MARGINAL).
>
> This might be a bit problematic.  If we can keep the default
> behaviour identical to the code before this patch, while allowing
> the configuration to tweak the behaviour, that would have been
> more easily acceptable.

Done in v1.

>> 2. The %G? format specifier no longer includes 'U' for signatures made
>>    with a key that is either TRUST_UNDEFINED or TRUST_NEVER.
>
> Hmm, I can sort-of-see why you want to introduce a new placeholder
> "%GT" to disambiguate two sources of 'U', but why would this change
> to "%G?" necessary?

U is re-introduced in v1.  %GT is still there (since %G? doesn't print
all trust levels) but I don't mind removing it (I added it for
completeness sake when breaking backward-compatibility in v0).

-- 
hji

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v1 0/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-16 15:32 [PATCH 0/1] gpg-interface: add minTrustLevel as a configuration option Hans Jerry Illikainen
  2019-12-16 15:32 ` [PATCH 1/1] " Hans Jerry Illikainen
  2019-12-16 20:58 ` [PATCH 0/1] " Junio C Hamano
@ 2019-12-19  0:01 ` Hans Jerry Illikainen
  2019-12-19  0:01   ` [PATCH v1 1/1] " Hans Jerry Illikainen
  2019-12-22  0:31   ` [PATCH v2 0/1] " Hans Jerry Illikainen
  2 siblings, 2 replies; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-19  0:01 UTC (permalink / raw)
  To: git; +Cc: Hans Jerry Illikainen

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].


Changes since v0:
* Added backward-compatibility with the old behavior of
  verify_merge_signature().  The old behavior is overridden if a user
  has configured gpg.minTrustLevel.  My approach is kind of ugly because
  now all users of verify_merge_signature() has to be aware of
  gpg.minTrustLevel to know if it should override the default behavior.
  An alternative might be to have a configurable per-operation trust
  level (e.g. merge.minTrustLevel), but I'm not sure how sensible that
  is either.
* Added backward-compatiblity with the old behavior of %G?.

[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

Hans Jerry Illikainen (1):
  gpg-interface: add minTrustLevel as a configuration option

 Documentation/config/gpg.txt       | 15 ++++++
 Documentation/pretty-formats.txt   |  1 +
 builtin/merge.c                    |  9 +++-
 builtin/pull.c                     | 13 ++++-
 commit.c                           | 12 +++--
 commit.h                           | 12 ++++-
 gpg-interface.c                    | 85 +++++++++++++++++++++++++-----
 gpg-interface.h                    | 10 +++-
 pretty.c                           | 30 ++++++++++-
 t/t5573-pull-verify-signatures.sh  | 64 ++++++++++++++++++++++
 t/t7030-verify-tag.sh              | 24 +++++++++
 t/t7510-signed-commit.sh           | 39 ++++++++++++++
 t/t7612-merge-verify-signatures.sh | 22 ++++++++
 13 files changed, 313 insertions(+), 23 deletions(-)

--
2.20.1

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v1 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-19  0:01 ` [PATCH v1 " Hans Jerry Illikainen
@ 2019-12-19  0:01   ` Hans Jerry Illikainen
  2019-12-22  0:31   ` [PATCH v2 0/1] " Hans Jerry Illikainen
  1 sibling, 0 replies; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-19  0:01 UTC (permalink / raw)
  To: git; +Cc: Hans Jerry Illikainen

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>
---
 Documentation/config/gpg.txt       | 15 ++++++
 Documentation/pretty-formats.txt   |  1 +
 builtin/merge.c                    |  9 +++-
 builtin/pull.c                     | 13 ++++-
 commit.c                           | 12 +++--
 commit.h                           | 12 ++++-
 gpg-interface.c                    | 85 +++++++++++++++++++++++++-----
 gpg-interface.h                    | 10 +++-
 pretty.c                           | 30 ++++++++++-
 t/t5573-pull-verify-signatures.sh  | 64 ++++++++++++++++++++++
 t/t7030-verify-tag.sh              | 24 +++++++++
 t/t7510-signed-commit.sh           | 39 ++++++++++++++
 t/t7612-merge-verify-signatures.sh | 22 ++++++++
 13 files changed, 313 insertions(+), 23 deletions(-)

diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index cce2c89245..d94025cb36 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -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`
diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt
index 1a7212ce5a..a4b6f49186 100644
--- a/Documentation/pretty-formats.txt
+++ b/Documentation/pretty-formats.txt
@@ -226,6 +226,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
diff --git a/builtin/merge.c b/builtin/merge.c
index 062e911441..d127d2225f 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -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;
@@ -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);
@@ -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);
@@ -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);
 		}
 	}
 
diff --git a/builtin/pull.c b/builtin/pull.c
index d25ff13a60..d4e3e77c8e 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -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;
@@ -355,6 +356,8 @@ 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;
@@ -362,7 +365,14 @@ static int git_pull_config(const char *var, const char *value, void *cb)
 		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);
 }
 
@@ -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);
 	}
 
 	/*
diff --git a/commit.c b/commit.c
index 434ec030d6..3f91d3efc5 100644
--- a/commit.c
+++ b/commit.c
@@ -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);
diff --git a/commit.h b/commit.h
index 221cdaa34b..008a0fa4a0 100644
--- a/commit.h
+++ b/commit.h
@@ -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);
diff --git a/gpg-interface.c b/gpg-interface.c
index 5134ce2780..f7b11480fb 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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;
@@ -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)
@@ -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)
@@ -115,10 +129,25 @@ 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;
 	const char *line, *next;
+	char *trust;
 	int i, j;
 	int seen_exclusive_status = 0;
 
@@ -136,9 +165,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)
@@ -154,6 +192,18 @@ 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) {
+					next = strchrnul(line, ' ');
+					trust = xmemdupz(line, next - line);
+					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;
@@ -191,14 +241,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);
@@ -264,6 +307,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);
@@ -273,7 +317,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);
@@ -320,6 +365,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)
@@ -339,6 +386,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";
 
diff --git a/gpg-interface.h b/gpg-interface.h
index 93cc3aff5c..f4e9b4f371 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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;
@@ -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)
 	 */
@@ -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);
diff --git a/pretty.c b/pretty.c
index 305e903192..f5fbbc5ffb 100644
--- a/pretty.c
+++ b/pretty.c
@@ -1311,9 +1311,18 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 		case '?':
 			switch (c->signature_check.result) {
 			case 'G':
+				switch (c->signature_check.trust_level) {
+				case TRUST_UNDEFINED:
+				case TRUST_NEVER:
+					strbuf_addch(sb, 'U');
+					break;
+				default:
+					strbuf_addch(sb, 'G');
+					break;
+				}
+				break;
 			case 'B':
 			case 'E':
-			case 'U':
 			case 'N':
 			case 'X':
 			case 'Y':
@@ -1337,6 +1346,25 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 			if (c->signature_check.primary_key_fingerprint)
 				strbuf_addstr(sb, c->signature_check.primary_key_fingerprint);
 			break;
+		case 'T':
+			switch (c->signature_check.trust_level) {
+			case TRUST_UNDEFINED:
+				strbuf_addstr(sb, "undefined");
+				break;
+			case TRUST_NEVER:
+				strbuf_addstr(sb, "never");
+				break;
+			case TRUST_MARGINAL:
+				strbuf_addstr(sb, "marginal");
+				break;
+			case TRUST_FULLY:
+				strbuf_addstr(sb, "fully");
+				break;
+			case TRUST_ULTIMATE:
+				strbuf_addstr(sb, "ultimate");
+				break;
+			}
+			break;
 		default:
 			return 0;
 		}
diff --git a/t/t5573-pull-verify-signatures.sh b/t/t5573-pull-verify-signatures.sh
index 3e9876e197..a53dd8550d 100755
--- a/t/t5573-pull-verify-signatures.sh
+++ b/t/t5573-pull-verify-signatures.sh
@@ -60,6 +60,27 @@ test_expect_success GPG 'pull commit with untrusted signature with --verify-sign
 	test_i18ngrep "has an untrusted GPG signature" pullerror
 '
 
+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=ultimate' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel ultimate &&
+	test_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=marginal' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel marginal &&
+	test_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=undefined' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel undefined &&
+	git pull --ff-only --verify-signatures untrusted >pulloutput &&
+	test_i18ngrep "has a good GPG signature" pulloutput
+'
+
 test_expect_success GPG 'pull signed commit with --verify-signatures' '
 	test_when_finished "git reset --hard && git checkout initial" &&
 	git pull --verify-signatures signed >pulloutput &&
@@ -79,10 +100,53 @@ test_expect_success GPG 'pull commit with bad signature with --no-verify-signatu
 '
 
 test_expect_success GPG 'pull unsigned commit into unborn branch' '
+	test_when_finished "rm -rf empty-repo" &&
 	git init empty-repo &&
 	test_must_fail \
 		git -C empty-repo pull --verify-signatures ..  2>pullerror &&
 	test_i18ngrep "does not have a GPG signature" pullerror
 '
 
+test_expect_success GPG 'pull commit into unborn branch with bad signature and --verify-signatures' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../bad 2>pullerror &&
+	test_i18ngrep "has a bad GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=ultimate' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_config_global gpg.minTrustLevel ultimate &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=marginal' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_config_global gpg.minTrustLevel marginal &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=undefined' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_config_global gpg.minTrustLevel undefined &&
+	git -C empty-repo pull --ff-only --verify-signatures ../untrusted >pulloutput &&
+	test_i18ngrep "has a good GPG signature" pulloutput
+'
+
 test_done
diff --git a/t/t7030-verify-tag.sh b/t/t7030-verify-tag.sh
index 8f077bea60..5c5bc32ccb 100755
--- a/t/t7030-verify-tag.sh
+++ b/t/t7030-verify-tag.sh
@@ -86,6 +86,30 @@ test_expect_success GPGSM 'verify and show signatures x509' '
 	echo ninth-signed-x509 OK
 '
 
+test_expect_success GPGSM 'verify and show signatures x509 with low minTrustLevel' '
+	test_config gpg.minTrustLevel undefined &&
+	git verify-tag ninth-signed-x509 2>actual &&
+	grep "Good signature from" actual &&
+	! grep "BAD signature from" actual &&
+	echo ninth-signed-x509 OK
+'
+
+test_expect_success GPGSM 'verify and show signatures x509 with matching minTrustLevel' '
+	test_config gpg.minTrustLevel fully &&
+	git verify-tag ninth-signed-x509 2>actual &&
+	grep "Good signature from" actual &&
+	! grep "BAD signature from" actual &&
+	echo ninth-signed-x509 OK
+'
+
+test_expect_success GPGSM 'verify and show signatures x509 with high minTrustLevel' '
+	test_config gpg.minTrustLevel ultimate &&
+	test_must_fail git verify-tag ninth-signed-x509 2>actual &&
+	grep "Good signature from" actual &&
+	! grep "BAD signature from" actual &&
+	echo ninth-signed-x509 OK
+'
+
 test_expect_success GPG 'detect fudged signature' '
 	git cat-file tag seventh-signed >raw &&
 	sed -e "/^tag / s/seventh/7th forged/" raw >forged1 &&
diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh
index 682b23a068..0c06d22a00 100755
--- a/t/t7510-signed-commit.sh
+++ b/t/t7510-signed-commit.sh
@@ -109,6 +109,21 @@ test_expect_success GPG 'verify-commit exits success on untrusted signature' '
 	grep "not certified" actual
 '
 
+test_expect_success GPG 'verify-commit exits success with matching minTrustLevel' '
+	test_config gpg.minTrustLevel ultimate &&
+	git verify-commit sixth-signed
+'
+
+test_expect_success GPG 'verify-commit exits success with low minTrustLevel' '
+	test_config gpg.minTrustLevel fully &&
+	git verify-commit sixth-signed
+'
+
+test_expect_success GPG 'verify-commit exits failure with high minTrustLevel' '
+	test_config gpg.minTrustLevel ultimate &&
+	test_must_fail git verify-commit eighth-signed-alt
+'
+
 test_expect_success GPG 'verify signatures with --raw' '
 	(
 		for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed
@@ -219,6 +234,30 @@ test_expect_success GPG 'show untrusted signature with custom format' '
 	test_cmp expect actual
 '
 
+test_expect_success GPG 'show untrusted signature with undefined trust level' '
+	cat >expect <<-\EOF &&
+	undefined
+	65A0EEA02E30CAD7
+	Eris Discordia <discord@example.net>
+	F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7
+	D4BE22311AD3131E5EDA29A461092E85B7227189
+	EOF
+	git log -1 --format="%GT%n%GK%n%GS%n%GF%n%GP" eighth-signed-alt >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success GPG 'show untrusted signature with ultimate trust level' '
+	cat >expect <<-\EOF &&
+	ultimate
+	13B6F51ECDDE430D
+	C O Mitter <committer@example.com>
+	73D758744BE721698EC54E8713B6F51ECDDE430D
+	73D758744BE721698EC54E8713B6F51ECDDE430D
+	EOF
+	git log -1 --format="%GT%n%GK%n%GS%n%GF%n%GP" sixth-signed >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success GPG 'show unknown signature with custom format' '
 	cat >expect <<-\EOF &&
 	E
diff --git a/t/t7612-merge-verify-signatures.sh b/t/t7612-merge-verify-signatures.sh
index d99218a725..a426f3a89a 100755
--- a/t/t7612-merge-verify-signatures.sh
+++ b/t/t7612-merge-verify-signatures.sh
@@ -66,6 +66,20 @@ test_expect_success GPG 'merge commit with untrusted signature with verification
 	test_i18ngrep "has an untrusted GPG signature" mergeerror
 '
 
+test_expect_success GPG 'merge commit with untrusted signature with verification and high minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel marginal &&
+	test_must_fail git merge --ff-only --verify-signatures side-untrusted 2>mergeerror &&
+	test_i18ngrep "has an untrusted GPG signature" mergeerror
+'
+
+test_expect_success GPG 'merge commit with untrusted signature with verification and low minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel undefined &&
+	git merge --ff-only --verify-signatures side-untrusted >mergeoutput &&
+	test_i18ngrep "has a good GPG signature" mergeoutput
+'
+
 test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true' '
 	test_when_finished "git reset --hard && git checkout initial" &&
 	test_config merge.verifySignatures true &&
@@ -73,6 +87,14 @@ test_expect_success GPG 'merge commit with untrusted signature with merge.verify
 	test_i18ngrep "has an untrusted GPG signature" mergeerror
 '
 
+test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true and minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config merge.verifySignatures true &&
+	test_config gpg.minTrustLevel marginal &&
+	test_must_fail git merge --ff-only side-untrusted 2>mergeerror &&
+	test_i18ngrep "has an untrusted GPG signature" mergeerror
+'
+
 test_expect_success GPG 'merge signed commit with verification' '
 	test_when_finished "git reset --hard && git checkout initial" &&
 	git merge --verbose --ff-only --verify-signatures side-signed >mergeoutput &&
-- 
2.20.1


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* Re: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-16 15:32 ` [PATCH 1/1] " Hans Jerry Illikainen
@ 2019-12-20 22:57   ` SZEDER Gábor
  2019-12-21 18:59     ` Hans Jerry Illikainen
  0 siblings, 1 reply; 19+ messages in thread
From: SZEDER Gábor @ 2019-12-20 22:57 UTC (permalink / raw)
  To: Hans Jerry Illikainen; +Cc: git

On Mon, Dec 16, 2019 at 03:32:04PM +0000, Hans Jerry Illikainen wrote:
> 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.
> 
> 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.
> 
> Unfortunately, it breaks backward-compatibility in two ways:
> 
> 1. The default trust level is TRUST_UNDEFINED.  This is compatible with
>    the old behavior of every code path *except* for
>    verify_merge_signature() (since, again, it used to die()s on trust
>    levels below TRUST_MARGINAL).
> 
> 2. The %G? format specifier no longer includes 'U' for signatures made
>    with a key that is either TRUST_UNDEFINED or TRUST_NEVER.  Instead, a
>    new %GT format specifier is introduced that outputs the trust level
>    (as a complete string to avoid ambiguity with TRUST_UNDEFINED and
>    TRUST_ULTIMATE).
> 
> 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 (and it would also have broken backward-compatibility #1).
> 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

This patch causes several test failures:

  https://travis-ci.org/git/git/jobs/627909430#L2259

I see you've already posted an updated version, so I tried it locally,
and the same test scripts fail with the updated version as well.

I noticed that only Linux CI jobs failed, while the OSX jobs
succeeded.  Our Linux CI jobs (and my box) are based on Ubuntu 16.04,
and thus use GnuPG v1.4, while the OSX jobs use v2.something.  Not
sure that the version difference is connected to the test failures,
but I figured it's worth pointing out.

> Signed-off-by: Hans Jerry Illikainen <hji@dyntopia.com>
> ---
>  Documentation/config/gpg.txt       | 11 ++++
>  Documentation/pretty-formats.txt   |  2 +-
>  commit.c                           |  9 ++--
>  gpg-interface.c                    | 85 +++++++++++++++++++++++++-----
>  gpg-interface.h                    | 10 +++-
>  pretty.c                           | 20 ++++++-
>  t/t5573-pull-verify-signatures.sh  |  7 +++
>  t/t7510-signed-commit.sh           | 19 ++++++-
>  t/t7612-merge-verify-signatures.sh | 15 ++++++
>  9 files changed, 157 insertions(+), 21 deletions(-)
> 
> diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
> index cce2c89245..030311fce3 100644
> --- a/Documentation/config/gpg.txt
> +++ b/Documentation/config/gpg.txt
> @@ -18,3 +18,14 @@ 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.  The
> +	default value is "undefined".  Supported values (in increasing
> +	order of significance):
> ++
> +* `undefined`
> +* `never`
> +* `marginal`
> +* `fully`
> +* `ultimate`
> diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt
> index 1a7212ce5a..f2e74241fe 100644
> --- a/Documentation/pretty-formats.txt
> +++ b/Documentation/pretty-formats.txt
> @@ -215,7 +215,6 @@ endif::git-rev-list[]
>  '%GG':: raw verification message from GPG for a signed commit
>  '%G?':: show "G" for a good (valid) signature,
>  	"B" for a bad signature,
> -	"U" for a good signature with unknown validity,
>  	"X" for a good signature that has expired,
>  	"Y" for a good signature made by an expired key,
>  	"R" for a good signature made by a revoked key,
> @@ -226,6 +225,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
> diff --git a/commit.c b/commit.c
> index 434ec030d6..f6d3ce4a6e 100644
> --- a/commit.c
> +++ b/commit.c
> @@ -1140,17 +1140,18 @@ void verify_merge_signature(struct commit *commit, int verbosity)
>  {
>  	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)
> +			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);
> diff --git a/gpg-interface.c b/gpg-interface.c
> index 5134ce2780..f7b11480fb 100644
> --- a/gpg-interface.c
> +++ b/gpg-interface.c
> @@ -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;
> @@ -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)
> @@ -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)
> @@ -115,10 +129,25 @@ 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;
>  	const char *line, *next;
> +	char *trust;
>  	int i, j;
>  	int seen_exclusive_status = 0;
>  
> @@ -136,9 +165,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)
> @@ -154,6 +192,18 @@ 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) {
> +					next = strchrnul(line, ' ');
> +					trust = xmemdupz(line, next - line);
> +					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;
> @@ -191,14 +241,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);
> @@ -264,6 +307,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);
> @@ -273,7 +317,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);
> @@ -320,6 +365,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)
> @@ -339,6 +386,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";
>  
> diff --git a/gpg-interface.h b/gpg-interface.h
> index 93cc3aff5c..f4e9b4f371 100644
> --- a/gpg-interface.h
> +++ b/gpg-interface.h
> @@ -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;
> @@ -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)
>  	 */
> @@ -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);
> diff --git a/pretty.c b/pretty.c
> index 305e903192..0afe05714d 100644
> --- a/pretty.c
> +++ b/pretty.c
> @@ -1313,7 +1313,6 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
>  			case 'G':
>  			case 'B':
>  			case 'E':
> -			case 'U':
>  			case 'N':
>  			case 'X':
>  			case 'Y':
> @@ -1337,6 +1336,25 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
>  			if (c->signature_check.primary_key_fingerprint)
>  				strbuf_addstr(sb, c->signature_check.primary_key_fingerprint);
>  			break;
> +		case 'T':
> +			switch (c->signature_check.trust_level) {
> +			case TRUST_UNDEFINED:
> +				strbuf_addstr(sb, "undefined");
> +				break;
> +			case TRUST_NEVER:
> +				strbuf_addstr(sb, "never");
> +				break;
> +			case TRUST_MARGINAL:
> +				strbuf_addstr(sb, "marginal");
> +				break;
> +			case TRUST_FULLY:
> +				strbuf_addstr(sb, "fully");
> +				break;
> +			case TRUST_ULTIMATE:
> +				strbuf_addstr(sb, "ultimate");
> +				break;
> +			}
> +			break;
>  		default:
>  			return 0;
>  		}
> diff --git a/t/t5573-pull-verify-signatures.sh b/t/t5573-pull-verify-signatures.sh
> index 3e9876e197..d7d46d9382 100755
> --- a/t/t5573-pull-verify-signatures.sh
> +++ b/t/t5573-pull-verify-signatures.sh
> @@ -56,6 +56,13 @@ test_expect_success GPG 'pull commit with bad signature with --verify-signatures
>  
>  test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures' '
>  	test_when_finished "git reset --hard && git checkout initial" &&
> +	git pull --ff-only --verify-signatures untrusted >pulloutput &&
> +	test_i18ngrep "has a good GPG signature" pulloutput
> +'
> +
> +test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel' '
> +	test_when_finished "git reset --hard && git checkout initial" &&
> +	test_config gpg.minTrustLevel marginal &&
>  	test_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&
>  	test_i18ngrep "has an untrusted GPG signature" pullerror
>  '
> diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh
> index 682b23a068..8ab29e80ce 100755
> --- a/t/t7510-signed-commit.sh
> +++ b/t/t7510-signed-commit.sh
> @@ -109,6 +109,21 @@ test_expect_success GPG 'verify-commit exits success on untrusted signature' '
>  	grep "not certified" actual
>  '
>  
> +test_expect_success GPG 'verify-commit exits success with matching minTrustLevel' '
> +	test_config gpg.minTrustLevel ultimate &&
> +	git verify-commit sixth-signed
> +'
> +
> +test_expect_success GPG 'verify-commit exits success with low minTrustLevel' '
> +	test_config gpg.minTrustLevel fully &&
> +	git verify-commit sixth-signed
> +'
> +
> +test_expect_success GPG 'verify-commit exits failure with high minTrustLevel' '
> +	test_config gpg.minTrustLevel ultimate &&
> +	test_must_fail git verify-commit eighth-signed-alt
> +'
> +
>  test_expect_success GPG 'verify signatures with --raw' '
>  	(
>  		for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed
> @@ -209,13 +224,13 @@ test_expect_success GPG 'show bad signature with custom format' '
>  
>  test_expect_success GPG 'show untrusted signature with custom format' '
>  	cat >expect <<-\EOF &&
> -	U
> +	undefined
>  	65A0EEA02E30CAD7
>  	Eris Discordia <discord@example.net>
>  	F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7
>  	D4BE22311AD3131E5EDA29A461092E85B7227189
>  	EOF
> -	git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" eighth-signed-alt >actual &&
> +	git log -1 --format="%GT%n%GK%n%GS%n%GF%n%GP" eighth-signed-alt >actual &&
>  	test_cmp expect actual
>  '
>  
> diff --git a/t/t7612-merge-verify-signatures.sh b/t/t7612-merge-verify-signatures.sh
> index d99218a725..5a8e9afd8e 100755
> --- a/t/t7612-merge-verify-signatures.sh
> +++ b/t/t7612-merge-verify-signatures.sh
> @@ -62,6 +62,13 @@ test_expect_success GPG 'merge commit with bad signature with merge.verifySignat
>  
>  test_expect_success GPG 'merge commit with untrusted signature with verification' '
>  	test_when_finished "git reset --hard && git checkout initial" &&
> +	git merge --ff-only --verify-signatures side-untrusted >mergeoutput &&
> +	test_i18ngrep "has a good GPG signature" mergeoutput
> +'
> +
> +test_expect_success GPG 'merge commit with untrusted signature with verification and minTrustLevel' '
> +	test_when_finished "git reset --hard && git checkout initial" &&
> +	test_config gpg.minTrustLevel marginal &&
>  	test_must_fail git merge --ff-only --verify-signatures side-untrusted 2>mergeerror &&
>  	test_i18ngrep "has an untrusted GPG signature" mergeerror
>  '
> @@ -69,6 +76,14 @@ test_expect_success GPG 'merge commit with untrusted signature with verification
>  test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true' '
>  	test_when_finished "git reset --hard && git checkout initial" &&
>  	test_config merge.verifySignatures true &&
> +	git merge --ff-only side-untrusted >mergeoutput &&
> +	test_i18ngrep "has a good GPG signature" mergeoutput
> +'
> +
> +test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true and minTrustLevel' '
> +	test_when_finished "git reset --hard && git checkout initial" &&
> +	test_config merge.verifySignatures true &&
> +	test_config gpg.minTrustLevel marginal &&
>  	test_must_fail git merge --ff-only side-untrusted 2>mergeerror &&
>  	test_i18ngrep "has an untrusted GPG signature" mergeerror
>  '
> -- 
> 2.24.1.485.gad05a3d8e5.dirty
> 

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-20 22:57   ` SZEDER Gábor
@ 2019-12-21 18:59     ` Hans Jerry Illikainen
  2019-12-23 14:50       ` Randall S. Becker
  0 siblings, 1 reply; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-21 18:59 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git

On Fri, Dec 20 2019, SZEDER Gábor wrote:
> On Mon, Dec 16, 2019 at 03:32:04PM +0000, Hans Jerry Illikainen wrote:
>> 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.
>
> This patch causes several test failures:
>
>   https://travis-ci.org/git/git/jobs/627909430#L2259
>
> I see you've already posted an updated version, so I tried it locally,
> and the same test scripts fail with the updated version as well.

Sorry for that!  I'm preparing a v2 (tested with both gpg1 and gpg2).

> I noticed that only Linux CI jobs failed, while the OSX jobs
> succeeded.  Our Linux CI jobs (and my box) are based on Ubuntu 16.04,
> and thus use GnuPG v1.4, while the OSX jobs use v2.something.  Not
> sure that the version difference is connected to the test failures,
> but I figured it's worth pointing out.

Your observation about the different GPG versions was spot on; thanks!
That explains why all tests pass on my machine as well as on a personal
CI setup for my git contributions (both using gpg2).

The issue was that the search for the end of a trust level to parse
relied on the TRUST_ line being space-separated.  But that is not always
the case for gpg1 (only the lowest-two trust levels contain a space
followed by additional information in gpg1).

-- 
hji

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v2 0/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-19  0:01 ` [PATCH v1 " Hans Jerry Illikainen
  2019-12-19  0:01   ` [PATCH v1 1/1] " Hans Jerry Illikainen
@ 2019-12-22  0:31   ` Hans Jerry Illikainen
  2019-12-22  0:31     ` [PATCH v2 1/1] " Hans Jerry Illikainen
                       ` (2 more replies)
  1 sibling, 3 replies; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-22  0:31 UTC (permalink / raw)
  To: git; +Cc: Hans Jerry Illikainen

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].


Changes since v0:
* Added backward-compatibility with the old behavior of
  verify_merge_signature().  The old behavior is overridden if a user
  has configured gpg.minTrustLevel.  My approach is kind of ugly because
  now all users of verify_merge_signature() has to be aware of
  gpg.minTrustLevel to know if it should override the default behavior.
  An alternative might be to have a configurable per-operation trust
  level (e.g. merge.minTrustLevel), but I'm not sure how sensible that
  is either.
* Added backward-compatiblity with the old behavior of %G?.

Changes since v1:
* Fixed compatibility with gpg1 in parse_gpg_output().  One significant
  difference between gpg1 and gpg2 is that the trust levels above
  TRUST_NEVER are written without any additional space-separated
  information in gpg1 [3].  This broke the logic in the previous
  iterations, because the end of the TRUST_ string were searched for by
  looking for a space character.  Now a new-line is used as a fallback.


[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

Hans Jerry Illikainen (1):
  gpg-interface: add minTrustLevel as a configuration option

 Documentation/config/gpg.txt       | 15 +++++
 Documentation/pretty-formats.txt   |  1 +
 builtin/merge.c                    |  9 ++-
 builtin/pull.c                     | 13 ++++-
 commit.c                           | 12 ++--
 commit.h                           | 12 +++-
 gpg-interface.c                    | 93 ++++++++++++++++++++++++++----
 gpg-interface.h                    | 10 +++-
 pretty.c                           | 30 +++++++++-
 t/t5573-pull-verify-signatures.sh  | 64 ++++++++++++++++++++
 t/t7030-verify-tag.sh              | 24 ++++++++
 t/t7510-signed-commit.sh           | 39 +++++++++++++
 t/t7612-merge-verify-signatures.sh | 22 +++++++
 13 files changed, 321 insertions(+), 23 deletions(-)

--
2.15.1.8615.g16a7d4acf3

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v2 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-22  0:31   ` [PATCH v2 0/1] " Hans Jerry Illikainen
@ 2019-12-22  0:31     ` Hans Jerry Illikainen
  2019-12-24 19:02       ` Junio C Hamano
  2019-12-22  0:44     ` [PATCH v2 0/1] " Hans Jerry Illikainen
  2019-12-27 13:55     ` [PATCH v3 " Hans Jerry Illikainen
  2 siblings, 1 reply; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-22  0:31 UTC (permalink / raw)
  To: git; +Cc: Hans Jerry Illikainen

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>
---
 Documentation/config/gpg.txt       | 15 +++++
 Documentation/pretty-formats.txt   |  1 +
 builtin/merge.c                    |  9 ++-
 builtin/pull.c                     | 13 ++++-
 commit.c                           | 12 ++--
 commit.h                           | 12 +++-
 gpg-interface.c                    | 93 ++++++++++++++++++++++++++----
 gpg-interface.h                    | 10 +++-
 pretty.c                           | 30 +++++++++-
 t/t5573-pull-verify-signatures.sh  | 64 ++++++++++++++++++++
 t/t7030-verify-tag.sh              | 24 ++++++++
 t/t7510-signed-commit.sh           | 39 +++++++++++++
 t/t7612-merge-verify-signatures.sh | 22 +++++++
 13 files changed, 321 insertions(+), 23 deletions(-)

diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index cce2c89245..d94025cb36 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -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`
diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt
index 1a7212ce5a..a4b6f49186 100644
--- a/Documentation/pretty-formats.txt
+++ b/Documentation/pretty-formats.txt
@@ -226,6 +226,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
diff --git a/builtin/merge.c b/builtin/merge.c
index 062e911441..d127d2225f 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -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;
@@ -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);
@@ -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);
@@ -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);
 		}
 	}
 
diff --git a/builtin/pull.c b/builtin/pull.c
index d25ff13a60..d4e3e77c8e 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -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;
@@ -355,6 +356,8 @@ 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;
@@ -362,7 +365,14 @@ static int git_pull_config(const char *var, const char *value, void *cb)
 		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);
 }
 
@@ -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);
 	}
 
 	/*
diff --git a/commit.c b/commit.c
index 434ec030d6..3f91d3efc5 100644
--- a/commit.c
+++ b/commit.c
@@ -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);
diff --git a/commit.h b/commit.h
index 221cdaa34b..008a0fa4a0 100644
--- a/commit.h
+++ b/commit.h
@@ -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);
diff --git a/gpg-interface.c b/gpg-interface.c
index 5134ce2780..b7cd8bc073 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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;
@@ -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)
@@ -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)
@@ -115,10 +129,25 @@ 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;
 	const char *line, *next;
+	char *trust;
 	int i, j;
 	int seen_exclusive_status = 0;
 
@@ -136,9 +165,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)
@@ -154,6 +192,26 @@ 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.
+					 */
+					next = strchr(line, ' ');
+					if (!next)
+						next = strchrnul(line, '\n');
+					trust = xmemdupz(line, next - line);
+					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;
@@ -191,14 +249,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);
@@ -264,6 +315,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);
@@ -273,7 +325,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);
@@ -320,6 +373,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)
@@ -339,6 +394,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";
 
diff --git a/gpg-interface.h b/gpg-interface.h
index 93cc3aff5c..f4e9b4f371 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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;
@@ -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)
 	 */
@@ -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);
diff --git a/pretty.c b/pretty.c
index 305e903192..f5fbbc5ffb 100644
--- a/pretty.c
+++ b/pretty.c
@@ -1311,9 +1311,18 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 		case '?':
 			switch (c->signature_check.result) {
 			case 'G':
+				switch (c->signature_check.trust_level) {
+				case TRUST_UNDEFINED:
+				case TRUST_NEVER:
+					strbuf_addch(sb, 'U');
+					break;
+				default:
+					strbuf_addch(sb, 'G');
+					break;
+				}
+				break;
 			case 'B':
 			case 'E':
-			case 'U':
 			case 'N':
 			case 'X':
 			case 'Y':
@@ -1337,6 +1346,25 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 			if (c->signature_check.primary_key_fingerprint)
 				strbuf_addstr(sb, c->signature_check.primary_key_fingerprint);
 			break;
+		case 'T':
+			switch (c->signature_check.trust_level) {
+			case TRUST_UNDEFINED:
+				strbuf_addstr(sb, "undefined");
+				break;
+			case TRUST_NEVER:
+				strbuf_addstr(sb, "never");
+				break;
+			case TRUST_MARGINAL:
+				strbuf_addstr(sb, "marginal");
+				break;
+			case TRUST_FULLY:
+				strbuf_addstr(sb, "fully");
+				break;
+			case TRUST_ULTIMATE:
+				strbuf_addstr(sb, "ultimate");
+				break;
+			}
+			break;
 		default:
 			return 0;
 		}
diff --git a/t/t5573-pull-verify-signatures.sh b/t/t5573-pull-verify-signatures.sh
index 3e9876e197..a53dd8550d 100755
--- a/t/t5573-pull-verify-signatures.sh
+++ b/t/t5573-pull-verify-signatures.sh
@@ -60,6 +60,27 @@ test_expect_success GPG 'pull commit with untrusted signature with --verify-sign
 	test_i18ngrep "has an untrusted GPG signature" pullerror
 '
 
+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=ultimate' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel ultimate &&
+	test_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=marginal' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel marginal &&
+	test_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=undefined' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel undefined &&
+	git pull --ff-only --verify-signatures untrusted >pulloutput &&
+	test_i18ngrep "has a good GPG signature" pulloutput
+'
+
 test_expect_success GPG 'pull signed commit with --verify-signatures' '
 	test_when_finished "git reset --hard && git checkout initial" &&
 	git pull --verify-signatures signed >pulloutput &&
@@ -79,10 +100,53 @@ test_expect_success GPG 'pull commit with bad signature with --no-verify-signatu
 '
 
 test_expect_success GPG 'pull unsigned commit into unborn branch' '
+	test_when_finished "rm -rf empty-repo" &&
 	git init empty-repo &&
 	test_must_fail \
 		git -C empty-repo pull --verify-signatures ..  2>pullerror &&
 	test_i18ngrep "does not have a GPG signature" pullerror
 '
 
+test_expect_success GPG 'pull commit into unborn branch with bad signature and --verify-signatures' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../bad 2>pullerror &&
+	test_i18ngrep "has a bad GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=ultimate' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_config_global gpg.minTrustLevel ultimate &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=marginal' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_config_global gpg.minTrustLevel marginal &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=undefined' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_config_global gpg.minTrustLevel undefined &&
+	git -C empty-repo pull --ff-only --verify-signatures ../untrusted >pulloutput &&
+	test_i18ngrep "has a good GPG signature" pulloutput
+'
+
 test_done
diff --git a/t/t7030-verify-tag.sh b/t/t7030-verify-tag.sh
index 8f077bea60..5c5bc32ccb 100755
--- a/t/t7030-verify-tag.sh
+++ b/t/t7030-verify-tag.sh
@@ -86,6 +86,30 @@ test_expect_success GPGSM 'verify and show signatures x509' '
 	echo ninth-signed-x509 OK
 '
 
+test_expect_success GPGSM 'verify and show signatures x509 with low minTrustLevel' '
+	test_config gpg.minTrustLevel undefined &&
+	git verify-tag ninth-signed-x509 2>actual &&
+	grep "Good signature from" actual &&
+	! grep "BAD signature from" actual &&
+	echo ninth-signed-x509 OK
+'
+
+test_expect_success GPGSM 'verify and show signatures x509 with matching minTrustLevel' '
+	test_config gpg.minTrustLevel fully &&
+	git verify-tag ninth-signed-x509 2>actual &&
+	grep "Good signature from" actual &&
+	! grep "BAD signature from" actual &&
+	echo ninth-signed-x509 OK
+'
+
+test_expect_success GPGSM 'verify and show signatures x509 with high minTrustLevel' '
+	test_config gpg.minTrustLevel ultimate &&
+	test_must_fail git verify-tag ninth-signed-x509 2>actual &&
+	grep "Good signature from" actual &&
+	! grep "BAD signature from" actual &&
+	echo ninth-signed-x509 OK
+'
+
 test_expect_success GPG 'detect fudged signature' '
 	git cat-file tag seventh-signed >raw &&
 	sed -e "/^tag / s/seventh/7th forged/" raw >forged1 &&
diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh
index 682b23a068..0c06d22a00 100755
--- a/t/t7510-signed-commit.sh
+++ b/t/t7510-signed-commit.sh
@@ -109,6 +109,21 @@ test_expect_success GPG 'verify-commit exits success on untrusted signature' '
 	grep "not certified" actual
 '
 
+test_expect_success GPG 'verify-commit exits success with matching minTrustLevel' '
+	test_config gpg.minTrustLevel ultimate &&
+	git verify-commit sixth-signed
+'
+
+test_expect_success GPG 'verify-commit exits success with low minTrustLevel' '
+	test_config gpg.minTrustLevel fully &&
+	git verify-commit sixth-signed
+'
+
+test_expect_success GPG 'verify-commit exits failure with high minTrustLevel' '
+	test_config gpg.minTrustLevel ultimate &&
+	test_must_fail git verify-commit eighth-signed-alt
+'
+
 test_expect_success GPG 'verify signatures with --raw' '
 	(
 		for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed
@@ -219,6 +234,30 @@ test_expect_success GPG 'show untrusted signature with custom format' '
 	test_cmp expect actual
 '
 
+test_expect_success GPG 'show untrusted signature with undefined trust level' '
+	cat >expect <<-\EOF &&
+	undefined
+	65A0EEA02E30CAD7
+	Eris Discordia <discord@example.net>
+	F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7
+	D4BE22311AD3131E5EDA29A461092E85B7227189
+	EOF
+	git log -1 --format="%GT%n%GK%n%GS%n%GF%n%GP" eighth-signed-alt >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success GPG 'show untrusted signature with ultimate trust level' '
+	cat >expect <<-\EOF &&
+	ultimate
+	13B6F51ECDDE430D
+	C O Mitter <committer@example.com>
+	73D758744BE721698EC54E8713B6F51ECDDE430D
+	73D758744BE721698EC54E8713B6F51ECDDE430D
+	EOF
+	git log -1 --format="%GT%n%GK%n%GS%n%GF%n%GP" sixth-signed >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success GPG 'show unknown signature with custom format' '
 	cat >expect <<-\EOF &&
 	E
diff --git a/t/t7612-merge-verify-signatures.sh b/t/t7612-merge-verify-signatures.sh
index d99218a725..a426f3a89a 100755
--- a/t/t7612-merge-verify-signatures.sh
+++ b/t/t7612-merge-verify-signatures.sh
@@ -66,6 +66,20 @@ test_expect_success GPG 'merge commit with untrusted signature with verification
 	test_i18ngrep "has an untrusted GPG signature" mergeerror
 '
 
+test_expect_success GPG 'merge commit with untrusted signature with verification and high minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel marginal &&
+	test_must_fail git merge --ff-only --verify-signatures side-untrusted 2>mergeerror &&
+	test_i18ngrep "has an untrusted GPG signature" mergeerror
+'
+
+test_expect_success GPG 'merge commit with untrusted signature with verification and low minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel undefined &&
+	git merge --ff-only --verify-signatures side-untrusted >mergeoutput &&
+	test_i18ngrep "has a good GPG signature" mergeoutput
+'
+
 test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true' '
 	test_when_finished "git reset --hard && git checkout initial" &&
 	test_config merge.verifySignatures true &&
@@ -73,6 +87,14 @@ test_expect_success GPG 'merge commit with untrusted signature with merge.verify
 	test_i18ngrep "has an untrusted GPG signature" mergeerror
 '
 
+test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true and minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config merge.verifySignatures true &&
+	test_config gpg.minTrustLevel marginal &&
+	test_must_fail git merge --ff-only side-untrusted 2>mergeerror &&
+	test_i18ngrep "has an untrusted GPG signature" mergeerror
+'
+
 test_expect_success GPG 'merge signed commit with verification' '
 	test_when_finished "git reset --hard && git checkout initial" &&
 	git merge --verbose --ff-only --verify-signatures side-signed >mergeoutput &&
-- 
2.15.1.8615.g16a7d4acf3


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 0/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-22  0:31   ` [PATCH v2 0/1] " Hans Jerry Illikainen
  2019-12-22  0:31     ` [PATCH v2 1/1] " Hans Jerry Illikainen
@ 2019-12-22  0:44     ` Hans Jerry Illikainen
  2019-12-27 13:55     ` [PATCH v3 " Hans Jerry Illikainen
  2 siblings, 0 replies; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-22  0:44 UTC (permalink / raw)
  To: git

On Sun, Dec 22 2019, Hans Jerry Illikainen wrote:
> Changes since v1:
> * Fixed compatibility with gpg1 in parse_gpg_output().  One significant
>   difference between gpg1 and gpg2 is that the trust levels above
>   TRUST_NEVER are written without any additional space-separated
>   information in gpg1 [3].  This broke the logic in the previous
>   iterations, because the end of the TRUST_ string were searched for by
>   looking for a space character.  Now a new-line is used as a fallback.

[3] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;h=de0f21ccba60c3037c2a155156202df1cd098507;hb=refs/heads/STABLE-BRANCH-1-4#l286

(missed reference in the cover letter)

-- 
hji

^ permalink raw reply	[flat|nested] 19+ messages in thread

* RE: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-21 18:59     ` Hans Jerry Illikainen
@ 2019-12-23 14:50       ` Randall S. Becker
  2019-12-24 11:30         ` Hans Jerry Illikainen
  0 siblings, 1 reply; 19+ messages in thread
From: Randall S. Becker @ 2019-12-23 14:50 UTC (permalink / raw)
  To: 'Hans Jerry Illikainen', 'SZEDER Gábor'; +Cc: git

On December 21, 2019 1:59 PM, Hans Jerry Illikainen wrote:
> On Fri, Dec 20 2019, SZEDER Gábor wrote:
> > On Mon, Dec 16, 2019 at 03:32:04PM +0000, Hans Jerry Illikainen wrote:
> >> 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.
> >
> > This patch causes several test failures:
> >
> >   https://travis-ci.org/git/git/jobs/627909430#L2259
> >
> > I see you've already posted an updated version, so I tried it locally,
> > and the same test scripts fail with the updated version as well.
> 
> Sorry for that!  I'm preparing a v2 (tested with both gpg1 and gpg2).
> 
> > I noticed that only Linux CI jobs failed, while the OSX jobs
> > succeeded.  Our Linux CI jobs (and my box) are based on Ubuntu 16.04,
> > and thus use GnuPG v1.4, while the OSX jobs use v2.something.  Not
> > sure that the version difference is connected to the test failures,
> > but I figured it's worth pointing out.
> 
> Your observation about the different GPG versions was spot on; thanks!
> That explains why all tests pass on my machine as well as on a personal CI
> setup for my git contributions (both using gpg2).
> 
> The issue was that the search for the end of a trust level to parse relied on
> the TRUST_ line being space-separated.  But that is not always the case for
> gpg1 (only the lowest-two trust levels contain a space followed by additional
> information in gpg1).

Side question: are there any tests running with alternate GPG packages? I have a platform where the official GPG itself is not available, so am looking for alternatives for that community.

Cheers,
Randall



^ permalink raw reply	[flat|nested] 19+ messages in thread

* RE: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-23 14:50       ` Randall S. Becker
@ 2019-12-24 11:30         ` Hans Jerry Illikainen
  2019-12-24 14:20           ` Randall S. Becker
  0 siblings, 1 reply; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-24 11:30 UTC (permalink / raw)
  To: Randall S. Becker, 'SZEDER Gábor'; +Cc: git

On Mon, Dec 23 2019, Randall S. Becker wrote:
> Side question: are there any tests running with alternate GPG
> packages? I have a platform where the official GPG itself is not
> available, so am looking for alternatives for that community.

Do you mean non-standard builds or forks of GnuPG, or alternative
implementations of PGP?

As it stands, the test suite is hardcoded to use gpg and gpgsm (see e.g.
t/lib-gpg.sh).  For normal use, the gpg.program and gpg.<format>.program
config options can be used to override the programs to use.  However,
any alternative implementation would have to mimic the behavior of GnuPG
(see gpg-interface.c -- a number of hardcoded arguments are passed in
verify_signed_buffer() and sign_buffer(), and the output from various
operations are GnuPG-specific.)

-- 
hji

^ permalink raw reply	[flat|nested] 19+ messages in thread

* RE: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-24 11:30         ` Hans Jerry Illikainen
@ 2019-12-24 14:20           ` Randall S. Becker
  0 siblings, 0 replies; 19+ messages in thread
From: Randall S. Becker @ 2019-12-24 14:20 UTC (permalink / raw)
  To: 'Hans Jerry Illikainen', 'SZEDER Gábor'; +Cc: git

On December 24, 2019 6:31 AM, Hans Jerry Illikainen wrote:
> To: Randall S. Becker <rsbecker@nexbridge.com>; 'SZEDER Gábor'
> <szeder.dev@gmail.com>
> Cc: git@vger.kernel.org
> Subject: RE: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration
> option
> 
> On Mon, Dec 23 2019, Randall S. Becker wrote:
> > Side question: are there any tests running with alternate GPG
> > packages? I have a platform where the official GPG itself is not
> > available, so am looking for alternatives for that community.
> 
> Do you mean non-standard builds or forks of GnuPG, or alternative
> implementations of PGP?

I am specially looking for alterative implementations of PGP, not forks of GnuPG. GnuPG v2 introduced some dependencies that are not available on a few platforms that I support.

> As it stands, the test suite is hardcoded to use gpg and gpgsm (see e.g.
> t/lib-gpg.sh).  For normal use, the gpg.program and gpg.<format>.program
> config options can be used to override the programs to use.  However, any
> alternative implementation would have to mimic the behavior of GnuPG (see
> gpg-interface.c -- a number of hardcoded arguments are passed in
> verify_signed_buffer() and sign_buffer(), and the output from various
> operations are GnuPG-specific.)

Thanks,
Randall


^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-22  0:31     ` [PATCH v2 1/1] " Hans Jerry Illikainen
@ 2019-12-24 19:02       ` Junio C Hamano
  2019-12-27 13:46         ` Hans Jerry Illikainen
  0 siblings, 1 reply; 19+ messages in thread
From: Junio C Hamano @ 2019-12-24 19:02 UTC (permalink / raw)
  To: Hans Jerry Illikainen; +Cc: git

Hans Jerry Illikainen <hji@dyntopia.com> writes:

> +				/* 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.
> +					 */
> +					next = strchr(line, ' ');
> +					if (!next)
> +						next = strchrnul(line, '\n');
> +					trust = xmemdupz(line, next - line);

I wonder if telling strcspn() to stop at either SP or LF is more in
line with the existing codebase [*1*] and/or more readable.  It
would make this part to:

		size_t trust_size = strcspn(line, " \n");
		trust = xmemdupz(line, trust_size);

without the need to use or update the 'next' variable, if I am not
mistaken?

By the way, while we are looking at this patch, I notice that,
throughout the function, the use of variable 'next' feels rather
misleading, at least to me.

When I see a loop that iterates over a block of lines, and a
variable 'line' is used to point at the beginning of the current
line at the beginning of each iteration and the code in the
iteration updates a pointer 'next', I'd expect 'next' (or perhaps
'next+1') to become the new value of 'line' when the current round
of the iteration ends (i.e. the name 'next' would stand for 'here is
where we expect the next line to start').  But the code we see in
this function uses it for 'here is the end of the current _token_ on
the line', primarily so that it can do something to the byte range
(line,<end-of-token>) and it never gets used as 'now we are done
with the line, let's move on to the next line'.

This matters because it makes it unclear to decide if the above two
lines I gave as a counter-proposal is sufficient, or if it also
needs to say "next = line + trust_size" to keep 'next' up-to-date.
The name of the varirable implies it should be, but the way the
code uses 'next' says it is a throw-away variable whose value does
not matter once we have done with the end of the current token.

I wonder if the code becomes less misleading if we either (1)
renamed 'next' to a name that hints more strongly that it is not the
'next' line but the end of the current token we are interested in,
or (2) get rid of the pointer and instead counted size of the
current token we are interested in, or perhaps both?  

This is not the fault of this patch, but I just mention it before I
forget.

Thanks.

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-24 19:02       ` Junio C Hamano
@ 2019-12-27 13:46         ` Hans Jerry Illikainen
  2019-12-27 22:21           ` Junio C Hamano
  0 siblings, 1 reply; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-27 13:46 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git

On Tue, Dec 24 2019, Junio C Hamano wrote:
> Hans Jerry Illikainen <hji@dyntopia.com> writes:
>
>> +				/* 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.
>> +					 */
>> +					next = strchr(line, ' ');
>> +					if (!next)
>> +						next = strchrnul(line, '\n');
>> +					trust = xmemdupz(line, next - line);
>
> I wonder if telling strcspn() to stop at either SP or LF is more in
> line with the existing codebase [*1*] and/or more readable.  It
> would make this part to:
>
> 		size_t trust_size = strcspn(line, " \n");
> 		trust = xmemdupz(line, trust_size);
>
> without the need to use or update the 'next' variable, if I am not
> mistaken?

I agree; fixed in v3.

> By the way, while we are looking at this patch, I notice that,
> throughout the function, the use of variable 'next' feels rather
> misleading, at least to me.
>
> [...]
>
> I wonder if the code becomes less misleading if we either (1)
> renamed 'next' to a name that hints more strongly that it is not the
> 'next' line but the end of the current token we are interested in,
> or (2) get rid of the pointer and instead counted size of the
> current token we are interested in, or perhaps both?  

Yeah the name 'next' does seem a bit counter-intuitive when used in
relation to 'line'.  Looking through the function it seems that both (1)
and (2) would work.

> This is not the fault of this patch, but I just mention it before I
> forget.
>
> Thanks.

Thanks for the review!

-- 
hji

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 0/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-22  0:31   ` [PATCH v2 0/1] " Hans Jerry Illikainen
  2019-12-22  0:31     ` [PATCH v2 1/1] " Hans Jerry Illikainen
  2019-12-22  0:44     ` [PATCH v2 0/1] " Hans Jerry Illikainen
@ 2019-12-27 13:55     ` Hans Jerry Illikainen
  2019-12-27 13:55       ` [PATCH v3 1/1] " Hans Jerry Illikainen
  2 siblings, 1 reply; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-27 13:55 UTC (permalink / raw)
  To: git; +Cc: Hans Jerry Illikainen

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].


Changes since v0:
* Added backward-compatibility with the old behavior of
  verify_merge_signature().  The old behavior is overridden if a user
  has configured gpg.minTrustLevel.  My approach is kind of ugly because
  now all users of verify_merge_signature() has to be aware of
  gpg.minTrustLevel to know if it should override the default behavior.
  An alternative might be to have a configurable per-operation trust
  level (e.g. merge.minTrustLevel), but I'm not sure how sensible that
  is either.
* Added backward-compatiblity with the old behavior of %G?.

Changes since v1:
* Fixed compatibility with gpg1 in parse_gpg_output().  One significant
  difference between gpg1 and gpg2 is that the trust levels above
  TRUST_NEVER are written without any additional space-separated
  information in gpg1 [3].  This broke the logic in the previous
  iterations, because the end of the TRUST_ string were searched for by
  looking for a space character.  Now a new-line is used as a fallback.

Changes since v2:
* Replaced strstr() + strchrnul() with strcspn().


[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
[3] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;h=de0f21ccba60c3037c2a155156202df1cd098507;hb=refs/heads/STABLE-BRANCH-1-4#l286

Hans Jerry Illikainen (1):
  gpg-interface: add minTrustLevel as a configuration option

 Documentation/config/gpg.txt       | 15 +++++
 Documentation/pretty-formats.txt   |  1 +
 builtin/merge.c                    |  9 ++-
 builtin/pull.c                     | 13 ++++-
 commit.c                           | 12 ++--
 commit.h                           | 12 +++-
 gpg-interface.c                    | 91 ++++++++++++++++++++++++++----
 gpg-interface.h                    | 10 +++-
 pretty.c                           | 30 +++++++++-
 t/t5573-pull-verify-signatures.sh  | 64 +++++++++++++++++++++
 t/t7030-verify-tag.sh              | 24 ++++++++
 t/t7510-signed-commit.sh           | 39 +++++++++++++
 t/t7612-merge-verify-signatures.sh | 22 ++++++++
 13 files changed, 319 insertions(+), 23 deletions(-)

--
2.24.1.591.g64816733a6

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-27 13:55     ` [PATCH v3 " Hans Jerry Illikainen
@ 2019-12-27 13:55       ` Hans Jerry Illikainen
  0 siblings, 0 replies; 19+ messages in thread
From: Hans Jerry Illikainen @ 2019-12-27 13:55 UTC (permalink / raw)
  To: git; +Cc: Hans Jerry Illikainen

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>
---
 Documentation/config/gpg.txt       | 15 +++++
 Documentation/pretty-formats.txt   |  1 +
 builtin/merge.c                    |  9 ++-
 builtin/pull.c                     | 13 ++++-
 commit.c                           | 12 ++--
 commit.h                           | 12 +++-
 gpg-interface.c                    | 91 ++++++++++++++++++++++++++----
 gpg-interface.h                    | 10 +++-
 pretty.c                           | 30 +++++++++-
 t/t5573-pull-verify-signatures.sh  | 64 +++++++++++++++++++++
 t/t7030-verify-tag.sh              | 24 ++++++++
 t/t7510-signed-commit.sh           | 39 +++++++++++++
 t/t7612-merge-verify-signatures.sh | 22 ++++++++
 13 files changed, 319 insertions(+), 23 deletions(-)

diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index cce2c89245..d94025cb36 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -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`
diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt
index 1a7212ce5a..a4b6f49186 100644
--- a/Documentation/pretty-formats.txt
+++ b/Documentation/pretty-formats.txt
@@ -226,6 +226,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
diff --git a/builtin/merge.c b/builtin/merge.c
index 062e911441..d127d2225f 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -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;
@@ -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);
@@ -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);
@@ -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);
 		}
 	}
 
diff --git a/builtin/pull.c b/builtin/pull.c
index d25ff13a60..d4e3e77c8e 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -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;
@@ -355,6 +356,8 @@ 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;
@@ -362,7 +365,14 @@ static int git_pull_config(const char *var, const char *value, void *cb)
 		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);
 }
 
@@ -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);
 	}
 
 	/*
diff --git a/commit.c b/commit.c
index 434ec030d6..3f91d3efc5 100644
--- a/commit.c
+++ b/commit.c
@@ -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);
diff --git a/commit.h b/commit.h
index 221cdaa34b..008a0fa4a0 100644
--- a/commit.h
+++ b/commit.h
@@ -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);
diff --git a/gpg-interface.c b/gpg-interface.c
index 5134ce2780..2d538bcd6e 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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;
@@ -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)
@@ -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)
@@ -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;
@@ -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)
@@ -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;
@@ -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);
@@ -264,6 +313,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);
@@ -273,7 +323,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);
@@ -320,6 +371,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)
@@ -339,6 +392,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";
 
diff --git a/gpg-interface.h b/gpg-interface.h
index 93cc3aff5c..f4e9b4f371 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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;
@@ -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)
 	 */
@@ -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);
diff --git a/pretty.c b/pretty.c
index 305e903192..f5fbbc5ffb 100644
--- a/pretty.c
+++ b/pretty.c
@@ -1311,9 +1311,18 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 		case '?':
 			switch (c->signature_check.result) {
 			case 'G':
+				switch (c->signature_check.trust_level) {
+				case TRUST_UNDEFINED:
+				case TRUST_NEVER:
+					strbuf_addch(sb, 'U');
+					break;
+				default:
+					strbuf_addch(sb, 'G');
+					break;
+				}
+				break;
 			case 'B':
 			case 'E':
-			case 'U':
 			case 'N':
 			case 'X':
 			case 'Y':
@@ -1337,6 +1346,25 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 			if (c->signature_check.primary_key_fingerprint)
 				strbuf_addstr(sb, c->signature_check.primary_key_fingerprint);
 			break;
+		case 'T':
+			switch (c->signature_check.trust_level) {
+			case TRUST_UNDEFINED:
+				strbuf_addstr(sb, "undefined");
+				break;
+			case TRUST_NEVER:
+				strbuf_addstr(sb, "never");
+				break;
+			case TRUST_MARGINAL:
+				strbuf_addstr(sb, "marginal");
+				break;
+			case TRUST_FULLY:
+				strbuf_addstr(sb, "fully");
+				break;
+			case TRUST_ULTIMATE:
+				strbuf_addstr(sb, "ultimate");
+				break;
+			}
+			break;
 		default:
 			return 0;
 		}
diff --git a/t/t5573-pull-verify-signatures.sh b/t/t5573-pull-verify-signatures.sh
index 3e9876e197..a53dd8550d 100755
--- a/t/t5573-pull-verify-signatures.sh
+++ b/t/t5573-pull-verify-signatures.sh
@@ -60,6 +60,27 @@ test_expect_success GPG 'pull commit with untrusted signature with --verify-sign
 	test_i18ngrep "has an untrusted GPG signature" pullerror
 '
 
+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=ultimate' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel ultimate &&
+	test_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=marginal' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel marginal &&
+	test_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=undefined' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel undefined &&
+	git pull --ff-only --verify-signatures untrusted >pulloutput &&
+	test_i18ngrep "has a good GPG signature" pulloutput
+'
+
 test_expect_success GPG 'pull signed commit with --verify-signatures' '
 	test_when_finished "git reset --hard && git checkout initial" &&
 	git pull --verify-signatures signed >pulloutput &&
@@ -79,10 +100,53 @@ test_expect_success GPG 'pull commit with bad signature with --no-verify-signatu
 '
 
 test_expect_success GPG 'pull unsigned commit into unborn branch' '
+	test_when_finished "rm -rf empty-repo" &&
 	git init empty-repo &&
 	test_must_fail \
 		git -C empty-repo pull --verify-signatures ..  2>pullerror &&
 	test_i18ngrep "does not have a GPG signature" pullerror
 '
 
+test_expect_success GPG 'pull commit into unborn branch with bad signature and --verify-signatures' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../bad 2>pullerror &&
+	test_i18ngrep "has a bad GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=ultimate' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_config_global gpg.minTrustLevel ultimate &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=marginal' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_config_global gpg.minTrustLevel marginal &&
+	test_must_fail \
+		git -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&
+	test_i18ngrep "has an untrusted GPG signature" pullerror
+'
+
+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=undefined' '
+	test_when_finished "rm -rf empty-repo" &&
+	git init empty-repo &&
+	test_config_global gpg.minTrustLevel undefined &&
+	git -C empty-repo pull --ff-only --verify-signatures ../untrusted >pulloutput &&
+	test_i18ngrep "has a good GPG signature" pulloutput
+'
+
 test_done
diff --git a/t/t7030-verify-tag.sh b/t/t7030-verify-tag.sh
index 8f077bea60..5c5bc32ccb 100755
--- a/t/t7030-verify-tag.sh
+++ b/t/t7030-verify-tag.sh
@@ -86,6 +86,30 @@ test_expect_success GPGSM 'verify and show signatures x509' '
 	echo ninth-signed-x509 OK
 '
 
+test_expect_success GPGSM 'verify and show signatures x509 with low minTrustLevel' '
+	test_config gpg.minTrustLevel undefined &&
+	git verify-tag ninth-signed-x509 2>actual &&
+	grep "Good signature from" actual &&
+	! grep "BAD signature from" actual &&
+	echo ninth-signed-x509 OK
+'
+
+test_expect_success GPGSM 'verify and show signatures x509 with matching minTrustLevel' '
+	test_config gpg.minTrustLevel fully &&
+	git verify-tag ninth-signed-x509 2>actual &&
+	grep "Good signature from" actual &&
+	! grep "BAD signature from" actual &&
+	echo ninth-signed-x509 OK
+'
+
+test_expect_success GPGSM 'verify and show signatures x509 with high minTrustLevel' '
+	test_config gpg.minTrustLevel ultimate &&
+	test_must_fail git verify-tag ninth-signed-x509 2>actual &&
+	grep "Good signature from" actual &&
+	! grep "BAD signature from" actual &&
+	echo ninth-signed-x509 OK
+'
+
 test_expect_success GPG 'detect fudged signature' '
 	git cat-file tag seventh-signed >raw &&
 	sed -e "/^tag / s/seventh/7th forged/" raw >forged1 &&
diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh
index 682b23a068..0c06d22a00 100755
--- a/t/t7510-signed-commit.sh
+++ b/t/t7510-signed-commit.sh
@@ -109,6 +109,21 @@ test_expect_success GPG 'verify-commit exits success on untrusted signature' '
 	grep "not certified" actual
 '
 
+test_expect_success GPG 'verify-commit exits success with matching minTrustLevel' '
+	test_config gpg.minTrustLevel ultimate &&
+	git verify-commit sixth-signed
+'
+
+test_expect_success GPG 'verify-commit exits success with low minTrustLevel' '
+	test_config gpg.minTrustLevel fully &&
+	git verify-commit sixth-signed
+'
+
+test_expect_success GPG 'verify-commit exits failure with high minTrustLevel' '
+	test_config gpg.minTrustLevel ultimate &&
+	test_must_fail git verify-commit eighth-signed-alt
+'
+
 test_expect_success GPG 'verify signatures with --raw' '
 	(
 		for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed
@@ -219,6 +234,30 @@ test_expect_success GPG 'show untrusted signature with custom format' '
 	test_cmp expect actual
 '
 
+test_expect_success GPG 'show untrusted signature with undefined trust level' '
+	cat >expect <<-\EOF &&
+	undefined
+	65A0EEA02E30CAD7
+	Eris Discordia <discord@example.net>
+	F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7
+	D4BE22311AD3131E5EDA29A461092E85B7227189
+	EOF
+	git log -1 --format="%GT%n%GK%n%GS%n%GF%n%GP" eighth-signed-alt >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success GPG 'show untrusted signature with ultimate trust level' '
+	cat >expect <<-\EOF &&
+	ultimate
+	13B6F51ECDDE430D
+	C O Mitter <committer@example.com>
+	73D758744BE721698EC54E8713B6F51ECDDE430D
+	73D758744BE721698EC54E8713B6F51ECDDE430D
+	EOF
+	git log -1 --format="%GT%n%GK%n%GS%n%GF%n%GP" sixth-signed >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success GPG 'show unknown signature with custom format' '
 	cat >expect <<-\EOF &&
 	E
diff --git a/t/t7612-merge-verify-signatures.sh b/t/t7612-merge-verify-signatures.sh
index d99218a725..a426f3a89a 100755
--- a/t/t7612-merge-verify-signatures.sh
+++ b/t/t7612-merge-verify-signatures.sh
@@ -66,6 +66,20 @@ test_expect_success GPG 'merge commit with untrusted signature with verification
 	test_i18ngrep "has an untrusted GPG signature" mergeerror
 '
 
+test_expect_success GPG 'merge commit with untrusted signature with verification and high minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel marginal &&
+	test_must_fail git merge --ff-only --verify-signatures side-untrusted 2>mergeerror &&
+	test_i18ngrep "has an untrusted GPG signature" mergeerror
+'
+
+test_expect_success GPG 'merge commit with untrusted signature with verification and low minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config gpg.minTrustLevel undefined &&
+	git merge --ff-only --verify-signatures side-untrusted >mergeoutput &&
+	test_i18ngrep "has a good GPG signature" mergeoutput
+'
+
 test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true' '
 	test_when_finished "git reset --hard && git checkout initial" &&
 	test_config merge.verifySignatures true &&
@@ -73,6 +87,14 @@ test_expect_success GPG 'merge commit with untrusted signature with merge.verify
 	test_i18ngrep "has an untrusted GPG signature" mergeerror
 '
 
+test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true and minTrustLevel' '
+	test_when_finished "git reset --hard && git checkout initial" &&
+	test_config merge.verifySignatures true &&
+	test_config gpg.minTrustLevel marginal &&
+	test_must_fail git merge --ff-only side-untrusted 2>mergeerror &&
+	test_i18ngrep "has an untrusted GPG signature" mergeerror
+'
+
 test_expect_success GPG 'merge signed commit with verification' '
 	test_when_finished "git reset --hard && git checkout initial" &&
 	git merge --verbose --ff-only --verify-signatures side-signed >mergeoutput &&
-- 
2.24.1.591.g64816733a6


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 1/1] gpg-interface: add minTrustLevel as a configuration option
  2019-12-27 13:46         ` Hans Jerry Illikainen
@ 2019-12-27 22:21           ` Junio C Hamano
  0 siblings, 0 replies; 19+ messages in thread
From: Junio C Hamano @ 2019-12-27 22:21 UTC (permalink / raw)
  To: Hans Jerry Illikainen; +Cc: git

Hans Jerry Illikainen <hji@dyntopia.com> writes:

>> I wonder if the code becomes less misleading if we either (1)
>> renamed 'next' to a name that hints more strongly that it is not the
>> 'next' line but the end of the current token we are interested in,
>> or (2) get rid of the pointer and instead counted size of the
>> current token we are interested in, or perhaps both?  
>
> Yeah the name 'next' does seem a bit counter-intuitive when used in
> relation to 'line'.  Looking through the function it seems that both (1)
> and (2) would work.

Thanks for thinking the code a bit more than necessary for the
purpose of this topic.  Let's leave such a clean-up outside the
scope of this topic, but perhaps a #leftoverbits marker may help us
remember it as something we could do when we have nothing else
better to do ;-)


^ permalink raw reply	[flat|nested] 19+ messages in thread

end of thread, other threads:[~2019-12-27 22:21 UTC | newest]

Thread overview: 19+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2019-12-16 15:32 [PATCH 0/1] gpg-interface: add minTrustLevel as a configuration option Hans Jerry Illikainen
2019-12-16 15:32 ` [PATCH 1/1] " Hans Jerry Illikainen
2019-12-20 22:57   ` SZEDER Gábor
2019-12-21 18:59     ` Hans Jerry Illikainen
2019-12-23 14:50       ` Randall S. Becker
2019-12-24 11:30         ` Hans Jerry Illikainen
2019-12-24 14:20           ` Randall S. Becker
2019-12-16 20:58 ` [PATCH 0/1] " Junio C Hamano
2019-12-18 23:59   ` Hans Jerry Illikainen
2019-12-19  0:01 ` [PATCH v1 " Hans Jerry Illikainen
2019-12-19  0:01   ` [PATCH v1 1/1] " Hans Jerry Illikainen
2019-12-22  0:31   ` [PATCH v2 0/1] " Hans Jerry Illikainen
2019-12-22  0:31     ` [PATCH v2 1/1] " Hans Jerry Illikainen
2019-12-24 19:02       ` Junio C Hamano
2019-12-27 13:46         ` Hans Jerry Illikainen
2019-12-27 22:21           ` Junio C Hamano
2019-12-22  0:44     ` [PATCH v2 0/1] " Hans Jerry Illikainen
2019-12-27 13:55     ` [PATCH v3 " Hans Jerry Illikainen
2019-12-27 13:55       ` [PATCH v3 1/1] " Hans Jerry Illikainen

Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).