{"thread":{"id":"52467","subject":"[PATCH 0/1] gpg-interface: add minTrustLevel as a configuration option","startedAt":"2019-12-16T15:32:14Z","lastAt":"2019-12-27T22:21:24Z","messageCount":19,"participants":["Hans Jerry Illikainen","Junio C Hamano","SZEDER Gábor","Randall S. Becker"],"isPatch":true,"patchVersion":1,"patchTotal":1},"messages":[{"id":"388261","messageId":"20191216153204.8906-1-hji@dyntopia.com","threadId":"52467","inReplyTo":null,"subject":"[PATCH 0/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-16T15:32:03Z","receivedAt":"2019-12-16T15:32:14Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"Previously, signature verification for merge and pull operations checked\nif the key had a trust-level of either TRUST_NEVER or TRUST_UNDEFINED in\nverify_merge_signature().  If that was the case, the process die()d.\n\nThe other code paths that did signature verification relied entirely on\nthe return code from check_commit_signature().  And signatures made with\na good key, irregardless of its trust level, was considered valid by\ncheck_commit_signature().\n\nThis difference in behavior might induce users to erroneously assume\nthat the trust level of a key in their keyring is always considered by\nGit, even for operations where it is not (e.g. during a verify-commit or\nverify-tag).\n\nThe way it worked was by gpg-interface.c storing the result from the\nkey/signature status *and* the lowest-two trust levels in the `result`\nmember of the signature_check structure (the last of these status lines\nthat were encountered got written to `result`).  These are documented in\nGPG under the subsection `General status codes` and `Key related`,\nrespectively [1].\n\nThe GPG documentation says the following on the TRUST_ status codes [1]:\n\n    \"\"\"\n    These are several similar status codes:\n\n    - TRUST_UNDEFINED <error_token>\n    - TRUST_NEVER     <error_token>\n    - TRUST_MARGINAL  [0  [<validation_model>]]\n    - TRUST_FULLY     [0  [<validation_model>]]\n    - TRUST_ULTIMATE  [0  [<validation_model>]]\n\n    For good signatures one of these status lines are emitted to\n    indicate the validity of the key used to create the signature.\n    The error token values are currently only emitted by gpgsm.\n    \"\"\"\n\nMy interpretation is that the trust level is conceptionally different\nfrom the validity of the key and/or signature.  That seems to also have\nbeen the assumption of the old code in check_signature() where a result\nof 'G' (as in GOODSIG) and 'U' (as in TRUST_NEVER or TRUST_UNDEFINED)\nwere both considered a success.\n\nThe two cases where a result of 'U' had special meaning were in\nverify_merge_signature() (where this caused git to die()) and in\nformat_commit_one() (where it affected the output of the %G? format\nspecifier).\n\nI think it makes sense to refactor the processing of TRUST_ status lines\nsuch that users can configure a minimum trust level that is enforced\nglobally, rather than have individual parts of git (e.g. merge) do it\nthemselves.\n\nI also think it makes sense to not store the trust level in the same\nstruct member as the key/signature status.  While the presence of a\nTRUST_ status code does imply that the signature is good (see the first\nparagraph in the included snippet above), as far as I can tell, the\norder of the status lines from GPG isn't well-defined; thus it would\nseem plausible that the trust level could be overwritten with the\nkey/signature status if they were stored in the same member of the\nsignature_check structure.\n\nThis patch introduces a new configuration option: gpg.minTrustLevel.  It\nconsolidates trust-level verification to gpg-interface.c and adds a new\n`trust_level` member to the signature_check structure.\n\nUnfortunately, it breaks backward-compatibility in two ways:\n\n1. The default trust level is TRUST_UNDEFINED.  This is compatible with\n   the old behavior of every code path *except* for\n   verify_merge_signature() (since, again, it used to die()s on trust\n   levels below TRUST_MARGINAL).\n\n2. The %G? format specifier no longer includes 'U' for signatures made\n   with a key that is either TRUST_UNDEFINED or TRUST_NEVER.  Instead, a\n   new %GT format specifier is introduced that outputs the trust level\n   (as a complete string to avoid ambiguity with TRUST_UNDEFINED and\n   TRUST_ULTIMATE).\n\nAnother approach would have been to simply drop the trust-level\nrequirement in verify_merge_signature().  This would also have made the\nbehavior consistent with other parts of git that perform signature\nverification (and it would also have broken backward-compatibility #1).\nHowever, requiring a minimum trust level for signing keys does seem to\nhave a real-world use-case.  For example, the build system used by the\nQubes OS project currently parses the raw output from verify-tag in\norder to assert a minimum trust level for keys used to sign git tags\n[2].\n\n[1] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/doc/DETAILS;h=bd00006e933ac56719b1edd2478ecd79273eae72;hb=refs/heads/master\n[2] https://github.com/QubesOS/qubes-builder/blob/9674c1991deef45b1a1b1c71fddfab14ba50dccf/scripts/verify-git-tag#L43\n\nHans Jerry Illikainen (1):\n  gpg-interface: add minTrustLevel as a configuration option\n\n Documentation/config/gpg.txt       | 11 ++++\n Documentation/pretty-formats.txt   |  2 +-\n commit.c                           |  9 ++--\n gpg-interface.c                    | 85 +++++++++++++++++++++++++-----\n gpg-interface.h                    | 10 +++-\n pretty.c                           | 20 ++++++-\n t/t5573-pull-verify-signatures.sh  |  7 +++\n t/t7510-signed-commit.sh           | 19 ++++++-\n t/t7612-merge-verify-signatures.sh | 15 ++++++\n 9 files changed, 157 insertions(+), 21 deletions(-)\n\n-- \n2.24.1.485.gad05a3d8e5.dirty\n\n"},{"id":"388262","messageId":"20191216153204.8906-2-hji@dyntopia.com","threadId":"52467","inReplyTo":"20191216153204.8906-1-hji@dyntopia.com","subject":"[PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-16T15:32:04Z","receivedAt":"2019-12-16T15:32:19Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"Previously, signature verification for merge and pull operations checked\nif the key had a trust-level of either TRUST_NEVER or TRUST_UNDEFINED in\nverify_merge_signature().  If that was the case, the process die()d.\n\nThe other code paths that did signature verification relied entirely on\nthe return code from check_commit_signature().  And signatures made with\na good key, irregardless of its trust level, was considered valid by\ncheck_commit_signature().\n\nThis difference in behavior might induce users to erroneously assume\nthat the trust level of a key in their keyring is always considered by\nGit, even for operations where it is not (e.g. during a verify-commit or\nverify-tag).\n\nThe way it worked was by gpg-interface.c storing the result from the\nkey/signature status *and* the lowest-two trust levels in the `result`\nmember of the signature_check structure (the last of these status lines\nthat were encountered got written to `result`).  These are documented in\nGPG under the subsection `General status codes` and `Key related`,\nrespectively [1].\n\nThe GPG documentation says the following on the TRUST_ status codes [1]:\n\n    \"\"\"\n    These are several similar status codes:\n\n    - TRUST_UNDEFINED <error_token>\n    - TRUST_NEVER     <error_token>\n    - TRUST_MARGINAL  [0  [<validation_model>]]\n    - TRUST_FULLY     [0  [<validation_model>]]\n    - TRUST_ULTIMATE  [0  [<validation_model>]]\n\n    For good signatures one of these status lines are emitted to\n    indicate the validity of the key used to create the signature.\n    The error token values are currently only emitted by gpgsm.\n    \"\"\"\n\nMy interpretation is that the trust level is conceptionally different\nfrom the validity of the key and/or signature.  That seems to also have\nbeen the assumption of the old code in check_signature() where a result\nof 'G' (as in GOODSIG) and 'U' (as in TRUST_NEVER or TRUST_UNDEFINED)\nwere both considered a success.\n\nThe two cases where a result of 'U' had special meaning were in\nverify_merge_signature() (where this caused git to die()) and in\nformat_commit_one() (where it affected the output of the %G? format\nspecifier).\n\nI think it makes sense to refactor the processing of TRUST_ status lines\nsuch that users can configure a minimum trust level that is enforced\nglobally, rather than have individual parts of git (e.g. merge) do it\nthemselves.\n\nI also think it makes sense to not store the trust level in the same\nstruct member as the key/signature status.  While the presence of a\nTRUST_ status code does imply that the signature is good (see the first\nparagraph in the included snippet above), as far as I can tell, the\norder of the status lines from GPG isn't well-defined; thus it would\nseem plausible that the trust level could be overwritten with the\nkey/signature status if they were stored in the same member of the\nsignature_check structure.\n\nThis patch introduces a new configuration option: gpg.minTrustLevel.  It\nconsolidates trust-level verification to gpg-interface.c and adds a new\n`trust_level` member to the signature_check structure.\n\nUnfortunately, it breaks backward-compatibility in two ways:\n\n1. The default trust level is TRUST_UNDEFINED.  This is compatible with\n   the old behavior of every code path *except* for\n   verify_merge_signature() (since, again, it used to die()s on trust\n   levels below TRUST_MARGINAL).\n\n2. The %G? format specifier no longer includes 'U' for signatures made\n   with a key that is either TRUST_UNDEFINED or TRUST_NEVER.  Instead, a\n   new %GT format specifier is introduced that outputs the trust level\n   (as a complete string to avoid ambiguity with TRUST_UNDEFINED and\n   TRUST_ULTIMATE).\n\nAnother approach would have been to simply drop the trust-level\nrequirement in verify_merge_signature().  This would also have made the\nbehavior consistent with other parts of git that perform signature\nverification (and it would also have broken backward-compatibility #1).\nHowever, requiring a minimum trust level for signing keys does seem to\nhave a real-world use-case.  For example, the build system used by the\nQubes OS project currently parses the raw output from verify-tag in\norder to assert a minimum trust level for keys used to sign git tags\n[2].\n\n[1] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/doc/DETAILS;h=bd00006e933ac56719b1edd2478ecd79273eae72;hb=refs/heads/master\n[2] https://github.com/QubesOS/qubes-builder/blob/9674c1991deef45b1a1b1c71fddfab14ba50dccf/scripts/verify-git-tag#L43\n\nSigned-off-by: Hans Jerry Illikainen <hji@dyntopia.com>\n---\n Documentation/config/gpg.txt       | 11 ++++\n Documentation/pretty-formats.txt   |  2 +-\n commit.c                           |  9 ++--\n gpg-interface.c                    | 85 +++++++++++++++++++++++++-----\n gpg-interface.h                    | 10 +++-\n pretty.c                           | 20 ++++++-\n t/t5573-pull-verify-signatures.sh  |  7 +++\n t/t7510-signed-commit.sh           | 19 ++++++-\n t/t7612-merge-verify-signatures.sh | 15 ++++++\n 9 files changed, 157 insertions(+), 21 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex cce2c89245..030311fce3 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -18,3 +18,14 @@ gpg.<format>.program::\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n \tvalue for `gpg.x509.program` is \"gpgsm\".\n+\n+gpg.minTrustLevel::\n+\tSpecifies a minimum trust level for signature verification.  The\n+\tdefault value is \"undefined\".  Supported values (in increasing\n+\torder of significance):\n++\n+* `undefined`\n+* `never`\n+* `marginal`\n+* `fully`\n+* `ultimate`\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 1a7212ce5a..f2e74241fe 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -215,7 +215,6 @@ endif::git-rev-list[]\n '%GG':: raw verification message from GPG for a signed commit\n '%G?':: show \"G\" for a good (valid) signature,\n \t\"B\" for a bad signature,\n-\t\"U\" for a good signature with unknown validity,\n \t\"X\" for a good signature that has expired,\n \t\"Y\" for a good signature made by an expired key,\n \t\"R\" for a good signature made by a revoked key,\n@@ -226,6 +225,7 @@ endif::git-rev-list[]\n '%GF':: show the fingerprint of the key used to sign a signed commit\n '%GP':: show the fingerprint of the primary key whose subkey was used\n \tto sign a signed commit\n+'%GT':: show the trust level for the key used to sign a signed commit\n '%gD':: reflog selector, e.g., `refs/stash@{1}` or `refs/stash@{2\n \tminutes ago}`; the format follows the rules described for the\n \t`-g` option. The portion before the `@` is the refname as\ndiff --git a/commit.c b/commit.c\nindex 434ec030d6..f6d3ce4a6e 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1140,17 +1140,18 @@ void verify_merge_signature(struct commit *commit, int verbosity)\n {\n \tchar hex[GIT_MAX_HEXSZ + 1];\n \tstruct signature_check signature_check;\n+\tint ret;\n \tmemset(&signature_check, 0, sizeof(signature_check));\n \n-\tcheck_commit_signature(commit, &signature_check);\n+\tret = check_commit_signature(commit, &signature_check);\n \n \tfind_unique_abbrev_r(hex, &commit->object.oid, DEFAULT_ABBREV);\n \tswitch (signature_check.result) {\n \tcase 'G':\n+\t\tif (ret)\n+\t\t\tdie(_(\"Commit %s has an untrusted GPG signature, \"\n+\t\t\t      \"allegedly by %s.\"), hex, signature_check.signer);\n \t\tbreak;\n-\tcase 'U':\n-\t\tdie(_(\"Commit %s has an untrusted GPG signature, \"\n-\t\t      \"allegedly by %s.\"), hex, signature_check.signer);\n \tcase 'B':\n \t\tdie(_(\"Commit %s has a bad GPG signature \"\n \t\t      \"allegedly by %s.\"), hex, signature_check.signer);\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 5134ce2780..f7b11480fb 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -7,6 +7,8 @@\n #include \"tempfile.h\"\n \n static char *configured_signing_key;\n+static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n+\n struct gpg_format {\n \tconst char *name;\n \tconst char *program;\n@@ -85,6 +87,8 @@ void signature_check_clear(struct signature_check *sigc)\n #define GPG_STATUS_UID\t\t(1<<2)\n /* The status includes key fingerprints */\n #define GPG_STATUS_FINGERPRINT\t(1<<3)\n+/* The status includes trust level */\n+#define GPG_STATUS_TRUST_LEVEL\t(1<<4)\n \n /* Short-hand for standard exclusive *SIG status with keyid & UID */\n #define GPG_STATUS_STDSIG\t(GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID|GPG_STATUS_UID)\n@@ -96,13 +100,23 @@ static struct {\n } sigcheck_gpg_status[] = {\n \t{ 'G', \"GOODSIG \", GPG_STATUS_STDSIG },\n \t{ 'B', \"BADSIG \", GPG_STATUS_STDSIG },\n-\t{ 'U', \"TRUST_NEVER\", 0 },\n-\t{ 'U', \"TRUST_UNDEFINED\", 0 },\n \t{ 'E', \"ERRSIG \", GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID },\n \t{ 'X', \"EXPSIG \", GPG_STATUS_STDSIG },\n \t{ 'Y', \"EXPKEYSIG \", GPG_STATUS_STDSIG },\n \t{ 'R', \"REVKEYSIG \", GPG_STATUS_STDSIG },\n \t{ 0, \"VALIDSIG \", GPG_STATUS_FINGERPRINT },\n+\t{ 0, \"TRUST_\", GPG_STATUS_TRUST_LEVEL },\n+};\n+\n+static struct {\n+\tconst char *key;\n+\tenum signature_trust_level value;\n+} sigcheck_gpg_trust_level[] = {\n+\t{ \"UNDEFINED\", TRUST_UNDEFINED },\n+\t{ \"NEVER\", TRUST_NEVER },\n+\t{ \"MARGINAL\", TRUST_MARGINAL },\n+\t{ \"FULLY\", TRUST_FULLY },\n+\t{ \"ULTIMATE\", TRUST_ULTIMATE },\n };\n \n static void replace_cstring(char **field, const char *line, const char *next)\n@@ -115,10 +129,25 @@ static void replace_cstring(char **field, const char *line, const char *next)\n \t\t*field = NULL;\n }\n \n+static int parse_gpg_trust_level(const char *level,\n+\t\t\t\t enum signature_trust_level *res)\n+{\n+\tsize_t i;\n+\n+\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_trust_level); i++) {\n+\t\tif (!strcmp(sigcheck_gpg_trust_level[i].key, level)) {\n+\t\t\t*res = sigcheck_gpg_trust_level[i].value;\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\treturn 1;\n+}\n+\n static void parse_gpg_output(struct signature_check *sigc)\n {\n \tconst char *buf = sigc->gpg_status;\n \tconst char *line, *next;\n+\tchar *trust;\n \tint i, j;\n \tint seen_exclusive_status = 0;\n \n@@ -136,9 +165,18 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t\t/* Iterate over all search strings */\n \t\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {\n \t\t\tif (skip_prefix(line, sigcheck_gpg_status[i].check, &line)) {\n+\t\t\t\t/*\n+\t\t\t\t * GOODSIG, BADSIG etc. can occur only once for\n+\t\t\t\t * each signature.  Therefore, if we had more\n+\t\t\t\t * than one then we're dealing with multiple\n+\t\t\t\t * signatures.  We don't support them\n+\t\t\t\t * currently, and they're rather hard to\n+\t\t\t\t * create, so something is likely fishy and we\n+\t\t\t\t * should reject them altogether.\n+\t\t\t\t */\n \t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_EXCLUSIVE) {\n \t\t\t\t\tif (seen_exclusive_status++)\n-\t\t\t\t\t\tgoto found_duplicate_status;\n+\t\t\t\t\t\tgoto error;\n \t\t\t\t}\n \n \t\t\t\tif (sigcheck_gpg_status[i].result)\n@@ -154,6 +192,18 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t\t\t\t\t\treplace_cstring(&sigc->signer, line, next);\n \t\t\t\t\t}\n \t\t\t\t}\n+\n+\t\t\t\t/* Do we have trust level? */\n+\t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_TRUST_LEVEL) {\n+\t\t\t\t\tnext = strchrnul(line, ' ');\n+\t\t\t\t\ttrust = xmemdupz(line, next - line);\n+\t\t\t\t\tif (parse_gpg_trust_level(trust, &sigc->trust_level)) {\n+\t\t\t\t\t\tfree(trust);\n+\t\t\t\t\t\tgoto error;\n+\t\t\t\t\t}\n+\t\t\t\t\tfree(trust);\n+\t\t\t\t}\n+\n \t\t\t\t/* Do we have fingerprint? */\n \t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_FINGERPRINT) {\n \t\t\t\t\tconst char *limit;\n@@ -191,14 +241,7 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t}\n \treturn;\n \n-found_duplicate_status:\n-\t/*\n-\t * GOODSIG, BADSIG etc. can occur only once for each signature.\n-\t * Therefore, if we had more than one then we're dealing with multiple\n-\t * signatures.  We don't support them currently, and they're rather\n-\t * hard to create, so something is likely fishy and we should reject\n-\t * them altogether.\n-\t */\n+error:\n \tsigc->result = 'E';\n \t/* Clear partial data to avoid confusion */\n \tFREE_AND_NULL(sigc->primary_key_fingerprint);\n@@ -264,6 +307,7 @@ int check_signature(const char *payload, size_t plen, const char *signature,\n \tint status;\n \n \tsigc->result = 'N';\n+\tsigc->trust_level = -1;\n \n \tstatus = verify_signed_buffer(payload, plen, signature, slen,\n \t\t\t\t      &gpg_output, &gpg_status);\n@@ -273,7 +317,8 @@ int check_signature(const char *payload, size_t plen, const char *signature,\n \tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n \tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n \tparse_gpg_output(sigc);\n-\tstatus |= sigc->result != 'G' && sigc->result != 'U';\n+\tstatus |= sigc->result != 'G';\n+\tstatus |= sigc->trust_level < configured_min_trust_level;\n \n  out:\n \tstrbuf_release(&gpg_status);\n@@ -320,6 +365,8 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n {\n \tstruct gpg_format *fmt = NULL;\n \tchar *fmtname = NULL;\n+\tchar *trust;\n+\tint ret;\n \n \tif (!strcmp(var, \"user.signingkey\")) {\n \t\tif (!value)\n@@ -339,6 +386,20 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.mintrustlevel\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\n+\t\ttrust = xstrdup_toupper(value);\n+\t\tret = parse_gpg_trust_level(trust, &configured_min_trust_level);\n+\t\tfree(trust);\n+\n+\t\tif (ret)\n+\t\t\treturn error(\"unsupported value for %s: %s\", var,\n+\t\t\t\t     value);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n \t\tfmtname = \"openpgp\";\n \ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 93cc3aff5c..f4e9b4f371 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -7,6 +7,14 @@ struct strbuf;\n #define GPG_VERIFY_RAW\t\t\t2\n #define GPG_VERIFY_OMIT_STATUS\t4\n \n+enum signature_trust_level {\n+\tTRUST_UNDEFINED,\n+\tTRUST_NEVER,\n+\tTRUST_MARGINAL,\n+\tTRUST_FULLY,\n+\tTRUST_ULTIMATE,\n+};\n+\n struct signature_check {\n \tchar *payload;\n \tchar *gpg_output;\n@@ -16,7 +24,6 @@ struct signature_check {\n \t * possible \"result\":\n \t * 0 (not checked)\n \t * N (checked but no further result)\n-\t * U (untrusted good)\n \t * G (good)\n \t * B (bad)\n \t */\n@@ -25,6 +32,7 @@ struct signature_check {\n \tchar *key;\n \tchar *fingerprint;\n \tchar *primary_key_fingerprint;\n+\tenum signature_trust_level trust_level;\n };\n \n void signature_check_clear(struct signature_check *sigc);\ndiff --git a/pretty.c b/pretty.c\nindex 305e903192..0afe05714d 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1313,7 +1313,6 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tcase 'G':\n \t\t\tcase 'B':\n \t\t\tcase 'E':\n-\t\t\tcase 'U':\n \t\t\tcase 'N':\n \t\t\tcase 'X':\n \t\t\tcase 'Y':\n@@ -1337,6 +1336,25 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tif (c->signature_check.primary_key_fingerprint)\n \t\t\t\tstrbuf_addstr(sb, c->signature_check.primary_key_fingerprint);\n \t\t\tbreak;\n+\t\tcase 'T':\n+\t\t\tswitch (c->signature_check.trust_level) {\n+\t\t\tcase TRUST_UNDEFINED:\n+\t\t\t\tstrbuf_addstr(sb, \"undefined\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_NEVER:\n+\t\t\t\tstrbuf_addstr(sb, \"never\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_MARGINAL:\n+\t\t\t\tstrbuf_addstr(sb, \"marginal\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_FULLY:\n+\t\t\t\tstrbuf_addstr(sb, \"fully\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_ULTIMATE:\n+\t\t\t\tstrbuf_addstr(sb, \"ultimate\");\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tbreak;\n \t\tdefault:\n \t\t\treturn 0;\n \t\t}\ndiff --git a/t/t5573-pull-verify-signatures.sh b/t/t5573-pull-verify-signatures.sh\nindex 3e9876e197..d7d46d9382 100755\n--- a/t/t5573-pull-verify-signatures.sh\n+++ b/t/t5573-pull-verify-signatures.sh\n@@ -56,6 +56,13 @@ test_expect_success GPG 'pull commit with bad signature with --verify-signatures\n \n test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\tgit pull --ff-only --verify-signatures untrusted >pulloutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" pulloutput\n+'\n+\n+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n \ttest_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&\n \ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n '\ndiff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\nindex 682b23a068..8ab29e80ce 100755\n--- a/t/t7510-signed-commit.sh\n+++ b/t/t7510-signed-commit.sh\n@@ -109,6 +109,21 @@ test_expect_success GPG 'verify-commit exits success on untrusted signature' '\n \tgrep \"not certified\" actual\n '\n \n+test_expect_success GPG 'verify-commit exits success with matching minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPG 'verify-commit exits success with low minTrustLevel' '\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPG 'verify-commit exits failure with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-commit eighth-signed-alt\n+'\n+\n test_expect_success GPG 'verify signatures with --raw' '\n \t(\n \t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n@@ -209,13 +224,13 @@ test_expect_success GPG 'show bad signature with custom format' '\n \n test_expect_success GPG 'show untrusted signature with custom format' '\n \tcat >expect <<-\\EOF &&\n-\tU\n+\tundefined\n \t65A0EEA02E30CAD7\n \tEris Discordia <discord@example.net>\n \tF8364A59E07FFE9F4D63005A65A0EEA02E30CAD7\n \tD4BE22311AD3131E5EDA29A461092E85B7227189\n \tEOF\n-\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n \ttest_cmp expect actual\n '\n \ndiff --git a/t/t7612-merge-verify-signatures.sh b/t/t7612-merge-verify-signatures.sh\nindex d99218a725..5a8e9afd8e 100755\n--- a/t/t7612-merge-verify-signatures.sh\n+++ b/t/t7612-merge-verify-signatures.sh\n@@ -62,6 +62,13 @@ test_expect_success GPG 'merge commit with bad signature with merge.verifySignat\n \n test_expect_success GPG 'merge commit with untrusted signature with verification' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\tgit merge --ff-only --verify-signatures side-untrusted >mergeoutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" mergeoutput\n+'\n+\n+test_expect_success GPG 'merge commit with untrusted signature with verification and minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n \ttest_must_fail git merge --ff-only --verify-signatures side-untrusted 2>mergeerror &&\n \ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n '\n@@ -69,6 +76,14 @@ test_expect_success GPG 'merge commit with untrusted signature with verification\n test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n \ttest_config merge.verifySignatures true &&\n+\tgit merge --ff-only side-untrusted >mergeoutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" mergeoutput\n+'\n+\n+test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true and minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config merge.verifySignatures true &&\n+\ttest_config gpg.minTrustLevel marginal &&\n \ttest_must_fail git merge --ff-only side-untrusted 2>mergeerror &&\n \ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n '\n-- \n2.24.1.485.gad05a3d8e5.dirty\n\n"},{"id":"388297","messageId":"xmqq4ky0j8ca.fsf@gitster-ct.c.googlers.com","threadId":"52467","inReplyTo":"20191216153204.8906-1-hji@dyntopia.com","subject":"Re: [PATCH 0/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2019-12-16T20:58:45Z","receivedAt":"2019-12-16T20:58:52Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Hans Jerry Illikainen <hji@dyntopia.com> writes:\n\n> I think it makes sense to refactor the processing of TRUST_ status lines\n> such that users can configure a minimum trust level that is enforced\n> globally, rather than have individual parts of git (e.g. merge) do it\n> themselves.\n\nOK.\n\n> I also think it makes sense to not store the trust level in the same\n> struct member as the key/signature status.  While the presence of a\n> TRUST_ status code does imply that the signature is good (see the first\n> paragraph in the included snippet above), as far as I can tell, the\n> order of the status lines from GPG isn't well-defined; thus it would\n> seem plausible that the trust level could be overwritten with the\n> key/signature status if they were stored in the same member of the\n> signature_check structure.\n\nI agree that this is a good move---ever since gpg-interface.c was\nwritten, I have found it disturbing that the order of the lines in\nthe output can affect the result we store and return to our callers\n\n> This patch introduces a new configuration option: gpg.minTrustLevel.  It\n> consolidates trust-level verification to gpg-interface.c and adds a new\n> `trust_level` member to the signature_check structure.\n\nNice.\n\n> Unfortunately, it breaks backward-compatibility in two ways:\n>\n> 1. The default trust level is TRUST_UNDEFINED.  This is compatible with\n>    the old behavior of every code path *except* for\n>    verify_merge_signature() (since, again, it used to die()s on trust\n>    levels below TRUST_MARGINAL).\n\nThis might be a bit problematic.  If we can keep the default\nbehaviour identical to the code before this patch, while allowing\nthe configuration to tweak the behaviour, that would have been\nmore easily acceptable.\n\nThat is, can we rearrange this patch so that each user of the\nverification code define its default minimum trust level (and\nverify-merge-signature would have a bit higher standard than\neverybody else), so that the uneven trust requirement is kept by\ndefault?  And when the user explicitly sets the gpg.minTrustLevel\nconfiguration, these uneven default would all set to the given\nvalue.  Later when the code gets a bit more mature, we would declare\nthat we'd break the backward compatibility and set the default trust\nlevel for all codepaths even (either by raising everybody to\nmarginal-or-better, or lowering everybody to undefined).\n\n> 2. The %G? format specifier no longer includes 'U' for signatures made\n>    with a key that is either TRUST_UNDEFINED or TRUST_NEVER.\n\nHmm, I can sort-of-see why you want to introduce a new placeholder\n\"%GT\" to disambiguate two sources of 'U', but why would this change\nto \"%G?\" necessary?\n\n>    Instead, a\n>    new %GT format specifier is introduced that outputs the trust level\n>    (as a complete string to avoid ambiguity with TRUST_UNDEFINED and\n>    TRUST_ULTIMATE).\n\n"},{"id":"388486","messageId":"20191219000140.9056-1-hji@dyntopia.com","threadId":"52467","inReplyTo":"20191216153204.8906-1-hji@dyntopia.com","subject":"[PATCH v1 0/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-19T00:01:39Z","receivedAt":"2019-12-19T00:01:47Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"Previously, signature verification for merge and pull operations checked\nif the key had a trust-level of either TRUST_NEVER or TRUST_UNDEFINED in\nverify_merge_signature().  If that was the case, the process die()d.\n\nThe other code paths that did signature verification relied entirely on\nthe return code from check_commit_signature().  And signatures made with\na good key, irregardless of its trust level, was considered valid by\ncheck_commit_signature().\n\nThis difference in behavior might induce users to erroneously assume\nthat the trust level of a key in their keyring is always considered by\nGit, even for operations where it is not (e.g. during a verify-commit or\nverify-tag).\n\nThe way it worked was by gpg-interface.c storing the result from the\nkey/signature status *and* the lowest-two trust levels in the `result`\nmember of the signature_check structure (the last of these status lines\nthat were encountered got written to `result`).  These are documented in\nGPG under the subsection `General status codes` and `Key related`,\nrespectively [1].\n\nThe GPG documentation says the following on the TRUST_ status codes [1]:\n\n    \"\"\"\n    These are several similar status codes:\n\n    - TRUST_UNDEFINED <error_token>\n    - TRUST_NEVER     <error_token>\n    - TRUST_MARGINAL  [0  [<validation_model>]]\n    - TRUST_FULLY     [0  [<validation_model>]]\n    - TRUST_ULTIMATE  [0  [<validation_model>]]\n\n    For good signatures one of these status lines are emitted to\n    indicate the validity of the key used to create the signature.\n    The error token values are currently only emitted by gpgsm.\n    \"\"\"\n\nMy interpretation is that the trust level is conceptionally different\nfrom the validity of the key and/or signature.  That seems to also have\nbeen the assumption of the old code in check_signature() where a result\nof 'G' (as in GOODSIG) and 'U' (as in TRUST_NEVER or TRUST_UNDEFINED)\nwere both considered a success.\n\nThe two cases where a result of 'U' had special meaning were in\nverify_merge_signature() (where this caused git to die()) and in\nformat_commit_one() (where it affected the output of the %G? format\nspecifier).\n\nI think it makes sense to refactor the processing of TRUST_ status lines\nsuch that users can configure a minimum trust level that is enforced\nglobally, rather than have individual parts of git (e.g. merge) do it\nthemselves (except for a grace period with backward compatibility).\n\nI also think it makes sense to not store the trust level in the same\nstruct member as the key/signature status.  While the presence of a\nTRUST_ status code does imply that the signature is good (see the first\nparagraph in the included snippet above), as far as I can tell, the\norder of the status lines from GPG isn't well-defined; thus it would\nseem plausible that the trust level could be overwritten with the\nkey/signature status if they were stored in the same member of the\nsignature_check structure.\n\nThis patch introduces a new configuration option: gpg.minTrustLevel.  It\nconsolidates trust-level verification to gpg-interface.c and adds a new\n`trust_level` member to the signature_check structure.\n\nBackward-compatibility is maintained by introducing a special case in\nverify_merge_signature() such that if no user-configurable\ngpg.minTrustLevel is set, then the old behavior of rejecting\nTRUST_UNDEFINED and TRUST_NEVER is enforced.  If, on the other hand,\ngpg.minTrustLevel is set, then that value overrides the old behavior.\n\nSimilarly, the %G? format specifier will continue show 'U' for\nsignatures made with a key that has a trust level of TRUST_UNDEFINED or\nTRUST_NEVER, even though the 'U' character no longer exist in the\n`result` member of the signature_check structure.  A new format\nspecifier, %GT, is also introduced for users that want to show all\npossible trust levels for a signature.\n\nAnother approach would have been to simply drop the trust-level\nrequirement in verify_merge_signature().  This would also have made the\nbehavior consistent with other parts of git that perform signature\nverification.  However, requiring a minimum trust level for signing keys\ndoes seem to have a real-world use-case.  For example, the build system\nused by the Qubes OS project currently parses the raw output from\nverify-tag in order to assert a minimum trust level for keys used to\nsign git tags [2].\n\n\nChanges since v0:\n* Added backward-compatibility with the old behavior of\n  verify_merge_signature().  The old behavior is overridden if a user\n  has configured gpg.minTrustLevel.  My approach is kind of ugly because\n  now all users of verify_merge_signature() has to be aware of\n  gpg.minTrustLevel to know if it should override the default behavior.\n  An alternative might be to have a configurable per-operation trust\n  level (e.g. merge.minTrustLevel), but I'm not sure how sensible that\n  is either.\n* Added backward-compatiblity with the old behavior of %G?.\n\n[1] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/doc/DETAILS;h=bd00006e933ac56719b1edd2478ecd79273eae72;hb=refs/heads/master\n[2] https://github.com/QubesOS/qubes-builder/blob/9674c1991deef45b1a1b1c71fddfab14ba50dccf/scripts/verify-git-tag#L43\n\nHans Jerry Illikainen (1):\n  gpg-interface: add minTrustLevel as a configuration option\n\n Documentation/config/gpg.txt       | 15 ++++++\n Documentation/pretty-formats.txt   |  1 +\n builtin/merge.c                    |  9 +++-\n builtin/pull.c                     | 13 ++++-\n commit.c                           | 12 +++--\n commit.h                           | 12 ++++-\n gpg-interface.c                    | 85 +++++++++++++++++++++++++-----\n gpg-interface.h                    | 10 +++-\n pretty.c                           | 30 ++++++++++-\n t/t5573-pull-verify-signatures.sh  | 64 ++++++++++++++++++++++\n t/t7030-verify-tag.sh              | 24 +++++++++\n t/t7510-signed-commit.sh           | 39 ++++++++++++++\n t/t7612-merge-verify-signatures.sh | 22 ++++++++\n 13 files changed, 313 insertions(+), 23 deletions(-)\n\n--\n2.20.1\n"},{"id":"388487","messageId":"20191219000140.9056-2-hji@dyntopia.com","threadId":"52467","inReplyTo":"20191219000140.9056-1-hji@dyntopia.com","subject":"[PATCH v1 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-19T00:01:40Z","receivedAt":"2019-12-19T00:01:55Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"Previously, signature verification for merge and pull operations checked\nif the key had a trust-level of either TRUST_NEVER or TRUST_UNDEFINED in\nverify_merge_signature().  If that was the case, the process die()d.\n\nThe other code paths that did signature verification relied entirely on\nthe return code from check_commit_signature().  And signatures made with\na good key, irregardless of its trust level, was considered valid by\ncheck_commit_signature().\n\nThis difference in behavior might induce users to erroneously assume\nthat the trust level of a key in their keyring is always considered by\nGit, even for operations where it is not (e.g. during a verify-commit or\nverify-tag).\n\nThe way it worked was by gpg-interface.c storing the result from the\nkey/signature status *and* the lowest-two trust levels in the `result`\nmember of the signature_check structure (the last of these status lines\nthat were encountered got written to `result`).  These are documented in\nGPG under the subsection `General status codes` and `Key related`,\nrespectively [1].\n\nThe GPG documentation says the following on the TRUST_ status codes [1]:\n\n    \"\"\"\n    These are several similar status codes:\n\n    - TRUST_UNDEFINED <error_token>\n    - TRUST_NEVER     <error_token>\n    - TRUST_MARGINAL  [0  [<validation_model>]]\n    - TRUST_FULLY     [0  [<validation_model>]]\n    - TRUST_ULTIMATE  [0  [<validation_model>]]\n\n    For good signatures one of these status lines are emitted to\n    indicate the validity of the key used to create the signature.\n    The error token values are currently only emitted by gpgsm.\n    \"\"\"\n\nMy interpretation is that the trust level is conceptionally different\nfrom the validity of the key and/or signature.  That seems to also have\nbeen the assumption of the old code in check_signature() where a result\nof 'G' (as in GOODSIG) and 'U' (as in TRUST_NEVER or TRUST_UNDEFINED)\nwere both considered a success.\n\nThe two cases where a result of 'U' had special meaning were in\nverify_merge_signature() (where this caused git to die()) and in\nformat_commit_one() (where it affected the output of the %G? format\nspecifier).\n\nI think it makes sense to refactor the processing of TRUST_ status lines\nsuch that users can configure a minimum trust level that is enforced\nglobally, rather than have individual parts of git (e.g. merge) do it\nthemselves (except for a grace period with backward compatibility).\n\nI also think it makes sense to not store the trust level in the same\nstruct member as the key/signature status.  While the presence of a\nTRUST_ status code does imply that the signature is good (see the first\nparagraph in the included snippet above), as far as I can tell, the\norder of the status lines from GPG isn't well-defined; thus it would\nseem plausible that the trust level could be overwritten with the\nkey/signature status if they were stored in the same member of the\nsignature_check structure.\n\nThis patch introduces a new configuration option: gpg.minTrustLevel.  It\nconsolidates trust-level verification to gpg-interface.c and adds a new\n`trust_level` member to the signature_check structure.\n\nBackward-compatibility is maintained by introducing a special case in\nverify_merge_signature() such that if no user-configurable\ngpg.minTrustLevel is set, then the old behavior of rejecting\nTRUST_UNDEFINED and TRUST_NEVER is enforced.  If, on the other hand,\ngpg.minTrustLevel is set, then that value overrides the old behavior.\n\nSimilarly, the %G? format specifier will continue show 'U' for\nsignatures made with a key that has a trust level of TRUST_UNDEFINED or\nTRUST_NEVER, even though the 'U' character no longer exist in the\n`result` member of the signature_check structure.  A new format\nspecifier, %GT, is also introduced for users that want to show all\npossible trust levels for a signature.\n\nAnother approach would have been to simply drop the trust-level\nrequirement in verify_merge_signature().  This would also have made the\nbehavior consistent with other parts of git that perform signature\nverification.  However, requiring a minimum trust level for signing keys\ndoes seem to have a real-world use-case.  For example, the build system\nused by the Qubes OS project currently parses the raw output from\nverify-tag in order to assert a minimum trust level for keys used to\nsign git tags [2].\n\n[1] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/doc/DETAILS;h=bd00006e933ac56719b1edd2478ecd79273eae72;hb=refs/heads/master\n[2] https://github.com/QubesOS/qubes-builder/blob/9674c1991deef45b1a1b1c71fddfab14ba50dccf/scripts/verify-git-tag#L43\n\nSigned-off-by: Hans Jerry Illikainen <hji@dyntopia.com>\n---\n Documentation/config/gpg.txt       | 15 ++++++\n Documentation/pretty-formats.txt   |  1 +\n builtin/merge.c                    |  9 +++-\n builtin/pull.c                     | 13 ++++-\n commit.c                           | 12 +++--\n commit.h                           | 12 ++++-\n gpg-interface.c                    | 85 +++++++++++++++++++++++++-----\n gpg-interface.h                    | 10 +++-\n pretty.c                           | 30 ++++++++++-\n t/t5573-pull-verify-signatures.sh  | 64 ++++++++++++++++++++++\n t/t7030-verify-tag.sh              | 24 +++++++++\n t/t7510-signed-commit.sh           | 39 ++++++++++++++\n t/t7612-merge-verify-signatures.sh | 22 ++++++++\n 13 files changed, 313 insertions(+), 23 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex cce2c89245..d94025cb36 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -18,3 +18,18 @@ gpg.<format>.program::\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n \tvalue for `gpg.x509.program` is \"gpgsm\".\n+\n+gpg.minTrustLevel::\n+\tSpecifies a minimum trust level for signature verification.  If\n+\tthis option is unset, then signature verification for merge\n+\toperations require a key with at least `marginal` trust.  Other\n+\toperations that perform signature verification require a key\n+\twith at least `undefined` trust.  Setting this option overrides\n+\tthe required trust-level for all operations.  Supported values,\n+\tin increasing order of significance:\n++\n+* `undefined`\n+* `never`\n+* `marginal`\n+* `fully`\n+* `ultimate`\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 1a7212ce5a..a4b6f49186 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -226,6 +226,7 @@ endif::git-rev-list[]\n '%GF':: show the fingerprint of the key used to sign a signed commit\n '%GP':: show the fingerprint of the primary key whose subkey was used\n \tto sign a signed commit\n+'%GT':: show the trust level for the key used to sign a signed commit\n '%gD':: reflog selector, e.g., `refs/stash@{1}` or `refs/stash@{2\n \tminutes ago}`; the format follows the rules described for the\n \t`-g` option. The portion before the `@` is the refname as\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 062e911441..d127d2225f 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -62,6 +62,7 @@ static int show_diffstat = 1, shortlog_len = -1, squash;\n static int option_commit = -1;\n static int option_edit = -1;\n static int allow_trivial = 1, have_message, verify_signatures;\n+static int check_trust_level = 1;\n static int overwrite_ignore = 1;\n static struct strbuf merge_msg = STRBUF_INIT;\n static struct strategy **use_strategies;\n@@ -631,6 +632,8 @@ static int git_merge_config(const char *k, const char *v, void *cb)\n \t} else if (!strcmp(k, \"commit.gpgsign\")) {\n \t\tsign_commit = git_config_bool(k, v) ? \"\" : NULL;\n \t\treturn 0;\n+\t} else if (!strcmp(k, \"gpg.mintrustlevel\")) {\n+\t\tcheck_trust_level = 0;\n \t}\n \n \tstatus = fmt_merge_msg_config(k, v, cb);\n@@ -1397,7 +1400,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \t\t\tdie(_(\"Can merge only exactly one commit into empty head\"));\n \n \t\tif (verify_signatures)\n-\t\t\tverify_merge_signature(remoteheads->item, verbosity);\n+\t\t\tverify_merge_signature(remoteheads->item, verbosity,\n+\t\t\t\t\t       check_trust_level);\n \n \t\tremote_head_oid = &remoteheads->item->object.oid;\n \t\tread_empty(remote_head_oid, 0);\n@@ -1420,7 +1424,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \n \tif (verify_signatures) {\n \t\tfor (p = remoteheads; p; p = p->next) {\n-\t\t\tverify_merge_signature(p->item, verbosity);\n+\t\t\tverify_merge_signature(p->item, verbosity,\n+\t\t\t\t\t       check_trust_level);\n \t\t}\n \t}\n \ndiff --git a/builtin/pull.c b/builtin/pull.c\nindex d25ff13a60..d4e3e77c8e 100644\n--- a/builtin/pull.c\n+++ b/builtin/pull.c\n@@ -107,6 +107,7 @@ static char *opt_ff;\n static char *opt_verify_signatures;\n static int opt_autostash = -1;\n static int config_autostash;\n+static int check_trust_level = 1;\n static struct argv_array opt_strategies = ARGV_ARRAY_INIT;\n static struct argv_array opt_strategy_opts = ARGV_ARRAY_INIT;\n static char *opt_gpg_sign;\n@@ -355,6 +356,8 @@ static enum rebase_type config_get_rebase(void)\n  */\n static int git_pull_config(const char *var, const char *value, void *cb)\n {\n+\tint status;\n+\n \tif (!strcmp(var, \"rebase.autostash\")) {\n \t\tconfig_autostash = git_config_bool(var, value);\n \t\treturn 0;\n@@ -362,7 +365,14 @@ static int git_pull_config(const char *var, const char *value, void *cb)\n \t\trecurse_submodules = git_config_bool(var, value) ?\n \t\t\tRECURSE_SUBMODULES_ON : RECURSE_SUBMODULES_OFF;\n \t\treturn 0;\n+\t} else if (!strcmp(var, \"gpg.mintrustlevel\")) {\n+\t\tcheck_trust_level = 0;\n \t}\n+\n+\tstatus = git_gpg_config(var, value, cb);\n+\tif (status)\n+\t\treturn status;\n+\n \treturn git_default_config(var, value, cb);\n }\n \n@@ -587,7 +597,8 @@ static int pull_into_void(const struct object_id *merge_head,\n \t\t\tdie(_(\"unable to access commit %s\"),\n \t\t\t    oid_to_hex(merge_head));\n \n-\t\tverify_merge_signature(commit, opt_verbosity);\n+\t\tverify_merge_signature(commit, opt_verbosity,\n+\t\t\t\t       check_trust_level);\n \t}\n \n \t/*\ndiff --git a/commit.c b/commit.c\nindex 434ec030d6..3f91d3efc5 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1136,21 +1136,23 @@ int check_commit_signature(const struct commit *commit, struct signature_check *\n \treturn ret;\n }\n \n-void verify_merge_signature(struct commit *commit, int verbosity)\n+void verify_merge_signature(struct commit *commit, int verbosity,\n+\t\t\t    int check_trust)\n {\n \tchar hex[GIT_MAX_HEXSZ + 1];\n \tstruct signature_check signature_check;\n+\tint ret;\n \tmemset(&signature_check, 0, sizeof(signature_check));\n \n-\tcheck_commit_signature(commit, &signature_check);\n+\tret = check_commit_signature(commit, &signature_check);\n \n \tfind_unique_abbrev_r(hex, &commit->object.oid, DEFAULT_ABBREV);\n \tswitch (signature_check.result) {\n \tcase 'G':\n+\t\tif (ret || (check_trust && signature_check.trust_level < TRUST_MARGINAL))\n+\t\t\tdie(_(\"Commit %s has an untrusted GPG signature, \"\n+\t\t\t      \"allegedly by %s.\"), hex, signature_check.signer);\n \t\tbreak;\n-\tcase 'U':\n-\t\tdie(_(\"Commit %s has an untrusted GPG signature, \"\n-\t\t      \"allegedly by %s.\"), hex, signature_check.signer);\n \tcase 'B':\n \t\tdie(_(\"Commit %s has a bad GPG signature \"\n \t\t      \"allegedly by %s.\"), hex, signature_check.signer);\ndiff --git a/commit.h b/commit.h\nindex 221cdaa34b..008a0fa4a0 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -383,8 +383,18 @@ int compare_commits_by_author_date(const void *a_, const void *b_, void *unused)\n  * Verify a single commit with check_commit_signature() and die() if it is not\n  * a good signature. This isn't really suitable for general use, but is a\n  * helper to implement consistent logic for pull/merge --verify-signatures.\n+ *\n+ * The check_trust parameter is meant for backward-compatibility.  The GPG\n+ * interface verifies key trust with a default trust level that is below the\n+ * default trust level for merge operations.  Its value should be non-zero if\n+ * the user hasn't set a minimum trust level explicitly in their configuration.\n+ *\n+ * If the user has set a minimum trust level, then that value should be obeyed\n+ * and check_trust should be zero, even if the configured trust level is below\n+ * the default trust level for merges.\n  */\n-void verify_merge_signature(struct commit *commit, int verbose);\n+void verify_merge_signature(struct commit *commit, int verbose,\n+\t\t\t    int check_trust);\n \n int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused);\n int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 5134ce2780..f7b11480fb 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -7,6 +7,8 @@\n #include \"tempfile.h\"\n \n static char *configured_signing_key;\n+static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n+\n struct gpg_format {\n \tconst char *name;\n \tconst char *program;\n@@ -85,6 +87,8 @@ void signature_check_clear(struct signature_check *sigc)\n #define GPG_STATUS_UID\t\t(1<<2)\n /* The status includes key fingerprints */\n #define GPG_STATUS_FINGERPRINT\t(1<<3)\n+/* The status includes trust level */\n+#define GPG_STATUS_TRUST_LEVEL\t(1<<4)\n \n /* Short-hand for standard exclusive *SIG status with keyid & UID */\n #define GPG_STATUS_STDSIG\t(GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID|GPG_STATUS_UID)\n@@ -96,13 +100,23 @@ static struct {\n } sigcheck_gpg_status[] = {\n \t{ 'G', \"GOODSIG \", GPG_STATUS_STDSIG },\n \t{ 'B', \"BADSIG \", GPG_STATUS_STDSIG },\n-\t{ 'U', \"TRUST_NEVER\", 0 },\n-\t{ 'U', \"TRUST_UNDEFINED\", 0 },\n \t{ 'E', \"ERRSIG \", GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID },\n \t{ 'X', \"EXPSIG \", GPG_STATUS_STDSIG },\n \t{ 'Y', \"EXPKEYSIG \", GPG_STATUS_STDSIG },\n \t{ 'R', \"REVKEYSIG \", GPG_STATUS_STDSIG },\n \t{ 0, \"VALIDSIG \", GPG_STATUS_FINGERPRINT },\n+\t{ 0, \"TRUST_\", GPG_STATUS_TRUST_LEVEL },\n+};\n+\n+static struct {\n+\tconst char *key;\n+\tenum signature_trust_level value;\n+} sigcheck_gpg_trust_level[] = {\n+\t{ \"UNDEFINED\", TRUST_UNDEFINED },\n+\t{ \"NEVER\", TRUST_NEVER },\n+\t{ \"MARGINAL\", TRUST_MARGINAL },\n+\t{ \"FULLY\", TRUST_FULLY },\n+\t{ \"ULTIMATE\", TRUST_ULTIMATE },\n };\n \n static void replace_cstring(char **field, const char *line, const char *next)\n@@ -115,10 +129,25 @@ static void replace_cstring(char **field, const char *line, const char *next)\n \t\t*field = NULL;\n }\n \n+static int parse_gpg_trust_level(const char *level,\n+\t\t\t\t enum signature_trust_level *res)\n+{\n+\tsize_t i;\n+\n+\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_trust_level); i++) {\n+\t\tif (!strcmp(sigcheck_gpg_trust_level[i].key, level)) {\n+\t\t\t*res = sigcheck_gpg_trust_level[i].value;\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\treturn 1;\n+}\n+\n static void parse_gpg_output(struct signature_check *sigc)\n {\n \tconst char *buf = sigc->gpg_status;\n \tconst char *line, *next;\n+\tchar *trust;\n \tint i, j;\n \tint seen_exclusive_status = 0;\n \n@@ -136,9 +165,18 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t\t/* Iterate over all search strings */\n \t\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {\n \t\t\tif (skip_prefix(line, sigcheck_gpg_status[i].check, &line)) {\n+\t\t\t\t/*\n+\t\t\t\t * GOODSIG, BADSIG etc. can occur only once for\n+\t\t\t\t * each signature.  Therefore, if we had more\n+\t\t\t\t * than one then we're dealing with multiple\n+\t\t\t\t * signatures.  We don't support them\n+\t\t\t\t * currently, and they're rather hard to\n+\t\t\t\t * create, so something is likely fishy and we\n+\t\t\t\t * should reject them altogether.\n+\t\t\t\t */\n \t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_EXCLUSIVE) {\n \t\t\t\t\tif (seen_exclusive_status++)\n-\t\t\t\t\t\tgoto found_duplicate_status;\n+\t\t\t\t\t\tgoto error;\n \t\t\t\t}\n \n \t\t\t\tif (sigcheck_gpg_status[i].result)\n@@ -154,6 +192,18 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t\t\t\t\t\treplace_cstring(&sigc->signer, line, next);\n \t\t\t\t\t}\n \t\t\t\t}\n+\n+\t\t\t\t/* Do we have trust level? */\n+\t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_TRUST_LEVEL) {\n+\t\t\t\t\tnext = strchrnul(line, ' ');\n+\t\t\t\t\ttrust = xmemdupz(line, next - line);\n+\t\t\t\t\tif (parse_gpg_trust_level(trust, &sigc->trust_level)) {\n+\t\t\t\t\t\tfree(trust);\n+\t\t\t\t\t\tgoto error;\n+\t\t\t\t\t}\n+\t\t\t\t\tfree(trust);\n+\t\t\t\t}\n+\n \t\t\t\t/* Do we have fingerprint? */\n \t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_FINGERPRINT) {\n \t\t\t\t\tconst char *limit;\n@@ -191,14 +241,7 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t}\n \treturn;\n \n-found_duplicate_status:\n-\t/*\n-\t * GOODSIG, BADSIG etc. can occur only once for each signature.\n-\t * Therefore, if we had more than one then we're dealing with multiple\n-\t * signatures.  We don't support them currently, and they're rather\n-\t * hard to create, so something is likely fishy and we should reject\n-\t * them altogether.\n-\t */\n+error:\n \tsigc->result = 'E';\n \t/* Clear partial data to avoid confusion */\n \tFREE_AND_NULL(sigc->primary_key_fingerprint);\n@@ -264,6 +307,7 @@ int check_signature(const char *payload, size_t plen, const char *signature,\n \tint status;\n \n \tsigc->result = 'N';\n+\tsigc->trust_level = -1;\n \n \tstatus = verify_signed_buffer(payload, plen, signature, slen,\n \t\t\t\t      &gpg_output, &gpg_status);\n@@ -273,7 +317,8 @@ int check_signature(const char *payload, size_t plen, const char *signature,\n \tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n \tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n \tparse_gpg_output(sigc);\n-\tstatus |= sigc->result != 'G' && sigc->result != 'U';\n+\tstatus |= sigc->result != 'G';\n+\tstatus |= sigc->trust_level < configured_min_trust_level;\n \n  out:\n \tstrbuf_release(&gpg_status);\n@@ -320,6 +365,8 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n {\n \tstruct gpg_format *fmt = NULL;\n \tchar *fmtname = NULL;\n+\tchar *trust;\n+\tint ret;\n \n \tif (!strcmp(var, \"user.signingkey\")) {\n \t\tif (!value)\n@@ -339,6 +386,20 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.mintrustlevel\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\n+\t\ttrust = xstrdup_toupper(value);\n+\t\tret = parse_gpg_trust_level(trust, &configured_min_trust_level);\n+\t\tfree(trust);\n+\n+\t\tif (ret)\n+\t\t\treturn error(\"unsupported value for %s: %s\", var,\n+\t\t\t\t     value);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n \t\tfmtname = \"openpgp\";\n \ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 93cc3aff5c..f4e9b4f371 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -7,6 +7,14 @@ struct strbuf;\n #define GPG_VERIFY_RAW\t\t\t2\n #define GPG_VERIFY_OMIT_STATUS\t4\n \n+enum signature_trust_level {\n+\tTRUST_UNDEFINED,\n+\tTRUST_NEVER,\n+\tTRUST_MARGINAL,\n+\tTRUST_FULLY,\n+\tTRUST_ULTIMATE,\n+};\n+\n struct signature_check {\n \tchar *payload;\n \tchar *gpg_output;\n@@ -16,7 +24,6 @@ struct signature_check {\n \t * possible \"result\":\n \t * 0 (not checked)\n \t * N (checked but no further result)\n-\t * U (untrusted good)\n \t * G (good)\n \t * B (bad)\n \t */\n@@ -25,6 +32,7 @@ struct signature_check {\n \tchar *key;\n \tchar *fingerprint;\n \tchar *primary_key_fingerprint;\n+\tenum signature_trust_level trust_level;\n };\n \n void signature_check_clear(struct signature_check *sigc);\ndiff --git a/pretty.c b/pretty.c\nindex 305e903192..f5fbbc5ffb 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1311,9 +1311,18 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\tcase '?':\n \t\t\tswitch (c->signature_check.result) {\n \t\t\tcase 'G':\n+\t\t\t\tswitch (c->signature_check.trust_level) {\n+\t\t\t\tcase TRUST_UNDEFINED:\n+\t\t\t\tcase TRUST_NEVER:\n+\t\t\t\t\tstrbuf_addch(sb, 'U');\n+\t\t\t\t\tbreak;\n+\t\t\t\tdefault:\n+\t\t\t\t\tstrbuf_addch(sb, 'G');\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t\tbreak;\n \t\t\tcase 'B':\n \t\t\tcase 'E':\n-\t\t\tcase 'U':\n \t\t\tcase 'N':\n \t\t\tcase 'X':\n \t\t\tcase 'Y':\n@@ -1337,6 +1346,25 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tif (c->signature_check.primary_key_fingerprint)\n \t\t\t\tstrbuf_addstr(sb, c->signature_check.primary_key_fingerprint);\n \t\t\tbreak;\n+\t\tcase 'T':\n+\t\t\tswitch (c->signature_check.trust_level) {\n+\t\t\tcase TRUST_UNDEFINED:\n+\t\t\t\tstrbuf_addstr(sb, \"undefined\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_NEVER:\n+\t\t\t\tstrbuf_addstr(sb, \"never\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_MARGINAL:\n+\t\t\t\tstrbuf_addstr(sb, \"marginal\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_FULLY:\n+\t\t\t\tstrbuf_addstr(sb, \"fully\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_ULTIMATE:\n+\t\t\t\tstrbuf_addstr(sb, \"ultimate\");\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tbreak;\n \t\tdefault:\n \t\t\treturn 0;\n \t\t}\ndiff --git a/t/t5573-pull-verify-signatures.sh b/t/t5573-pull-verify-signatures.sh\nindex 3e9876e197..a53dd8550d 100755\n--- a/t/t5573-pull-verify-signatures.sh\n+++ b/t/t5573-pull-verify-signatures.sh\n@@ -60,6 +60,27 @@ test_expect_success GPG 'pull commit with untrusted signature with --verify-sign\n \ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n '\n \n+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=ultimate' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=marginal' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\ttest_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=undefined' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel undefined &&\n+\tgit pull --ff-only --verify-signatures untrusted >pulloutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" pulloutput\n+'\n+\n test_expect_success GPG 'pull signed commit with --verify-signatures' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n \tgit pull --verify-signatures signed >pulloutput &&\n@@ -79,10 +100,53 @@ test_expect_success GPG 'pull commit with bad signature with --no-verify-signatu\n '\n \n test_expect_success GPG 'pull unsigned commit into unborn branch' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n \tgit init empty-repo &&\n \ttest_must_fail \\\n \t\tgit -C empty-repo pull --verify-signatures ..  2>pullerror &&\n \ttest_i18ngrep \"does not have a GPG signature\" pullerror\n '\n \n+test_expect_success GPG 'pull commit into unborn branch with bad signature and --verify-signatures' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../bad 2>pullerror &&\n+\ttest_i18ngrep \"has a bad GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=ultimate' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_config_global gpg.minTrustLevel ultimate &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=marginal' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_config_global gpg.minTrustLevel marginal &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=undefined' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_config_global gpg.minTrustLevel undefined &&\n+\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted >pulloutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" pulloutput\n+'\n+\n test_done\ndiff --git a/t/t7030-verify-tag.sh b/t/t7030-verify-tag.sh\nindex 8f077bea60..5c5bc32ccb 100755\n--- a/t/t7030-verify-tag.sh\n+++ b/t/t7030-verify-tag.sh\n@@ -86,6 +86,30 @@ test_expect_success GPGSM 'verify and show signatures x509' '\n \techo ninth-signed-x509 OK\n '\n \n+test_expect_success GPGSM 'verify and show signatures x509 with low minTrustLevel' '\n+\ttest_config gpg.minTrustLevel undefined &&\n+\tgit verify-tag ninth-signed-x509 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual &&\n+\techo ninth-signed-x509 OK\n+'\n+\n+test_expect_success GPGSM 'verify and show signatures x509 with matching minTrustLevel' '\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-tag ninth-signed-x509 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual &&\n+\techo ninth-signed-x509 OK\n+'\n+\n+test_expect_success GPGSM 'verify and show signatures x509 with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-tag ninth-signed-x509 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual &&\n+\techo ninth-signed-x509 OK\n+'\n+\n test_expect_success GPG 'detect fudged signature' '\n \tgit cat-file tag seventh-signed >raw &&\n \tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\ndiff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\nindex 682b23a068..0c06d22a00 100755\n--- a/t/t7510-signed-commit.sh\n+++ b/t/t7510-signed-commit.sh\n@@ -109,6 +109,21 @@ test_expect_success GPG 'verify-commit exits success on untrusted signature' '\n \tgrep \"not certified\" actual\n '\n \n+test_expect_success GPG 'verify-commit exits success with matching minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPG 'verify-commit exits success with low minTrustLevel' '\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPG 'verify-commit exits failure with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-commit eighth-signed-alt\n+'\n+\n test_expect_success GPG 'verify signatures with --raw' '\n \t(\n \t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n@@ -219,6 +234,30 @@ test_expect_success GPG 'show untrusted signature with custom format' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPG 'show untrusted signature with undefined trust level' '\n+\tcat >expect <<-\\EOF &&\n+\tundefined\n+\t65A0EEA02E30CAD7\n+\tEris Discordia <discord@example.net>\n+\tF8364A59E07FFE9F4D63005A65A0EEA02E30CAD7\n+\tD4BE22311AD3131E5EDA29A461092E85B7227189\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPG 'show untrusted signature with ultimate trust level' '\n+\tcat >expect <<-\\EOF &&\n+\tultimate\n+\t13B6F51ECDDE430D\n+\tC O Mitter <committer@example.com>\n+\t73D758744BE721698EC54E8713B6F51ECDDE430D\n+\t73D758744BE721698EC54E8713B6F51ECDDE430D\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success GPG 'show unknown signature with custom format' '\n \tcat >expect <<-\\EOF &&\n \tE\ndiff --git a/t/t7612-merge-verify-signatures.sh b/t/t7612-merge-verify-signatures.sh\nindex d99218a725..a426f3a89a 100755\n--- a/t/t7612-merge-verify-signatures.sh\n+++ b/t/t7612-merge-verify-signatures.sh\n@@ -66,6 +66,20 @@ test_expect_success GPG 'merge commit with untrusted signature with verification\n \ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n '\n \n+test_expect_success GPG 'merge commit with untrusted signature with verification and high minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\ttest_must_fail git merge --ff-only --verify-signatures side-untrusted 2>mergeerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n+'\n+\n+test_expect_success GPG 'merge commit with untrusted signature with verification and low minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel undefined &&\n+\tgit merge --ff-only --verify-signatures side-untrusted >mergeoutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" mergeoutput\n+'\n+\n test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n \ttest_config merge.verifySignatures true &&\n@@ -73,6 +87,14 @@ test_expect_success GPG 'merge commit with untrusted signature with merge.verify\n \ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n '\n \n+test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true and minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config merge.verifySignatures true &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\ttest_must_fail git merge --ff-only side-untrusted 2>mergeerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n+'\n+\n test_expect_success GPG 'merge signed commit with verification' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n \tgit merge --verbose --ff-only --verify-signatures side-signed >mergeoutput &&\n-- \n2.20.1\n\n"},{"id":"388488","messageId":"87pnglyylt.hji@dyntopia.com","threadId":"52467","inReplyTo":"xmqq4ky0j8ca.fsf@gitster-ct.c.googlers.com","subject":"Re: [PATCH 0/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-18T23:59:10Z","receivedAt":"2019-12-19T00:02:28Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"On Mon, Dec 16 2019, Junio C Hamano wrote:\n>> Unfortunately, it breaks backward-compatibility in two ways:\n>>\n>> 1. The default trust level is TRUST_UNDEFINED.  This is compatible with\n>>    the old behavior of every code path *except* for\n>>    verify_merge_signature() (since, again, it used to die()s on trust\n>>    levels below TRUST_MARGINAL).\n>\n> This might be a bit problematic.  If we can keep the default\n> behaviour identical to the code before this patch, while allowing\n> the configuration to tweak the behaviour, that would have been\n> more easily acceptable.\n\nDone in v1.\n\n>> 2. The %G? format specifier no longer includes 'U' for signatures made\n>>    with a key that is either TRUST_UNDEFINED or TRUST_NEVER.\n>\n> Hmm, I can sort-of-see why you want to introduce a new placeholder\n> \"%GT\" to disambiguate two sources of 'U', but why would this change\n> to \"%G?\" necessary?\n\nU is re-introduced in v1.  %GT is still there (since %G? doesn't print\nall trust levels) but I don't mind removing it (I added it for\ncompleteness sake when breaking backward-compatibility in v0).\n\n-- \nhji\n"},{"id":"388694","messageId":"20191220225746.GF8609@szeder.dev","threadId":"52467","inReplyTo":"20191216153204.8906-2-hji@dyntopia.com","subject":"Re: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2019-12-20T22:57:46Z","receivedAt":"2019-12-20T22:57:56Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, Dec 16, 2019 at 03:32:04PM +0000, Hans Jerry Illikainen wrote:\n> Previously, signature verification for merge and pull operations checked\n> if the key had a trust-level of either TRUST_NEVER or TRUST_UNDEFINED in\n> verify_merge_signature().  If that was the case, the process die()d.\n> \n> The other code paths that did signature verification relied entirely on\n> the return code from check_commit_signature().  And signatures made with\n> a good key, irregardless of its trust level, was considered valid by\n> check_commit_signature().\n> \n> This difference in behavior might induce users to erroneously assume\n> that the trust level of a key in their keyring is always considered by\n> Git, even for operations where it is not (e.g. during a verify-commit or\n> verify-tag).\n> \n> The way it worked was by gpg-interface.c storing the result from the\n> key/signature status *and* the lowest-two trust levels in the `result`\n> member of the signature_check structure (the last of these status lines\n> that were encountered got written to `result`).  These are documented in\n> GPG under the subsection `General status codes` and `Key related`,\n> respectively [1].\n> \n> The GPG documentation says the following on the TRUST_ status codes [1]:\n> \n>     \"\"\"\n>     These are several similar status codes:\n> \n>     - TRUST_UNDEFINED <error_token>\n>     - TRUST_NEVER     <error_token>\n>     - TRUST_MARGINAL  [0  [<validation_model>]]\n>     - TRUST_FULLY     [0  [<validation_model>]]\n>     - TRUST_ULTIMATE  [0  [<validation_model>]]\n> \n>     For good signatures one of these status lines are emitted to\n>     indicate the validity of the key used to create the signature.\n>     The error token values are currently only emitted by gpgsm.\n>     \"\"\"\n> \n> My interpretation is that the trust level is conceptionally different\n> from the validity of the key and/or signature.  That seems to also have\n> been the assumption of the old code in check_signature() where a result\n> of 'G' (as in GOODSIG) and 'U' (as in TRUST_NEVER or TRUST_UNDEFINED)\n> were both considered a success.\n> \n> The two cases where a result of 'U' had special meaning were in\n> verify_merge_signature() (where this caused git to die()) and in\n> format_commit_one() (where it affected the output of the %G? format\n> specifier).\n> \n> I think it makes sense to refactor the processing of TRUST_ status lines\n> such that users can configure a minimum trust level that is enforced\n> globally, rather than have individual parts of git (e.g. merge) do it\n> themselves.\n> \n> I also think it makes sense to not store the trust level in the same\n> struct member as the key/signature status.  While the presence of a\n> TRUST_ status code does imply that the signature is good (see the first\n> paragraph in the included snippet above), as far as I can tell, the\n> order of the status lines from GPG isn't well-defined; thus it would\n> seem plausible that the trust level could be overwritten with the\n> key/signature status if they were stored in the same member of the\n> signature_check structure.\n> \n> This patch introduces a new configuration option: gpg.minTrustLevel.  It\n> consolidates trust-level verification to gpg-interface.c and adds a new\n> `trust_level` member to the signature_check structure.\n> \n> Unfortunately, it breaks backward-compatibility in two ways:\n> \n> 1. The default trust level is TRUST_UNDEFINED.  This is compatible with\n>    the old behavior of every code path *except* for\n>    verify_merge_signature() (since, again, it used to die()s on trust\n>    levels below TRUST_MARGINAL).\n> \n> 2. The %G? format specifier no longer includes 'U' for signatures made\n>    with a key that is either TRUST_UNDEFINED or TRUST_NEVER.  Instead, a\n>    new %GT format specifier is introduced that outputs the trust level\n>    (as a complete string to avoid ambiguity with TRUST_UNDEFINED and\n>    TRUST_ULTIMATE).\n> \n> Another approach would have been to simply drop the trust-level\n> requirement in verify_merge_signature().  This would also have made the\n> behavior consistent with other parts of git that perform signature\n> verification (and it would also have broken backward-compatibility #1).\n> However, requiring a minimum trust level for signing keys does seem to\n> have a real-world use-case.  For example, the build system used by the\n> Qubes OS project currently parses the raw output from verify-tag in\n> order to assert a minimum trust level for keys used to sign git tags\n> [2].\n> \n> [1] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/doc/DETAILS;h=bd00006e933ac56719b1edd2478ecd79273eae72;hb=refs/heads/master\n> [2] https://github.com/QubesOS/qubes-builder/blob/9674c1991deef45b1a1b1c71fddfab14ba50dccf/scripts/verify-git-tag#L43\n\nThis patch causes several test failures:\n\n  https://travis-ci.org/git/git/jobs/627909430#L2259\n\nI see you've already posted an updated version, so I tried it locally,\nand the same test scripts fail with the updated version as well.\n\nI noticed that only Linux CI jobs failed, while the OSX jobs\nsucceeded.  Our Linux CI jobs (and my box) are based on Ubuntu 16.04,\nand thus use GnuPG v1.4, while the OSX jobs use v2.something.  Not\nsure that the version difference is connected to the test failures,\nbut I figured it's worth pointing out.\n\n> Signed-off-by: Hans Jerry Illikainen <hji@dyntopia.com>\n> ---\n>  Documentation/config/gpg.txt       | 11 ++++\n>  Documentation/pretty-formats.txt   |  2 +-\n>  commit.c                           |  9 ++--\n>  gpg-interface.c                    | 85 +++++++++++++++++++++++++-----\n>  gpg-interface.h                    | 10 +++-\n>  pretty.c                           | 20 ++++++-\n>  t/t5573-pull-verify-signatures.sh  |  7 +++\n>  t/t7510-signed-commit.sh           | 19 ++++++-\n>  t/t7612-merge-verify-signatures.sh | 15 ++++++\n>  9 files changed, 157 insertions(+), 21 deletions(-)\n> \n> diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\n> index cce2c89245..030311fce3 100644\n> --- a/Documentation/config/gpg.txt\n> +++ b/Documentation/config/gpg.txt\n> @@ -18,3 +18,14 @@ gpg.<format>.program::\n>  \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n>  \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n>  \tvalue for `gpg.x509.program` is \"gpgsm\".\n> +\n> +gpg.minTrustLevel::\n> +\tSpecifies a minimum trust level for signature verification.  The\n> +\tdefault value is \"undefined\".  Supported values (in increasing\n> +\torder of significance):\n> ++\n> +* `undefined`\n> +* `never`\n> +* `marginal`\n> +* `fully`\n> +* `ultimate`\n> diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\n> index 1a7212ce5a..f2e74241fe 100644\n> --- a/Documentation/pretty-formats.txt\n> +++ b/Documentation/pretty-formats.txt\n> @@ -215,7 +215,6 @@ endif::git-rev-list[]\n>  '%GG':: raw verification message from GPG for a signed commit\n>  '%G?':: show \"G\" for a good (valid) signature,\n>  \t\"B\" for a bad signature,\n> -\t\"U\" for a good signature with unknown validity,\n>  \t\"X\" for a good signature that has expired,\n>  \t\"Y\" for a good signature made by an expired key,\n>  \t\"R\" for a good signature made by a revoked key,\n> @@ -226,6 +225,7 @@ endif::git-rev-list[]\n>  '%GF':: show the fingerprint of the key used to sign a signed commit\n>  '%GP':: show the fingerprint of the primary key whose subkey was used\n>  \tto sign a signed commit\n> +'%GT':: show the trust level for the key used to sign a signed commit\n>  '%gD':: reflog selector, e.g., `refs/stash@{1}` or `refs/stash@{2\n>  \tminutes ago}`; the format follows the rules described for the\n>  \t`-g` option. The portion before the `@` is the refname as\n> diff --git a/commit.c b/commit.c\n> index 434ec030d6..f6d3ce4a6e 100644\n> --- a/commit.c\n> +++ b/commit.c\n> @@ -1140,17 +1140,18 @@ void verify_merge_signature(struct commit *commit, int verbosity)\n>  {\n>  \tchar hex[GIT_MAX_HEXSZ + 1];\n>  \tstruct signature_check signature_check;\n> +\tint ret;\n>  \tmemset(&signature_check, 0, sizeof(signature_check));\n>  \n> -\tcheck_commit_signature(commit, &signature_check);\n> +\tret = check_commit_signature(commit, &signature_check);\n>  \n>  \tfind_unique_abbrev_r(hex, &commit->object.oid, DEFAULT_ABBREV);\n>  \tswitch (signature_check.result) {\n>  \tcase 'G':\n> +\t\tif (ret)\n> +\t\t\tdie(_(\"Commit %s has an untrusted GPG signature, \"\n> +\t\t\t      \"allegedly by %s.\"), hex, signature_check.signer);\n>  \t\tbreak;\n> -\tcase 'U':\n> -\t\tdie(_(\"Commit %s has an untrusted GPG signature, \"\n> -\t\t      \"allegedly by %s.\"), hex, signature_check.signer);\n>  \tcase 'B':\n>  \t\tdie(_(\"Commit %s has a bad GPG signature \"\n>  \t\t      \"allegedly by %s.\"), hex, signature_check.signer);\n> diff --git a/gpg-interface.c b/gpg-interface.c\n> index 5134ce2780..f7b11480fb 100644\n> --- a/gpg-interface.c\n> +++ b/gpg-interface.c\n> @@ -7,6 +7,8 @@\n>  #include \"tempfile.h\"\n>  \n>  static char *configured_signing_key;\n> +static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n> +\n>  struct gpg_format {\n>  \tconst char *name;\n>  \tconst char *program;\n> @@ -85,6 +87,8 @@ void signature_check_clear(struct signature_check *sigc)\n>  #define GPG_STATUS_UID\t\t(1<<2)\n>  /* The status includes key fingerprints */\n>  #define GPG_STATUS_FINGERPRINT\t(1<<3)\n> +/* The status includes trust level */\n> +#define GPG_STATUS_TRUST_LEVEL\t(1<<4)\n>  \n>  /* Short-hand for standard exclusive *SIG status with keyid & UID */\n>  #define GPG_STATUS_STDSIG\t(GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID|GPG_STATUS_UID)\n> @@ -96,13 +100,23 @@ static struct {\n>  } sigcheck_gpg_status[] = {\n>  \t{ 'G', \"GOODSIG \", GPG_STATUS_STDSIG },\n>  \t{ 'B', \"BADSIG \", GPG_STATUS_STDSIG },\n> -\t{ 'U', \"TRUST_NEVER\", 0 },\n> -\t{ 'U', \"TRUST_UNDEFINED\", 0 },\n>  \t{ 'E', \"ERRSIG \", GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID },\n>  \t{ 'X', \"EXPSIG \", GPG_STATUS_STDSIG },\n>  \t{ 'Y', \"EXPKEYSIG \", GPG_STATUS_STDSIG },\n>  \t{ 'R', \"REVKEYSIG \", GPG_STATUS_STDSIG },\n>  \t{ 0, \"VALIDSIG \", GPG_STATUS_FINGERPRINT },\n> +\t{ 0, \"TRUST_\", GPG_STATUS_TRUST_LEVEL },\n> +};\n> +\n> +static struct {\n> +\tconst char *key;\n> +\tenum signature_trust_level value;\n> +} sigcheck_gpg_trust_level[] = {\n> +\t{ \"UNDEFINED\", TRUST_UNDEFINED },\n> +\t{ \"NEVER\", TRUST_NEVER },\n> +\t{ \"MARGINAL\", TRUST_MARGINAL },\n> +\t{ \"FULLY\", TRUST_FULLY },\n> +\t{ \"ULTIMATE\", TRUST_ULTIMATE },\n>  };\n>  \n>  static void replace_cstring(char **field, const char *line, const char *next)\n> @@ -115,10 +129,25 @@ static void replace_cstring(char **field, const char *line, const char *next)\n>  \t\t*field = NULL;\n>  }\n>  \n> +static int parse_gpg_trust_level(const char *level,\n> +\t\t\t\t enum signature_trust_level *res)\n> +{\n> +\tsize_t i;\n> +\n> +\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_trust_level); i++) {\n> +\t\tif (!strcmp(sigcheck_gpg_trust_level[i].key, level)) {\n> +\t\t\t*res = sigcheck_gpg_trust_level[i].value;\n> +\t\t\treturn 0;\n> +\t\t}\n> +\t}\n> +\treturn 1;\n> +}\n> +\n>  static void parse_gpg_output(struct signature_check *sigc)\n>  {\n>  \tconst char *buf = sigc->gpg_status;\n>  \tconst char *line, *next;\n> +\tchar *trust;\n>  \tint i, j;\n>  \tint seen_exclusive_status = 0;\n>  \n> @@ -136,9 +165,18 @@ static void parse_gpg_output(struct signature_check *sigc)\n>  \t\t/* Iterate over all search strings */\n>  \t\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {\n>  \t\t\tif (skip_prefix(line, sigcheck_gpg_status[i].check, &line)) {\n> +\t\t\t\t/*\n> +\t\t\t\t * GOODSIG, BADSIG etc. can occur only once for\n> +\t\t\t\t * each signature.  Therefore, if we had more\n> +\t\t\t\t * than one then we're dealing with multiple\n> +\t\t\t\t * signatures.  We don't support them\n> +\t\t\t\t * currently, and they're rather hard to\n> +\t\t\t\t * create, so something is likely fishy and we\n> +\t\t\t\t * should reject them altogether.\n> +\t\t\t\t */\n>  \t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_EXCLUSIVE) {\n>  \t\t\t\t\tif (seen_exclusive_status++)\n> -\t\t\t\t\t\tgoto found_duplicate_status;\n> +\t\t\t\t\t\tgoto error;\n>  \t\t\t\t}\n>  \n>  \t\t\t\tif (sigcheck_gpg_status[i].result)\n> @@ -154,6 +192,18 @@ static void parse_gpg_output(struct signature_check *sigc)\n>  \t\t\t\t\t\treplace_cstring(&sigc->signer, line, next);\n>  \t\t\t\t\t}\n>  \t\t\t\t}\n> +\n> +\t\t\t\t/* Do we have trust level? */\n> +\t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_TRUST_LEVEL) {\n> +\t\t\t\t\tnext = strchrnul(line, ' ');\n> +\t\t\t\t\ttrust = xmemdupz(line, next - line);\n> +\t\t\t\t\tif (parse_gpg_trust_level(trust, &sigc->trust_level)) {\n> +\t\t\t\t\t\tfree(trust);\n> +\t\t\t\t\t\tgoto error;\n> +\t\t\t\t\t}\n> +\t\t\t\t\tfree(trust);\n> +\t\t\t\t}\n> +\n>  \t\t\t\t/* Do we have fingerprint? */\n>  \t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_FINGERPRINT) {\n>  \t\t\t\t\tconst char *limit;\n> @@ -191,14 +241,7 @@ static void parse_gpg_output(struct signature_check *sigc)\n>  \t}\n>  \treturn;\n>  \n> -found_duplicate_status:\n> -\t/*\n> -\t * GOODSIG, BADSIG etc. can occur only once for each signature.\n> -\t * Therefore, if we had more than one then we're dealing with multiple\n> -\t * signatures.  We don't support them currently, and they're rather\n> -\t * hard to create, so something is likely fishy and we should reject\n> -\t * them altogether.\n> -\t */\n> +error:\n>  \tsigc->result = 'E';\n>  \t/* Clear partial data to avoid confusion */\n>  \tFREE_AND_NULL(sigc->primary_key_fingerprint);\n> @@ -264,6 +307,7 @@ int check_signature(const char *payload, size_t plen, const char *signature,\n>  \tint status;\n>  \n>  \tsigc->result = 'N';\n> +\tsigc->trust_level = -1;\n>  \n>  \tstatus = verify_signed_buffer(payload, plen, signature, slen,\n>  \t\t\t\t      &gpg_output, &gpg_status);\n> @@ -273,7 +317,8 @@ int check_signature(const char *payload, size_t plen, const char *signature,\n>  \tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n>  \tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n>  \tparse_gpg_output(sigc);\n> -\tstatus |= sigc->result != 'G' && sigc->result != 'U';\n> +\tstatus |= sigc->result != 'G';\n> +\tstatus |= sigc->trust_level < configured_min_trust_level;\n>  \n>   out:\n>  \tstrbuf_release(&gpg_status);\n> @@ -320,6 +365,8 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n>  {\n>  \tstruct gpg_format *fmt = NULL;\n>  \tchar *fmtname = NULL;\n> +\tchar *trust;\n> +\tint ret;\n>  \n>  \tif (!strcmp(var, \"user.signingkey\")) {\n>  \t\tif (!value)\n> @@ -339,6 +386,20 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n>  \t\treturn 0;\n>  \t}\n>  \n> +\tif (!strcmp(var, \"gpg.mintrustlevel\")) {\n> +\t\tif (!value)\n> +\t\t\treturn config_error_nonbool(var);\n> +\n> +\t\ttrust = xstrdup_toupper(value);\n> +\t\tret = parse_gpg_trust_level(trust, &configured_min_trust_level);\n> +\t\tfree(trust);\n> +\n> +\t\tif (ret)\n> +\t\t\treturn error(\"unsupported value for %s: %s\", var,\n> +\t\t\t\t     value);\n> +\t\treturn 0;\n> +\t}\n> +\n>  \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n>  \t\tfmtname = \"openpgp\";\n>  \n> diff --git a/gpg-interface.h b/gpg-interface.h\n> index 93cc3aff5c..f4e9b4f371 100644\n> --- a/gpg-interface.h\n> +++ b/gpg-interface.h\n> @@ -7,6 +7,14 @@ struct strbuf;\n>  #define GPG_VERIFY_RAW\t\t\t2\n>  #define GPG_VERIFY_OMIT_STATUS\t4\n>  \n> +enum signature_trust_level {\n> +\tTRUST_UNDEFINED,\n> +\tTRUST_NEVER,\n> +\tTRUST_MARGINAL,\n> +\tTRUST_FULLY,\n> +\tTRUST_ULTIMATE,\n> +};\n> +\n>  struct signature_check {\n>  \tchar *payload;\n>  \tchar *gpg_output;\n> @@ -16,7 +24,6 @@ struct signature_check {\n>  \t * possible \"result\":\n>  \t * 0 (not checked)\n>  \t * N (checked but no further result)\n> -\t * U (untrusted good)\n>  \t * G (good)\n>  \t * B (bad)\n>  \t */\n> @@ -25,6 +32,7 @@ struct signature_check {\n>  \tchar *key;\n>  \tchar *fingerprint;\n>  \tchar *primary_key_fingerprint;\n> +\tenum signature_trust_level trust_level;\n>  };\n>  \n>  void signature_check_clear(struct signature_check *sigc);\n> diff --git a/pretty.c b/pretty.c\n> index 305e903192..0afe05714d 100644\n> --- a/pretty.c\n> +++ b/pretty.c\n> @@ -1313,7 +1313,6 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n>  \t\t\tcase 'G':\n>  \t\t\tcase 'B':\n>  \t\t\tcase 'E':\n> -\t\t\tcase 'U':\n>  \t\t\tcase 'N':\n>  \t\t\tcase 'X':\n>  \t\t\tcase 'Y':\n> @@ -1337,6 +1336,25 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n>  \t\t\tif (c->signature_check.primary_key_fingerprint)\n>  \t\t\t\tstrbuf_addstr(sb, c->signature_check.primary_key_fingerprint);\n>  \t\t\tbreak;\n> +\t\tcase 'T':\n> +\t\t\tswitch (c->signature_check.trust_level) {\n> +\t\t\tcase TRUST_UNDEFINED:\n> +\t\t\t\tstrbuf_addstr(sb, \"undefined\");\n> +\t\t\t\tbreak;\n> +\t\t\tcase TRUST_NEVER:\n> +\t\t\t\tstrbuf_addstr(sb, \"never\");\n> +\t\t\t\tbreak;\n> +\t\t\tcase TRUST_MARGINAL:\n> +\t\t\t\tstrbuf_addstr(sb, \"marginal\");\n> +\t\t\t\tbreak;\n> +\t\t\tcase TRUST_FULLY:\n> +\t\t\t\tstrbuf_addstr(sb, \"fully\");\n> +\t\t\t\tbreak;\n> +\t\t\tcase TRUST_ULTIMATE:\n> +\t\t\t\tstrbuf_addstr(sb, \"ultimate\");\n> +\t\t\t\tbreak;\n> +\t\t\t}\n> +\t\t\tbreak;\n>  \t\tdefault:\n>  \t\t\treturn 0;\n>  \t\t}\n> diff --git a/t/t5573-pull-verify-signatures.sh b/t/t5573-pull-verify-signatures.sh\n> index 3e9876e197..d7d46d9382 100755\n> --- a/t/t5573-pull-verify-signatures.sh\n> +++ b/t/t5573-pull-verify-signatures.sh\n> @@ -56,6 +56,13 @@ test_expect_success GPG 'pull commit with bad signature with --verify-signatures\n>  \n>  test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures' '\n>  \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n> +\tgit pull --ff-only --verify-signatures untrusted >pulloutput &&\n> +\ttest_i18ngrep \"has a good GPG signature\" pulloutput\n> +'\n> +\n> +test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel' '\n> +\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n> +\ttest_config gpg.minTrustLevel marginal &&\n>  \ttest_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&\n>  \ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n>  '\n> diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\n> index 682b23a068..8ab29e80ce 100755\n> --- a/t/t7510-signed-commit.sh\n> +++ b/t/t7510-signed-commit.sh\n> @@ -109,6 +109,21 @@ test_expect_success GPG 'verify-commit exits success on untrusted signature' '\n>  \tgrep \"not certified\" actual\n>  '\n>  \n> +test_expect_success GPG 'verify-commit exits success with matching minTrustLevel' '\n> +\ttest_config gpg.minTrustLevel ultimate &&\n> +\tgit verify-commit sixth-signed\n> +'\n> +\n> +test_expect_success GPG 'verify-commit exits success with low minTrustLevel' '\n> +\ttest_config gpg.minTrustLevel fully &&\n> +\tgit verify-commit sixth-signed\n> +'\n> +\n> +test_expect_success GPG 'verify-commit exits failure with high minTrustLevel' '\n> +\ttest_config gpg.minTrustLevel ultimate &&\n> +\ttest_must_fail git verify-commit eighth-signed-alt\n> +'\n> +\n>  test_expect_success GPG 'verify signatures with --raw' '\n>  \t(\n>  \t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n> @@ -209,13 +224,13 @@ test_expect_success GPG 'show bad signature with custom format' '\n>  \n>  test_expect_success GPG 'show untrusted signature with custom format' '\n>  \tcat >expect <<-\\EOF &&\n> -\tU\n> +\tundefined\n>  \t65A0EEA02E30CAD7\n>  \tEris Discordia <discord@example.net>\n>  \tF8364A59E07FFE9F4D63005A65A0EEA02E30CAD7\n>  \tD4BE22311AD3131E5EDA29A461092E85B7227189\n>  \tEOF\n> -\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n> +\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n>  \ttest_cmp expect actual\n>  '\n>  \n> diff --git a/t/t7612-merge-verify-signatures.sh b/t/t7612-merge-verify-signatures.sh\n> index d99218a725..5a8e9afd8e 100755\n> --- a/t/t7612-merge-verify-signatures.sh\n> +++ b/t/t7612-merge-verify-signatures.sh\n> @@ -62,6 +62,13 @@ test_expect_success GPG 'merge commit with bad signature with merge.verifySignat\n>  \n>  test_expect_success GPG 'merge commit with untrusted signature with verification' '\n>  \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n> +\tgit merge --ff-only --verify-signatures side-untrusted >mergeoutput &&\n> +\ttest_i18ngrep \"has a good GPG signature\" mergeoutput\n> +'\n> +\n> +test_expect_success GPG 'merge commit with untrusted signature with verification and minTrustLevel' '\n> +\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n> +\ttest_config gpg.minTrustLevel marginal &&\n>  \ttest_must_fail git merge --ff-only --verify-signatures side-untrusted 2>mergeerror &&\n>  \ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n>  '\n> @@ -69,6 +76,14 @@ test_expect_success GPG 'merge commit with untrusted signature with verification\n>  test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true' '\n>  \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n>  \ttest_config merge.verifySignatures true &&\n> +\tgit merge --ff-only side-untrusted >mergeoutput &&\n> +\ttest_i18ngrep \"has a good GPG signature\" mergeoutput\n> +'\n> +\n> +test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true and minTrustLevel' '\n> +\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n> +\ttest_config merge.verifySignatures true &&\n> +\ttest_config gpg.minTrustLevel marginal &&\n>  \ttest_must_fail git merge --ff-only side-untrusted 2>mergeerror &&\n>  \ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n>  '\n> -- \n> 2.24.1.485.gad05a3d8e5.dirty\n> \n"},{"id":"388715","messageId":"87y2v54i9v.hji@dyntopia.com","threadId":"52467","inReplyTo":"20191220225746.GF8609@szeder.dev","subject":"Re: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-21T18:59:08Z","receivedAt":"2019-12-21T19:14:42Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"On Fri, Dec 20 2019, SZEDER Gábor wrote:\n> On Mon, Dec 16, 2019 at 03:32:04PM +0000, Hans Jerry Illikainen wrote:\n>> This patch introduces a new configuration option: gpg.minTrustLevel.  It\n>> consolidates trust-level verification to gpg-interface.c and adds a new\n>> `trust_level` member to the signature_check structure.\n>\n> This patch causes several test failures:\n>\n>   https://travis-ci.org/git/git/jobs/627909430#L2259\n>\n> I see you've already posted an updated version, so I tried it locally,\n> and the same test scripts fail with the updated version as well.\n\nSorry for that!  I'm preparing a v2 (tested with both gpg1 and gpg2).\n\n> I noticed that only Linux CI jobs failed, while the OSX jobs\n> succeeded.  Our Linux CI jobs (and my box) are based on Ubuntu 16.04,\n> and thus use GnuPG v1.4, while the OSX jobs use v2.something.  Not\n> sure that the version difference is connected to the test failures,\n> but I figured it's worth pointing out.\n\nYour observation about the different GPG versions was spot on; thanks!\nThat explains why all tests pass on my machine as well as on a personal\nCI setup for my git contributions (both using gpg2).\n\nThe issue was that the search for the end of a trust level to parse\nrelied on the TRUST_ line being space-separated.  But that is not always\nthe case for gpg1 (only the lowest-two trust levels contain a space\nfollowed by additional information in gpg1).\n\n-- \nhji\n"},{"id":"388773","messageId":"20191222003123.10555-1-hji@dyntopia.com","threadId":"52467","inReplyTo":"20191219000140.9056-1-hji@dyntopia.com","subject":"[PATCH v2 0/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-22T00:31:22Z","receivedAt":"2019-12-22T00:31:32Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"Previously, signature verification for merge and pull operations checked\nif the key had a trust-level of either TRUST_NEVER or TRUST_UNDEFINED in\nverify_merge_signature().  If that was the case, the process die()d.\n\nThe other code paths that did signature verification relied entirely on\nthe return code from check_commit_signature().  And signatures made with\na good key, irregardless of its trust level, was considered valid by\ncheck_commit_signature().\n\nThis difference in behavior might induce users to erroneously assume\nthat the trust level of a key in their keyring is always considered by\nGit, even for operations where it is not (e.g. during a verify-commit or\nverify-tag).\n\nThe way it worked was by gpg-interface.c storing the result from the\nkey/signature status *and* the lowest-two trust levels in the `result`\nmember of the signature_check structure (the last of these status lines\nthat were encountered got written to `result`).  These are documented in\nGPG under the subsection `General status codes` and `Key related`,\nrespectively [1].\n\nThe GPG documentation says the following on the TRUST_ status codes [1]:\n\n    \"\"\"\n    These are several similar status codes:\n\n    - TRUST_UNDEFINED <error_token>\n    - TRUST_NEVER     <error_token>\n    - TRUST_MARGINAL  [0  [<validation_model>]]\n    - TRUST_FULLY     [0  [<validation_model>]]\n    - TRUST_ULTIMATE  [0  [<validation_model>]]\n\n    For good signatures one of these status lines are emitted to\n    indicate the validity of the key used to create the signature.\n    The error token values are currently only emitted by gpgsm.\n    \"\"\"\n\nMy interpretation is that the trust level is conceptionally different\nfrom the validity of the key and/or signature.  That seems to also have\nbeen the assumption of the old code in check_signature() where a result\nof 'G' (as in GOODSIG) and 'U' (as in TRUST_NEVER or TRUST_UNDEFINED)\nwere both considered a success.\n\nThe two cases where a result of 'U' had special meaning were in\nverify_merge_signature() (where this caused git to die()) and in\nformat_commit_one() (where it affected the output of the %G? format\nspecifier).\n\nI think it makes sense to refactor the processing of TRUST_ status lines\nsuch that users can configure a minimum trust level that is enforced\nglobally, rather than have individual parts of git (e.g. merge) do it\nthemselves (except for a grace period with backward compatibility).\n\nI also think it makes sense to not store the trust level in the same\nstruct member as the key/signature status.  While the presence of a\nTRUST_ status code does imply that the signature is good (see the first\nparagraph in the included snippet above), as far as I can tell, the\norder of the status lines from GPG isn't well-defined; thus it would\nseem plausible that the trust level could be overwritten with the\nkey/signature status if they were stored in the same member of the\nsignature_check structure.\n\nThis patch introduces a new configuration option: gpg.minTrustLevel.  It\nconsolidates trust-level verification to gpg-interface.c and adds a new\n`trust_level` member to the signature_check structure.\n\nBackward-compatibility is maintained by introducing a special case in\nverify_merge_signature() such that if no user-configurable\ngpg.minTrustLevel is set, then the old behavior of rejecting\nTRUST_UNDEFINED and TRUST_NEVER is enforced.  If, on the other hand,\ngpg.minTrustLevel is set, then that value overrides the old behavior.\n\nSimilarly, the %G? format specifier will continue show 'U' for\nsignatures made with a key that has a trust level of TRUST_UNDEFINED or\nTRUST_NEVER, even though the 'U' character no longer exist in the\n`result` member of the signature_check structure.  A new format\nspecifier, %GT, is also introduced for users that want to show all\npossible trust levels for a signature.\n\nAnother approach would have been to simply drop the trust-level\nrequirement in verify_merge_signature().  This would also have made the\nbehavior consistent with other parts of git that perform signature\nverification.  However, requiring a minimum trust level for signing keys\ndoes seem to have a real-world use-case.  For example, the build system\nused by the Qubes OS project currently parses the raw output from\nverify-tag in order to assert a minimum trust level for keys used to\nsign git tags [2].\n\n\nChanges since v0:\n* Added backward-compatibility with the old behavior of\n  verify_merge_signature().  The old behavior is overridden if a user\n  has configured gpg.minTrustLevel.  My approach is kind of ugly because\n  now all users of verify_merge_signature() has to be aware of\n  gpg.minTrustLevel to know if it should override the default behavior.\n  An alternative might be to have a configurable per-operation trust\n  level (e.g. merge.minTrustLevel), but I'm not sure how sensible that\n  is either.\n* Added backward-compatiblity with the old behavior of %G?.\n\nChanges since v1:\n* Fixed compatibility with gpg1 in parse_gpg_output().  One significant\n  difference between gpg1 and gpg2 is that the trust levels above\n  TRUST_NEVER are written without any additional space-separated\n  information in gpg1 [3].  This broke the logic in the previous\n  iterations, because the end of the TRUST_ string were searched for by\n  looking for a space character.  Now a new-line is used as a fallback.\n\n\n[1] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/doc/DETAILS;h=bd00006e933ac56719b1edd2478ecd79273eae72;hb=refs/heads/master\n[2] https://github.com/QubesOS/qubes-builder/blob/9674c1991deef45b1a1b1c71fddfab14ba50dccf/scripts/verify-git-tag#L43\n\nHans Jerry Illikainen (1):\n  gpg-interface: add minTrustLevel as a configuration option\n\n Documentation/config/gpg.txt       | 15 +++++\n Documentation/pretty-formats.txt   |  1 +\n builtin/merge.c                    |  9 ++-\n builtin/pull.c                     | 13 ++++-\n commit.c                           | 12 ++--\n commit.h                           | 12 +++-\n gpg-interface.c                    | 93 ++++++++++++++++++++++++++----\n gpg-interface.h                    | 10 +++-\n pretty.c                           | 30 +++++++++-\n t/t5573-pull-verify-signatures.sh  | 64 ++++++++++++++++++++\n t/t7030-verify-tag.sh              | 24 ++++++++\n t/t7510-signed-commit.sh           | 39 +++++++++++++\n t/t7612-merge-verify-signatures.sh | 22 +++++++\n 13 files changed, 321 insertions(+), 23 deletions(-)\n\n--\n2.15.1.8615.g16a7d4acf3\n"},{"id":"388774","messageId":"20191222003123.10555-2-hji@dyntopia.com","threadId":"52467","inReplyTo":"20191222003123.10555-1-hji@dyntopia.com","subject":"[PATCH v2 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-22T00:31:23Z","receivedAt":"2019-12-22T00:31:45Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"Previously, signature verification for merge and pull operations checked\nif the key had a trust-level of either TRUST_NEVER or TRUST_UNDEFINED in\nverify_merge_signature().  If that was the case, the process die()d.\n\nThe other code paths that did signature verification relied entirely on\nthe return code from check_commit_signature().  And signatures made with\na good key, irregardless of its trust level, was considered valid by\ncheck_commit_signature().\n\nThis difference in behavior might induce users to erroneously assume\nthat the trust level of a key in their keyring is always considered by\nGit, even for operations where it is not (e.g. during a verify-commit or\nverify-tag).\n\nThe way it worked was by gpg-interface.c storing the result from the\nkey/signature status *and* the lowest-two trust levels in the `result`\nmember of the signature_check structure (the last of these status lines\nthat were encountered got written to `result`).  These are documented in\nGPG under the subsection `General status codes` and `Key related`,\nrespectively [1].\n\nThe GPG documentation says the following on the TRUST_ status codes [1]:\n\n    \"\"\"\n    These are several similar status codes:\n\n    - TRUST_UNDEFINED <error_token>\n    - TRUST_NEVER     <error_token>\n    - TRUST_MARGINAL  [0  [<validation_model>]]\n    - TRUST_FULLY     [0  [<validation_model>]]\n    - TRUST_ULTIMATE  [0  [<validation_model>]]\n\n    For good signatures one of these status lines are emitted to\n    indicate the validity of the key used to create the signature.\n    The error token values are currently only emitted by gpgsm.\n    \"\"\"\n\nMy interpretation is that the trust level is conceptionally different\nfrom the validity of the key and/or signature.  That seems to also have\nbeen the assumption of the old code in check_signature() where a result\nof 'G' (as in GOODSIG) and 'U' (as in TRUST_NEVER or TRUST_UNDEFINED)\nwere both considered a success.\n\nThe two cases where a result of 'U' had special meaning were in\nverify_merge_signature() (where this caused git to die()) and in\nformat_commit_one() (where it affected the output of the %G? format\nspecifier).\n\nI think it makes sense to refactor the processing of TRUST_ status lines\nsuch that users can configure a minimum trust level that is enforced\nglobally, rather than have individual parts of git (e.g. merge) do it\nthemselves (except for a grace period with backward compatibility).\n\nI also think it makes sense to not store the trust level in the same\nstruct member as the key/signature status.  While the presence of a\nTRUST_ status code does imply that the signature is good (see the first\nparagraph in the included snippet above), as far as I can tell, the\norder of the status lines from GPG isn't well-defined; thus it would\nseem plausible that the trust level could be overwritten with the\nkey/signature status if they were stored in the same member of the\nsignature_check structure.\n\nThis patch introduces a new configuration option: gpg.minTrustLevel.  It\nconsolidates trust-level verification to gpg-interface.c and adds a new\n`trust_level` member to the signature_check structure.\n\nBackward-compatibility is maintained by introducing a special case in\nverify_merge_signature() such that if no user-configurable\ngpg.minTrustLevel is set, then the old behavior of rejecting\nTRUST_UNDEFINED and TRUST_NEVER is enforced.  If, on the other hand,\ngpg.minTrustLevel is set, then that value overrides the old behavior.\n\nSimilarly, the %G? format specifier will continue show 'U' for\nsignatures made with a key that has a trust level of TRUST_UNDEFINED or\nTRUST_NEVER, even though the 'U' character no longer exist in the\n`result` member of the signature_check structure.  A new format\nspecifier, %GT, is also introduced for users that want to show all\npossible trust levels for a signature.\n\nAnother approach would have been to simply drop the trust-level\nrequirement in verify_merge_signature().  This would also have made the\nbehavior consistent with other parts of git that perform signature\nverification.  However, requiring a minimum trust level for signing keys\ndoes seem to have a real-world use-case.  For example, the build system\nused by the Qubes OS project currently parses the raw output from\nverify-tag in order to assert a minimum trust level for keys used to\nsign git tags [2].\n\n[1] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/doc/DETAILS;h=bd00006e933ac56719b1edd2478ecd79273eae72;hb=refs/heads/master\n[2] https://github.com/QubesOS/qubes-builder/blob/9674c1991deef45b1a1b1c71fddfab14ba50dccf/scripts/verify-git-tag#L43\n\nSigned-off-by: Hans Jerry Illikainen <hji@dyntopia.com>\n---\n Documentation/config/gpg.txt       | 15 +++++\n Documentation/pretty-formats.txt   |  1 +\n builtin/merge.c                    |  9 ++-\n builtin/pull.c                     | 13 ++++-\n commit.c                           | 12 ++--\n commit.h                           | 12 +++-\n gpg-interface.c                    | 93 ++++++++++++++++++++++++++----\n gpg-interface.h                    | 10 +++-\n pretty.c                           | 30 +++++++++-\n t/t5573-pull-verify-signatures.sh  | 64 ++++++++++++++++++++\n t/t7030-verify-tag.sh              | 24 ++++++++\n t/t7510-signed-commit.sh           | 39 +++++++++++++\n t/t7612-merge-verify-signatures.sh | 22 +++++++\n 13 files changed, 321 insertions(+), 23 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex cce2c89245..d94025cb36 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -18,3 +18,18 @@ gpg.<format>.program::\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n \tvalue for `gpg.x509.program` is \"gpgsm\".\n+\n+gpg.minTrustLevel::\n+\tSpecifies a minimum trust level for signature verification.  If\n+\tthis option is unset, then signature verification for merge\n+\toperations require a key with at least `marginal` trust.  Other\n+\toperations that perform signature verification require a key\n+\twith at least `undefined` trust.  Setting this option overrides\n+\tthe required trust-level for all operations.  Supported values,\n+\tin increasing order of significance:\n++\n+* `undefined`\n+* `never`\n+* `marginal`\n+* `fully`\n+* `ultimate`\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 1a7212ce5a..a4b6f49186 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -226,6 +226,7 @@ endif::git-rev-list[]\n '%GF':: show the fingerprint of the key used to sign a signed commit\n '%GP':: show the fingerprint of the primary key whose subkey was used\n \tto sign a signed commit\n+'%GT':: show the trust level for the key used to sign a signed commit\n '%gD':: reflog selector, e.g., `refs/stash@{1}` or `refs/stash@{2\n \tminutes ago}`; the format follows the rules described for the\n \t`-g` option. The portion before the `@` is the refname as\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 062e911441..d127d2225f 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -62,6 +62,7 @@ static int show_diffstat = 1, shortlog_len = -1, squash;\n static int option_commit = -1;\n static int option_edit = -1;\n static int allow_trivial = 1, have_message, verify_signatures;\n+static int check_trust_level = 1;\n static int overwrite_ignore = 1;\n static struct strbuf merge_msg = STRBUF_INIT;\n static struct strategy **use_strategies;\n@@ -631,6 +632,8 @@ static int git_merge_config(const char *k, const char *v, void *cb)\n \t} else if (!strcmp(k, \"commit.gpgsign\")) {\n \t\tsign_commit = git_config_bool(k, v) ? \"\" : NULL;\n \t\treturn 0;\n+\t} else if (!strcmp(k, \"gpg.mintrustlevel\")) {\n+\t\tcheck_trust_level = 0;\n \t}\n \n \tstatus = fmt_merge_msg_config(k, v, cb);\n@@ -1397,7 +1400,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \t\t\tdie(_(\"Can merge only exactly one commit into empty head\"));\n \n \t\tif (verify_signatures)\n-\t\t\tverify_merge_signature(remoteheads->item, verbosity);\n+\t\t\tverify_merge_signature(remoteheads->item, verbosity,\n+\t\t\t\t\t       check_trust_level);\n \n \t\tremote_head_oid = &remoteheads->item->object.oid;\n \t\tread_empty(remote_head_oid, 0);\n@@ -1420,7 +1424,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \n \tif (verify_signatures) {\n \t\tfor (p = remoteheads; p; p = p->next) {\n-\t\t\tverify_merge_signature(p->item, verbosity);\n+\t\t\tverify_merge_signature(p->item, verbosity,\n+\t\t\t\t\t       check_trust_level);\n \t\t}\n \t}\n \ndiff --git a/builtin/pull.c b/builtin/pull.c\nindex d25ff13a60..d4e3e77c8e 100644\n--- a/builtin/pull.c\n+++ b/builtin/pull.c\n@@ -107,6 +107,7 @@ static char *opt_ff;\n static char *opt_verify_signatures;\n static int opt_autostash = -1;\n static int config_autostash;\n+static int check_trust_level = 1;\n static struct argv_array opt_strategies = ARGV_ARRAY_INIT;\n static struct argv_array opt_strategy_opts = ARGV_ARRAY_INIT;\n static char *opt_gpg_sign;\n@@ -355,6 +356,8 @@ static enum rebase_type config_get_rebase(void)\n  */\n static int git_pull_config(const char *var, const char *value, void *cb)\n {\n+\tint status;\n+\n \tif (!strcmp(var, \"rebase.autostash\")) {\n \t\tconfig_autostash = git_config_bool(var, value);\n \t\treturn 0;\n@@ -362,7 +365,14 @@ static int git_pull_config(const char *var, const char *value, void *cb)\n \t\trecurse_submodules = git_config_bool(var, value) ?\n \t\t\tRECURSE_SUBMODULES_ON : RECURSE_SUBMODULES_OFF;\n \t\treturn 0;\n+\t} else if (!strcmp(var, \"gpg.mintrustlevel\")) {\n+\t\tcheck_trust_level = 0;\n \t}\n+\n+\tstatus = git_gpg_config(var, value, cb);\n+\tif (status)\n+\t\treturn status;\n+\n \treturn git_default_config(var, value, cb);\n }\n \n@@ -587,7 +597,8 @@ static int pull_into_void(const struct object_id *merge_head,\n \t\t\tdie(_(\"unable to access commit %s\"),\n \t\t\t    oid_to_hex(merge_head));\n \n-\t\tverify_merge_signature(commit, opt_verbosity);\n+\t\tverify_merge_signature(commit, opt_verbosity,\n+\t\t\t\t       check_trust_level);\n \t}\n \n \t/*\ndiff --git a/commit.c b/commit.c\nindex 434ec030d6..3f91d3efc5 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1136,21 +1136,23 @@ int check_commit_signature(const struct commit *commit, struct signature_check *\n \treturn ret;\n }\n \n-void verify_merge_signature(struct commit *commit, int verbosity)\n+void verify_merge_signature(struct commit *commit, int verbosity,\n+\t\t\t    int check_trust)\n {\n \tchar hex[GIT_MAX_HEXSZ + 1];\n \tstruct signature_check signature_check;\n+\tint ret;\n \tmemset(&signature_check, 0, sizeof(signature_check));\n \n-\tcheck_commit_signature(commit, &signature_check);\n+\tret = check_commit_signature(commit, &signature_check);\n \n \tfind_unique_abbrev_r(hex, &commit->object.oid, DEFAULT_ABBREV);\n \tswitch (signature_check.result) {\n \tcase 'G':\n+\t\tif (ret || (check_trust && signature_check.trust_level < TRUST_MARGINAL))\n+\t\t\tdie(_(\"Commit %s has an untrusted GPG signature, \"\n+\t\t\t      \"allegedly by %s.\"), hex, signature_check.signer);\n \t\tbreak;\n-\tcase 'U':\n-\t\tdie(_(\"Commit %s has an untrusted GPG signature, \"\n-\t\t      \"allegedly by %s.\"), hex, signature_check.signer);\n \tcase 'B':\n \t\tdie(_(\"Commit %s has a bad GPG signature \"\n \t\t      \"allegedly by %s.\"), hex, signature_check.signer);\ndiff --git a/commit.h b/commit.h\nindex 221cdaa34b..008a0fa4a0 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -383,8 +383,18 @@ int compare_commits_by_author_date(const void *a_, const void *b_, void *unused)\n  * Verify a single commit with check_commit_signature() and die() if it is not\n  * a good signature. This isn't really suitable for general use, but is a\n  * helper to implement consistent logic for pull/merge --verify-signatures.\n+ *\n+ * The check_trust parameter is meant for backward-compatibility.  The GPG\n+ * interface verifies key trust with a default trust level that is below the\n+ * default trust level for merge operations.  Its value should be non-zero if\n+ * the user hasn't set a minimum trust level explicitly in their configuration.\n+ *\n+ * If the user has set a minimum trust level, then that value should be obeyed\n+ * and check_trust should be zero, even if the configured trust level is below\n+ * the default trust level for merges.\n  */\n-void verify_merge_signature(struct commit *commit, int verbose);\n+void verify_merge_signature(struct commit *commit, int verbose,\n+\t\t\t    int check_trust);\n \n int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused);\n int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 5134ce2780..b7cd8bc073 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -7,6 +7,8 @@\n #include \"tempfile.h\"\n \n static char *configured_signing_key;\n+static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n+\n struct gpg_format {\n \tconst char *name;\n \tconst char *program;\n@@ -85,6 +87,8 @@ void signature_check_clear(struct signature_check *sigc)\n #define GPG_STATUS_UID\t\t(1<<2)\n /* The status includes key fingerprints */\n #define GPG_STATUS_FINGERPRINT\t(1<<3)\n+/* The status includes trust level */\n+#define GPG_STATUS_TRUST_LEVEL\t(1<<4)\n \n /* Short-hand for standard exclusive *SIG status with keyid & UID */\n #define GPG_STATUS_STDSIG\t(GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID|GPG_STATUS_UID)\n@@ -96,13 +100,23 @@ static struct {\n } sigcheck_gpg_status[] = {\n \t{ 'G', \"GOODSIG \", GPG_STATUS_STDSIG },\n \t{ 'B', \"BADSIG \", GPG_STATUS_STDSIG },\n-\t{ 'U', \"TRUST_NEVER\", 0 },\n-\t{ 'U', \"TRUST_UNDEFINED\", 0 },\n \t{ 'E', \"ERRSIG \", GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID },\n \t{ 'X', \"EXPSIG \", GPG_STATUS_STDSIG },\n \t{ 'Y', \"EXPKEYSIG \", GPG_STATUS_STDSIG },\n \t{ 'R', \"REVKEYSIG \", GPG_STATUS_STDSIG },\n \t{ 0, \"VALIDSIG \", GPG_STATUS_FINGERPRINT },\n+\t{ 0, \"TRUST_\", GPG_STATUS_TRUST_LEVEL },\n+};\n+\n+static struct {\n+\tconst char *key;\n+\tenum signature_trust_level value;\n+} sigcheck_gpg_trust_level[] = {\n+\t{ \"UNDEFINED\", TRUST_UNDEFINED },\n+\t{ \"NEVER\", TRUST_NEVER },\n+\t{ \"MARGINAL\", TRUST_MARGINAL },\n+\t{ \"FULLY\", TRUST_FULLY },\n+\t{ \"ULTIMATE\", TRUST_ULTIMATE },\n };\n \n static void replace_cstring(char **field, const char *line, const char *next)\n@@ -115,10 +129,25 @@ static void replace_cstring(char **field, const char *line, const char *next)\n \t\t*field = NULL;\n }\n \n+static int parse_gpg_trust_level(const char *level,\n+\t\t\t\t enum signature_trust_level *res)\n+{\n+\tsize_t i;\n+\n+\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_trust_level); i++) {\n+\t\tif (!strcmp(sigcheck_gpg_trust_level[i].key, level)) {\n+\t\t\t*res = sigcheck_gpg_trust_level[i].value;\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\treturn 1;\n+}\n+\n static void parse_gpg_output(struct signature_check *sigc)\n {\n \tconst char *buf = sigc->gpg_status;\n \tconst char *line, *next;\n+\tchar *trust;\n \tint i, j;\n \tint seen_exclusive_status = 0;\n \n@@ -136,9 +165,18 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t\t/* Iterate over all search strings */\n \t\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {\n \t\t\tif (skip_prefix(line, sigcheck_gpg_status[i].check, &line)) {\n+\t\t\t\t/*\n+\t\t\t\t * GOODSIG, BADSIG etc. can occur only once for\n+\t\t\t\t * each signature.  Therefore, if we had more\n+\t\t\t\t * than one then we're dealing with multiple\n+\t\t\t\t * signatures.  We don't support them\n+\t\t\t\t * currently, and they're rather hard to\n+\t\t\t\t * create, so something is likely fishy and we\n+\t\t\t\t * should reject them altogether.\n+\t\t\t\t */\n \t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_EXCLUSIVE) {\n \t\t\t\t\tif (seen_exclusive_status++)\n-\t\t\t\t\t\tgoto found_duplicate_status;\n+\t\t\t\t\t\tgoto error;\n \t\t\t\t}\n \n \t\t\t\tif (sigcheck_gpg_status[i].result)\n@@ -154,6 +192,26 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t\t\t\t\t\treplace_cstring(&sigc->signer, line, next);\n \t\t\t\t\t}\n \t\t\t\t}\n+\n+\t\t\t\t/* Do we have trust level? */\n+\t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_TRUST_LEVEL) {\n+\t\t\t\t\t/*\n+\t\t\t\t\t * GPG v1 and v2 differs in how the\n+\t\t\t\t\t * TRUST_ lines are written.  Some\n+\t\t\t\t\t * trust lines contain no additional\n+\t\t\t\t\t * space-separated information for v1.\n+\t\t\t\t\t */\n+\t\t\t\t\tnext = strchr(line, ' ');\n+\t\t\t\t\tif (!next)\n+\t\t\t\t\t\tnext = strchrnul(line, '\\n');\n+\t\t\t\t\ttrust = xmemdupz(line, next - line);\n+\t\t\t\t\tif (parse_gpg_trust_level(trust, &sigc->trust_level)) {\n+\t\t\t\t\t\tfree(trust);\n+\t\t\t\t\t\tgoto error;\n+\t\t\t\t\t}\n+\t\t\t\t\tfree(trust);\n+\t\t\t\t}\n+\n \t\t\t\t/* Do we have fingerprint? */\n \t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_FINGERPRINT) {\n \t\t\t\t\tconst char *limit;\n@@ -191,14 +249,7 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t}\n \treturn;\n \n-found_duplicate_status:\n-\t/*\n-\t * GOODSIG, BADSIG etc. can occur only once for each signature.\n-\t * Therefore, if we had more than one then we're dealing with multiple\n-\t * signatures.  We don't support them currently, and they're rather\n-\t * hard to create, so something is likely fishy and we should reject\n-\t * them altogether.\n-\t */\n+error:\n \tsigc->result = 'E';\n \t/* Clear partial data to avoid confusion */\n \tFREE_AND_NULL(sigc->primary_key_fingerprint);\n@@ -264,6 +315,7 @@ int check_signature(const char *payload, size_t plen, const char *signature,\n \tint status;\n \n \tsigc->result = 'N';\n+\tsigc->trust_level = -1;\n \n \tstatus = verify_signed_buffer(payload, plen, signature, slen,\n \t\t\t\t      &gpg_output, &gpg_status);\n@@ -273,7 +325,8 @@ int check_signature(const char *payload, size_t plen, const char *signature,\n \tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n \tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n \tparse_gpg_output(sigc);\n-\tstatus |= sigc->result != 'G' && sigc->result != 'U';\n+\tstatus |= sigc->result != 'G';\n+\tstatus |= sigc->trust_level < configured_min_trust_level;\n \n  out:\n \tstrbuf_release(&gpg_status);\n@@ -320,6 +373,8 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n {\n \tstruct gpg_format *fmt = NULL;\n \tchar *fmtname = NULL;\n+\tchar *trust;\n+\tint ret;\n \n \tif (!strcmp(var, \"user.signingkey\")) {\n \t\tif (!value)\n@@ -339,6 +394,20 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.mintrustlevel\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\n+\t\ttrust = xstrdup_toupper(value);\n+\t\tret = parse_gpg_trust_level(trust, &configured_min_trust_level);\n+\t\tfree(trust);\n+\n+\t\tif (ret)\n+\t\t\treturn error(\"unsupported value for %s: %s\", var,\n+\t\t\t\t     value);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n \t\tfmtname = \"openpgp\";\n \ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 93cc3aff5c..f4e9b4f371 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -7,6 +7,14 @@ struct strbuf;\n #define GPG_VERIFY_RAW\t\t\t2\n #define GPG_VERIFY_OMIT_STATUS\t4\n \n+enum signature_trust_level {\n+\tTRUST_UNDEFINED,\n+\tTRUST_NEVER,\n+\tTRUST_MARGINAL,\n+\tTRUST_FULLY,\n+\tTRUST_ULTIMATE,\n+};\n+\n struct signature_check {\n \tchar *payload;\n \tchar *gpg_output;\n@@ -16,7 +24,6 @@ struct signature_check {\n \t * possible \"result\":\n \t * 0 (not checked)\n \t * N (checked but no further result)\n-\t * U (untrusted good)\n \t * G (good)\n \t * B (bad)\n \t */\n@@ -25,6 +32,7 @@ struct signature_check {\n \tchar *key;\n \tchar *fingerprint;\n \tchar *primary_key_fingerprint;\n+\tenum signature_trust_level trust_level;\n };\n \n void signature_check_clear(struct signature_check *sigc);\ndiff --git a/pretty.c b/pretty.c\nindex 305e903192..f5fbbc5ffb 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1311,9 +1311,18 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\tcase '?':\n \t\t\tswitch (c->signature_check.result) {\n \t\t\tcase 'G':\n+\t\t\t\tswitch (c->signature_check.trust_level) {\n+\t\t\t\tcase TRUST_UNDEFINED:\n+\t\t\t\tcase TRUST_NEVER:\n+\t\t\t\t\tstrbuf_addch(sb, 'U');\n+\t\t\t\t\tbreak;\n+\t\t\t\tdefault:\n+\t\t\t\t\tstrbuf_addch(sb, 'G');\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t\tbreak;\n \t\t\tcase 'B':\n \t\t\tcase 'E':\n-\t\t\tcase 'U':\n \t\t\tcase 'N':\n \t\t\tcase 'X':\n \t\t\tcase 'Y':\n@@ -1337,6 +1346,25 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tif (c->signature_check.primary_key_fingerprint)\n \t\t\t\tstrbuf_addstr(sb, c->signature_check.primary_key_fingerprint);\n \t\t\tbreak;\n+\t\tcase 'T':\n+\t\t\tswitch (c->signature_check.trust_level) {\n+\t\t\tcase TRUST_UNDEFINED:\n+\t\t\t\tstrbuf_addstr(sb, \"undefined\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_NEVER:\n+\t\t\t\tstrbuf_addstr(sb, \"never\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_MARGINAL:\n+\t\t\t\tstrbuf_addstr(sb, \"marginal\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_FULLY:\n+\t\t\t\tstrbuf_addstr(sb, \"fully\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_ULTIMATE:\n+\t\t\t\tstrbuf_addstr(sb, \"ultimate\");\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tbreak;\n \t\tdefault:\n \t\t\treturn 0;\n \t\t}\ndiff --git a/t/t5573-pull-verify-signatures.sh b/t/t5573-pull-verify-signatures.sh\nindex 3e9876e197..a53dd8550d 100755\n--- a/t/t5573-pull-verify-signatures.sh\n+++ b/t/t5573-pull-verify-signatures.sh\n@@ -60,6 +60,27 @@ test_expect_success GPG 'pull commit with untrusted signature with --verify-sign\n \ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n '\n \n+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=ultimate' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=marginal' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\ttest_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=undefined' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel undefined &&\n+\tgit pull --ff-only --verify-signatures untrusted >pulloutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" pulloutput\n+'\n+\n test_expect_success GPG 'pull signed commit with --verify-signatures' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n \tgit pull --verify-signatures signed >pulloutput &&\n@@ -79,10 +100,53 @@ test_expect_success GPG 'pull commit with bad signature with --no-verify-signatu\n '\n \n test_expect_success GPG 'pull unsigned commit into unborn branch' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n \tgit init empty-repo &&\n \ttest_must_fail \\\n \t\tgit -C empty-repo pull --verify-signatures ..  2>pullerror &&\n \ttest_i18ngrep \"does not have a GPG signature\" pullerror\n '\n \n+test_expect_success GPG 'pull commit into unborn branch with bad signature and --verify-signatures' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../bad 2>pullerror &&\n+\ttest_i18ngrep \"has a bad GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=ultimate' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_config_global gpg.minTrustLevel ultimate &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=marginal' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_config_global gpg.minTrustLevel marginal &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=undefined' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_config_global gpg.minTrustLevel undefined &&\n+\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted >pulloutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" pulloutput\n+'\n+\n test_done\ndiff --git a/t/t7030-verify-tag.sh b/t/t7030-verify-tag.sh\nindex 8f077bea60..5c5bc32ccb 100755\n--- a/t/t7030-verify-tag.sh\n+++ b/t/t7030-verify-tag.sh\n@@ -86,6 +86,30 @@ test_expect_success GPGSM 'verify and show signatures x509' '\n \techo ninth-signed-x509 OK\n '\n \n+test_expect_success GPGSM 'verify and show signatures x509 with low minTrustLevel' '\n+\ttest_config gpg.minTrustLevel undefined &&\n+\tgit verify-tag ninth-signed-x509 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual &&\n+\techo ninth-signed-x509 OK\n+'\n+\n+test_expect_success GPGSM 'verify and show signatures x509 with matching minTrustLevel' '\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-tag ninth-signed-x509 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual &&\n+\techo ninth-signed-x509 OK\n+'\n+\n+test_expect_success GPGSM 'verify and show signatures x509 with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-tag ninth-signed-x509 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual &&\n+\techo ninth-signed-x509 OK\n+'\n+\n test_expect_success GPG 'detect fudged signature' '\n \tgit cat-file tag seventh-signed >raw &&\n \tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\ndiff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\nindex 682b23a068..0c06d22a00 100755\n--- a/t/t7510-signed-commit.sh\n+++ b/t/t7510-signed-commit.sh\n@@ -109,6 +109,21 @@ test_expect_success GPG 'verify-commit exits success on untrusted signature' '\n \tgrep \"not certified\" actual\n '\n \n+test_expect_success GPG 'verify-commit exits success with matching minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPG 'verify-commit exits success with low minTrustLevel' '\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPG 'verify-commit exits failure with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-commit eighth-signed-alt\n+'\n+\n test_expect_success GPG 'verify signatures with --raw' '\n \t(\n \t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n@@ -219,6 +234,30 @@ test_expect_success GPG 'show untrusted signature with custom format' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPG 'show untrusted signature with undefined trust level' '\n+\tcat >expect <<-\\EOF &&\n+\tundefined\n+\t65A0EEA02E30CAD7\n+\tEris Discordia <discord@example.net>\n+\tF8364A59E07FFE9F4D63005A65A0EEA02E30CAD7\n+\tD4BE22311AD3131E5EDA29A461092E85B7227189\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPG 'show untrusted signature with ultimate trust level' '\n+\tcat >expect <<-\\EOF &&\n+\tultimate\n+\t13B6F51ECDDE430D\n+\tC O Mitter <committer@example.com>\n+\t73D758744BE721698EC54E8713B6F51ECDDE430D\n+\t73D758744BE721698EC54E8713B6F51ECDDE430D\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success GPG 'show unknown signature with custom format' '\n \tcat >expect <<-\\EOF &&\n \tE\ndiff --git a/t/t7612-merge-verify-signatures.sh b/t/t7612-merge-verify-signatures.sh\nindex d99218a725..a426f3a89a 100755\n--- a/t/t7612-merge-verify-signatures.sh\n+++ b/t/t7612-merge-verify-signatures.sh\n@@ -66,6 +66,20 @@ test_expect_success GPG 'merge commit with untrusted signature with verification\n \ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n '\n \n+test_expect_success GPG 'merge commit with untrusted signature with verification and high minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\ttest_must_fail git merge --ff-only --verify-signatures side-untrusted 2>mergeerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n+'\n+\n+test_expect_success GPG 'merge commit with untrusted signature with verification and low minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel undefined &&\n+\tgit merge --ff-only --verify-signatures side-untrusted >mergeoutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" mergeoutput\n+'\n+\n test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n \ttest_config merge.verifySignatures true &&\n@@ -73,6 +87,14 @@ test_expect_success GPG 'merge commit with untrusted signature with merge.verify\n \ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n '\n \n+test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true and minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config merge.verifySignatures true &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\ttest_must_fail git merge --ff-only side-untrusted 2>mergeerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n+'\n+\n test_expect_success GPG 'merge signed commit with verification' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n \tgit merge --verbose --ff-only --verify-signatures side-signed >mergeoutput &&\n-- \n2.15.1.8615.g16a7d4acf3\n\n"},{"id":"388775","messageId":"87lfr5kx40.hji@dyntopia.com","threadId":"52467","inReplyTo":"20191222003123.10555-1-hji@dyntopia.com","subject":"Re: [PATCH v2 0/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-22T00:44:15Z","receivedAt":"2019-12-22T00:44:29Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"On Sun, Dec 22 2019, Hans Jerry Illikainen wrote:\n> Changes since v1:\n> * Fixed compatibility with gpg1 in parse_gpg_output().  One significant\n>   difference between gpg1 and gpg2 is that the trust levels above\n>   TRUST_NEVER are written without any additional space-separated\n>   information in gpg1 [3].  This broke the logic in the previous\n>   iterations, because the end of the TRUST_ string were searched for by\n>   looking for a space character.  Now a new-line is used as a fallback.\n\n[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\n\n(missed reference in the cover letter)\n\n-- \nhji\n"},{"id":"388807","messageId":"011401d5b9a0$58604df0$0920e9d0$@nexbridge.com","threadId":"52467","inReplyTo":"87y2v54i9v.hji@dyntopia.com","subject":"RE: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Randall S. Becker","fromEmail":"rsbecker@nexbridge.com","sentAt":"2019-12-23T14:50:34Z","receivedAt":"2019-12-23T14:50:47Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On December 21, 2019 1:59 PM, Hans Jerry Illikainen wrote:\n> On Fri, Dec 20 2019, SZEDER Gábor wrote:\n> > On Mon, Dec 16, 2019 at 03:32:04PM +0000, Hans Jerry Illikainen wrote:\n> >> This patch introduces a new configuration option: gpg.minTrustLevel.\n> >> It consolidates trust-level verification to gpg-interface.c and adds\n> >> a new `trust_level` member to the signature_check structure.\n> >\n> > This patch causes several test failures:\n> >\n> >   https://travis-ci.org/git/git/jobs/627909430#L2259\n> >\n> > I see you've already posted an updated version, so I tried it locally,\n> > and the same test scripts fail with the updated version as well.\n> \n> Sorry for that!  I'm preparing a v2 (tested with both gpg1 and gpg2).\n> \n> > I noticed that only Linux CI jobs failed, while the OSX jobs\n> > succeeded.  Our Linux CI jobs (and my box) are based on Ubuntu 16.04,\n> > and thus use GnuPG v1.4, while the OSX jobs use v2.something.  Not\n> > sure that the version difference is connected to the test failures,\n> > but I figured it's worth pointing out.\n> \n> Your observation about the different GPG versions was spot on; thanks!\n> That explains why all tests pass on my machine as well as on a personal CI\n> setup for my git contributions (both using gpg2).\n> \n> The issue was that the search for the end of a trust level to parse relied on\n> the TRUST_ line being space-separated.  But that is not always the case for\n> gpg1 (only the lowest-two trust levels contain a space followed by additional\n> information in gpg1).\n\nSide 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.\n\nCheers,\nRandall\n\n\n"},{"id":"388865","messageId":"877e2mq7t9.hji@dyntopia.com","threadId":"52467","inReplyTo":"011401d5b9a0$58604df0$0920e9d0$@nexbridge.com","subject":"RE: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-24T11:30:58Z","receivedAt":"2019-12-24T11:32:35Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"On Mon, Dec 23 2019, Randall S. Becker wrote:\n> Side question: are there any tests running with alternate GPG\n> packages? I have a platform where the official GPG itself is not\n> available, so am looking for alternatives for that community.\n\nDo you mean non-standard builds or forks of GnuPG, or alternative\nimplementations of PGP?\n\nAs it stands, the test suite is hardcoded to use gpg and gpgsm (see e.g.\nt/lib-gpg.sh).  For normal use, the gpg.program and gpg.<format>.program\nconfig options can be used to override the programs to use.  However,\nany alternative implementation would have to mimic the behavior of GnuPG\n(see gpg-interface.c -- a number of hardcoded arguments are passed in\nverify_signed_buffer() and sign_buffer(), and the output from various\noperations are GnuPG-specific.)\n\n-- \nhji\n"},{"id":"388867","messageId":"016c01d5ba65$3d8690f0$b893b2d0$@nexbridge.com","threadId":"52467","inReplyTo":"877e2mq7t9.hji@dyntopia.com","subject":"RE: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Randall S. Becker","fromEmail":"rsbecker@nexbridge.com","sentAt":"2019-12-24T14:20:00Z","receivedAt":"2019-12-24T14:20:11Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On December 24, 2019 6:31 AM, Hans Jerry Illikainen wrote:\n> To: Randall S. Becker <rsbecker@nexbridge.com>; 'SZEDER Gábor'\n> <szeder.dev@gmail.com>\n> Cc: git@vger.kernel.org\n> Subject: RE: [PATCH 1/1] gpg-interface: add minTrustLevel as a configuration\n> option\n> \n> On Mon, Dec 23 2019, Randall S. Becker wrote:\n> > Side question: are there any tests running with alternate GPG\n> > packages? I have a platform where the official GPG itself is not\n> > available, so am looking for alternatives for that community.\n> \n> Do you mean non-standard builds or forks of GnuPG, or alternative\n> implementations of PGP?\n\nI 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.\n\n> As it stands, the test suite is hardcoded to use gpg and gpgsm (see e.g.\n> t/lib-gpg.sh).  For normal use, the gpg.program and gpg.<format>.program\n> config options can be used to override the programs to use.  However, any\n> alternative implementation would have to mimic the behavior of GnuPG (see\n> gpg-interface.c -- a number of hardcoded arguments are passed in\n> verify_signed_buffer() and sign_buffer(), and the output from various\n> operations are GnuPG-specific.)\n\nThanks,\nRandall\n\n"},{"id":"388871","messageId":"xmqqblrx5yxu.fsf@gitster-ct.c.googlers.com","threadId":"52467","inReplyTo":"20191222003123.10555-2-hji@dyntopia.com","subject":"Re: [PATCH v2 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2019-12-24T19:02:53Z","receivedAt":"2019-12-24T19:05:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Hans Jerry Illikainen <hji@dyntopia.com> writes:\n\n> +\t\t\t\t/* Do we have trust level? */\n> +\t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_TRUST_LEVEL) {\n> +\t\t\t\t\t/*\n> +\t\t\t\t\t * GPG v1 and v2 differs in how the\n> +\t\t\t\t\t * TRUST_ lines are written.  Some\n> +\t\t\t\t\t * trust lines contain no additional\n> +\t\t\t\t\t * space-separated information for v1.\n> +\t\t\t\t\t */\n> +\t\t\t\t\tnext = strchr(line, ' ');\n> +\t\t\t\t\tif (!next)\n> +\t\t\t\t\t\tnext = strchrnul(line, '\\n');\n> +\t\t\t\t\ttrust = xmemdupz(line, next - line);\n\nI wonder if telling strcspn() to stop at either SP or LF is more in\nline with the existing codebase [*1*] and/or more readable.  It\nwould make this part to:\n\n\t\tsize_t trust_size = strcspn(line, \" \\n\");\n\t\ttrust = xmemdupz(line, trust_size);\n\nwithout the need to use or update the 'next' variable, if I am not\nmistaken?\n\nBy the way, while we are looking at this patch, I notice that,\nthroughout the function, the use of variable 'next' feels rather\nmisleading, at least to me.\n\nWhen I see a loop that iterates over a block of lines, and a\nvariable 'line' is used to point at the beginning of the current\nline at the beginning of each iteration and the code in the\niteration updates a pointer 'next', I'd expect 'next' (or perhaps\n'next+1') to become the new value of 'line' when the current round\nof the iteration ends (i.e. the name 'next' would stand for 'here is\nwhere we expect the next line to start').  But the code we see in\nthis function uses it for 'here is the end of the current _token_ on\nthe line', primarily so that it can do something to the byte range\n(line,<end-of-token>) and it never gets used as 'now we are done\nwith the line, let's move on to the next line'.\n\nThis matters because it makes it unclear to decide if the above two\nlines I gave as a counter-proposal is sufficient, or if it also\nneeds to say \"next = line + trust_size\" to keep 'next' up-to-date.\nThe name of the varirable implies it should be, but the way the\ncode uses 'next' says it is a throw-away variable whose value does\nnot matter once we have done with the end of the current token.\n\nI wonder if the code becomes less misleading if we either (1)\nrenamed 'next' to a name that hints more strongly that it is not the\n'next' line but the end of the current token we are interested in,\nor (2) get rid of the pointer and instead counted size of the\ncurrent token we are interested in, or perhaps both?  \n\nThis is not the fault of this patch, but I just mention it before I\nforget.\n\nThanks.\n"},{"id":"388987","messageId":"20191227135557.31437-1-hji@dyntopia.com","threadId":"52467","inReplyTo":"20191222003123.10555-1-hji@dyntopia.com","subject":"[PATCH v3 0/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-27T13:55:56Z","receivedAt":"2019-12-27T13:56:11Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"Previously, signature verification for merge and pull operations checked\nif the key had a trust-level of either TRUST_NEVER or TRUST_UNDEFINED in\nverify_merge_signature().  If that was the case, the process die()d.\n\nThe other code paths that did signature verification relied entirely on\nthe return code from check_commit_signature().  And signatures made with\na good key, irregardless of its trust level, was considered valid by\ncheck_commit_signature().\n\nThis difference in behavior might induce users to erroneously assume\nthat the trust level of a key in their keyring is always considered by\nGit, even for operations where it is not (e.g. during a verify-commit or\nverify-tag).\n\nThe way it worked was by gpg-interface.c storing the result from the\nkey/signature status *and* the lowest-two trust levels in the `result`\nmember of the signature_check structure (the last of these status lines\nthat were encountered got written to `result`).  These are documented in\nGPG under the subsection `General status codes` and `Key related`,\nrespectively [1].\n\nThe GPG documentation says the following on the TRUST_ status codes [1]:\n\n    \"\"\"\n    These are several similar status codes:\n\n    - TRUST_UNDEFINED <error_token>\n    - TRUST_NEVER     <error_token>\n    - TRUST_MARGINAL  [0  [<validation_model>]]\n    - TRUST_FULLY     [0  [<validation_model>]]\n    - TRUST_ULTIMATE  [0  [<validation_model>]]\n\n    For good signatures one of these status lines are emitted to\n    indicate the validity of the key used to create the signature.\n    The error token values are currently only emitted by gpgsm.\n    \"\"\"\n\nMy interpretation is that the trust level is conceptionally different\nfrom the validity of the key and/or signature.  That seems to also have\nbeen the assumption of the old code in check_signature() where a result\nof 'G' (as in GOODSIG) and 'U' (as in TRUST_NEVER or TRUST_UNDEFINED)\nwere both considered a success.\n\nThe two cases where a result of 'U' had special meaning were in\nverify_merge_signature() (where this caused git to die()) and in\nformat_commit_one() (where it affected the output of the %G? format\nspecifier).\n\nI think it makes sense to refactor the processing of TRUST_ status lines\nsuch that users can configure a minimum trust level that is enforced\nglobally, rather than have individual parts of git (e.g. merge) do it\nthemselves (except for a grace period with backward compatibility).\n\nI also think it makes sense to not store the trust level in the same\nstruct member as the key/signature status.  While the presence of a\nTRUST_ status code does imply that the signature is good (see the first\nparagraph in the included snippet above), as far as I can tell, the\norder of the status lines from GPG isn't well-defined; thus it would\nseem plausible that the trust level could be overwritten with the\nkey/signature status if they were stored in the same member of the\nsignature_check structure.\n\nThis patch introduces a new configuration option: gpg.minTrustLevel.  It\nconsolidates trust-level verification to gpg-interface.c and adds a new\n`trust_level` member to the signature_check structure.\n\nBackward-compatibility is maintained by introducing a special case in\nverify_merge_signature() such that if no user-configurable\ngpg.minTrustLevel is set, then the old behavior of rejecting\nTRUST_UNDEFINED and TRUST_NEVER is enforced.  If, on the other hand,\ngpg.minTrustLevel is set, then that value overrides the old behavior.\n\nSimilarly, the %G? format specifier will continue show 'U' for\nsignatures made with a key that has a trust level of TRUST_UNDEFINED or\nTRUST_NEVER, even though the 'U' character no longer exist in the\n`result` member of the signature_check structure.  A new format\nspecifier, %GT, is also introduced for users that want to show all\npossible trust levels for a signature.\n\nAnother approach would have been to simply drop the trust-level\nrequirement in verify_merge_signature().  This would also have made the\nbehavior consistent with other parts of git that perform signature\nverification.  However, requiring a minimum trust level for signing keys\ndoes seem to have a real-world use-case.  For example, the build system\nused by the Qubes OS project currently parses the raw output from\nverify-tag in order to assert a minimum trust level for keys used to\nsign git tags [2].\n\n\nChanges since v0:\n* Added backward-compatibility with the old behavior of\n  verify_merge_signature().  The old behavior is overridden if a user\n  has configured gpg.minTrustLevel.  My approach is kind of ugly because\n  now all users of verify_merge_signature() has to be aware of\n  gpg.minTrustLevel to know if it should override the default behavior.\n  An alternative might be to have a configurable per-operation trust\n  level (e.g. merge.minTrustLevel), but I'm not sure how sensible that\n  is either.\n* Added backward-compatiblity with the old behavior of %G?.\n\nChanges since v1:\n* Fixed compatibility with gpg1 in parse_gpg_output().  One significant\n  difference between gpg1 and gpg2 is that the trust levels above\n  TRUST_NEVER are written without any additional space-separated\n  information in gpg1 [3].  This broke the logic in the previous\n  iterations, because the end of the TRUST_ string were searched for by\n  looking for a space character.  Now a new-line is used as a fallback.\n\nChanges since v2:\n* Replaced strstr() + strchrnul() with strcspn().\n\n\n[1] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/doc/DETAILS;h=bd00006e933ac56719b1edd2478ecd79273eae72;hb=refs/heads/master\n[2] https://github.com/QubesOS/qubes-builder/blob/9674c1991deef45b1a1b1c71fddfab14ba50dccf/scripts/verify-git-tag#L43\n[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\n\nHans Jerry Illikainen (1):\n  gpg-interface: add minTrustLevel as a configuration option\n\n Documentation/config/gpg.txt       | 15 +++++\n Documentation/pretty-formats.txt   |  1 +\n builtin/merge.c                    |  9 ++-\n builtin/pull.c                     | 13 ++++-\n commit.c                           | 12 ++--\n commit.h                           | 12 +++-\n gpg-interface.c                    | 91 ++++++++++++++++++++++++++----\n gpg-interface.h                    | 10 +++-\n pretty.c                           | 30 +++++++++-\n t/t5573-pull-verify-signatures.sh  | 64 +++++++++++++++++++++\n t/t7030-verify-tag.sh              | 24 ++++++++\n t/t7510-signed-commit.sh           | 39 +++++++++++++\n t/t7612-merge-verify-signatures.sh | 22 ++++++++\n 13 files changed, 319 insertions(+), 23 deletions(-)\n\n--\n2.24.1.591.g64816733a6\n"},{"id":"388988","messageId":"20191227135557.31437-2-hji@dyntopia.com","threadId":"52467","inReplyTo":"20191227135557.31437-1-hji@dyntopia.com","subject":"[PATCH v3 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-27T13:55:57Z","receivedAt":"2019-12-27T13:56:17Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"Previously, signature verification for merge and pull operations checked\nif the key had a trust-level of either TRUST_NEVER or TRUST_UNDEFINED in\nverify_merge_signature().  If that was the case, the process die()d.\n\nThe other code paths that did signature verification relied entirely on\nthe return code from check_commit_signature().  And signatures made with\na good key, irregardless of its trust level, was considered valid by\ncheck_commit_signature().\n\nThis difference in behavior might induce users to erroneously assume\nthat the trust level of a key in their keyring is always considered by\nGit, even for operations where it is not (e.g. during a verify-commit or\nverify-tag).\n\nThe way it worked was by gpg-interface.c storing the result from the\nkey/signature status *and* the lowest-two trust levels in the `result`\nmember of the signature_check structure (the last of these status lines\nthat were encountered got written to `result`).  These are documented in\nGPG under the subsection `General status codes` and `Key related`,\nrespectively [1].\n\nThe GPG documentation says the following on the TRUST_ status codes [1]:\n\n    \"\"\"\n    These are several similar status codes:\n\n    - TRUST_UNDEFINED <error_token>\n    - TRUST_NEVER     <error_token>\n    - TRUST_MARGINAL  [0  [<validation_model>]]\n    - TRUST_FULLY     [0  [<validation_model>]]\n    - TRUST_ULTIMATE  [0  [<validation_model>]]\n\n    For good signatures one of these status lines are emitted to\n    indicate the validity of the key used to create the signature.\n    The error token values are currently only emitted by gpgsm.\n    \"\"\"\n\nMy interpretation is that the trust level is conceptionally different\nfrom the validity of the key and/or signature.  That seems to also have\nbeen the assumption of the old code in check_signature() where a result\nof 'G' (as in GOODSIG) and 'U' (as in TRUST_NEVER or TRUST_UNDEFINED)\nwere both considered a success.\n\nThe two cases where a result of 'U' had special meaning were in\nverify_merge_signature() (where this caused git to die()) and in\nformat_commit_one() (where it affected the output of the %G? format\nspecifier).\n\nI think it makes sense to refactor the processing of TRUST_ status lines\nsuch that users can configure a minimum trust level that is enforced\nglobally, rather than have individual parts of git (e.g. merge) do it\nthemselves (except for a grace period with backward compatibility).\n\nI also think it makes sense to not store the trust level in the same\nstruct member as the key/signature status.  While the presence of a\nTRUST_ status code does imply that the signature is good (see the first\nparagraph in the included snippet above), as far as I can tell, the\norder of the status lines from GPG isn't well-defined; thus it would\nseem plausible that the trust level could be overwritten with the\nkey/signature status if they were stored in the same member of the\nsignature_check structure.\n\nThis patch introduces a new configuration option: gpg.minTrustLevel.  It\nconsolidates trust-level verification to gpg-interface.c and adds a new\n`trust_level` member to the signature_check structure.\n\nBackward-compatibility is maintained by introducing a special case in\nverify_merge_signature() such that if no user-configurable\ngpg.minTrustLevel is set, then the old behavior of rejecting\nTRUST_UNDEFINED and TRUST_NEVER is enforced.  If, on the other hand,\ngpg.minTrustLevel is set, then that value overrides the old behavior.\n\nSimilarly, the %G? format specifier will continue show 'U' for\nsignatures made with a key that has a trust level of TRUST_UNDEFINED or\nTRUST_NEVER, even though the 'U' character no longer exist in the\n`result` member of the signature_check structure.  A new format\nspecifier, %GT, is also introduced for users that want to show all\npossible trust levels for a signature.\n\nAnother approach would have been to simply drop the trust-level\nrequirement in verify_merge_signature().  This would also have made the\nbehavior consistent with other parts of git that perform signature\nverification.  However, requiring a minimum trust level for signing keys\ndoes seem to have a real-world use-case.  For example, the build system\nused by the Qubes OS project currently parses the raw output from\nverify-tag in order to assert a minimum trust level for keys used to\nsign git tags [2].\n\n[1] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/doc/DETAILS;h=bd00006e933ac56719b1edd2478ecd79273eae72;hb=refs/heads/master\n[2] https://github.com/QubesOS/qubes-builder/blob/9674c1991deef45b1a1b1c71fddfab14ba50dccf/scripts/verify-git-tag#L43\n\nSigned-off-by: Hans Jerry Illikainen <hji@dyntopia.com>\n---\n Documentation/config/gpg.txt       | 15 +++++\n Documentation/pretty-formats.txt   |  1 +\n builtin/merge.c                    |  9 ++-\n builtin/pull.c                     | 13 ++++-\n commit.c                           | 12 ++--\n commit.h                           | 12 +++-\n gpg-interface.c                    | 91 ++++++++++++++++++++++++++----\n gpg-interface.h                    | 10 +++-\n pretty.c                           | 30 +++++++++-\n t/t5573-pull-verify-signatures.sh  | 64 +++++++++++++++++++++\n t/t7030-verify-tag.sh              | 24 ++++++++\n t/t7510-signed-commit.sh           | 39 +++++++++++++\n t/t7612-merge-verify-signatures.sh | 22 ++++++++\n 13 files changed, 319 insertions(+), 23 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex cce2c89245..d94025cb36 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -18,3 +18,18 @@ gpg.<format>.program::\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n \tvalue for `gpg.x509.program` is \"gpgsm\".\n+\n+gpg.minTrustLevel::\n+\tSpecifies a minimum trust level for signature verification.  If\n+\tthis option is unset, then signature verification for merge\n+\toperations require a key with at least `marginal` trust.  Other\n+\toperations that perform signature verification require a key\n+\twith at least `undefined` trust.  Setting this option overrides\n+\tthe required trust-level for all operations.  Supported values,\n+\tin increasing order of significance:\n++\n+* `undefined`\n+* `never`\n+* `marginal`\n+* `fully`\n+* `ultimate`\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 1a7212ce5a..a4b6f49186 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -226,6 +226,7 @@ endif::git-rev-list[]\n '%GF':: show the fingerprint of the key used to sign a signed commit\n '%GP':: show the fingerprint of the primary key whose subkey was used\n \tto sign a signed commit\n+'%GT':: show the trust level for the key used to sign a signed commit\n '%gD':: reflog selector, e.g., `refs/stash@{1}` or `refs/stash@{2\n \tminutes ago}`; the format follows the rules described for the\n \t`-g` option. The portion before the `@` is the refname as\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 062e911441..d127d2225f 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -62,6 +62,7 @@ static int show_diffstat = 1, shortlog_len = -1, squash;\n static int option_commit = -1;\n static int option_edit = -1;\n static int allow_trivial = 1, have_message, verify_signatures;\n+static int check_trust_level = 1;\n static int overwrite_ignore = 1;\n static struct strbuf merge_msg = STRBUF_INIT;\n static struct strategy **use_strategies;\n@@ -631,6 +632,8 @@ static int git_merge_config(const char *k, const char *v, void *cb)\n \t} else if (!strcmp(k, \"commit.gpgsign\")) {\n \t\tsign_commit = git_config_bool(k, v) ? \"\" : NULL;\n \t\treturn 0;\n+\t} else if (!strcmp(k, \"gpg.mintrustlevel\")) {\n+\t\tcheck_trust_level = 0;\n \t}\n \n \tstatus = fmt_merge_msg_config(k, v, cb);\n@@ -1397,7 +1400,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \t\t\tdie(_(\"Can merge only exactly one commit into empty head\"));\n \n \t\tif (verify_signatures)\n-\t\t\tverify_merge_signature(remoteheads->item, verbosity);\n+\t\t\tverify_merge_signature(remoteheads->item, verbosity,\n+\t\t\t\t\t       check_trust_level);\n \n \t\tremote_head_oid = &remoteheads->item->object.oid;\n \t\tread_empty(remote_head_oid, 0);\n@@ -1420,7 +1424,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \n \tif (verify_signatures) {\n \t\tfor (p = remoteheads; p; p = p->next) {\n-\t\t\tverify_merge_signature(p->item, verbosity);\n+\t\t\tverify_merge_signature(p->item, verbosity,\n+\t\t\t\t\t       check_trust_level);\n \t\t}\n \t}\n \ndiff --git a/builtin/pull.c b/builtin/pull.c\nindex d25ff13a60..d4e3e77c8e 100644\n--- a/builtin/pull.c\n+++ b/builtin/pull.c\n@@ -107,6 +107,7 @@ static char *opt_ff;\n static char *opt_verify_signatures;\n static int opt_autostash = -1;\n static int config_autostash;\n+static int check_trust_level = 1;\n static struct argv_array opt_strategies = ARGV_ARRAY_INIT;\n static struct argv_array opt_strategy_opts = ARGV_ARRAY_INIT;\n static char *opt_gpg_sign;\n@@ -355,6 +356,8 @@ static enum rebase_type config_get_rebase(void)\n  */\n static int git_pull_config(const char *var, const char *value, void *cb)\n {\n+\tint status;\n+\n \tif (!strcmp(var, \"rebase.autostash\")) {\n \t\tconfig_autostash = git_config_bool(var, value);\n \t\treturn 0;\n@@ -362,7 +365,14 @@ static int git_pull_config(const char *var, const char *value, void *cb)\n \t\trecurse_submodules = git_config_bool(var, value) ?\n \t\t\tRECURSE_SUBMODULES_ON : RECURSE_SUBMODULES_OFF;\n \t\treturn 0;\n+\t} else if (!strcmp(var, \"gpg.mintrustlevel\")) {\n+\t\tcheck_trust_level = 0;\n \t}\n+\n+\tstatus = git_gpg_config(var, value, cb);\n+\tif (status)\n+\t\treturn status;\n+\n \treturn git_default_config(var, value, cb);\n }\n \n@@ -587,7 +597,8 @@ static int pull_into_void(const struct object_id *merge_head,\n \t\t\tdie(_(\"unable to access commit %s\"),\n \t\t\t    oid_to_hex(merge_head));\n \n-\t\tverify_merge_signature(commit, opt_verbosity);\n+\t\tverify_merge_signature(commit, opt_verbosity,\n+\t\t\t\t       check_trust_level);\n \t}\n \n \t/*\ndiff --git a/commit.c b/commit.c\nindex 434ec030d6..3f91d3efc5 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1136,21 +1136,23 @@ int check_commit_signature(const struct commit *commit, struct signature_check *\n \treturn ret;\n }\n \n-void verify_merge_signature(struct commit *commit, int verbosity)\n+void verify_merge_signature(struct commit *commit, int verbosity,\n+\t\t\t    int check_trust)\n {\n \tchar hex[GIT_MAX_HEXSZ + 1];\n \tstruct signature_check signature_check;\n+\tint ret;\n \tmemset(&signature_check, 0, sizeof(signature_check));\n \n-\tcheck_commit_signature(commit, &signature_check);\n+\tret = check_commit_signature(commit, &signature_check);\n \n \tfind_unique_abbrev_r(hex, &commit->object.oid, DEFAULT_ABBREV);\n \tswitch (signature_check.result) {\n \tcase 'G':\n+\t\tif (ret || (check_trust && signature_check.trust_level < TRUST_MARGINAL))\n+\t\t\tdie(_(\"Commit %s has an untrusted GPG signature, \"\n+\t\t\t      \"allegedly by %s.\"), hex, signature_check.signer);\n \t\tbreak;\n-\tcase 'U':\n-\t\tdie(_(\"Commit %s has an untrusted GPG signature, \"\n-\t\t      \"allegedly by %s.\"), hex, signature_check.signer);\n \tcase 'B':\n \t\tdie(_(\"Commit %s has a bad GPG signature \"\n \t\t      \"allegedly by %s.\"), hex, signature_check.signer);\ndiff --git a/commit.h b/commit.h\nindex 221cdaa34b..008a0fa4a0 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -383,8 +383,18 @@ int compare_commits_by_author_date(const void *a_, const void *b_, void *unused)\n  * Verify a single commit with check_commit_signature() and die() if it is not\n  * a good signature. This isn't really suitable for general use, but is a\n  * helper to implement consistent logic for pull/merge --verify-signatures.\n+ *\n+ * The check_trust parameter is meant for backward-compatibility.  The GPG\n+ * interface verifies key trust with a default trust level that is below the\n+ * default trust level for merge operations.  Its value should be non-zero if\n+ * the user hasn't set a minimum trust level explicitly in their configuration.\n+ *\n+ * If the user has set a minimum trust level, then that value should be obeyed\n+ * and check_trust should be zero, even if the configured trust level is below\n+ * the default trust level for merges.\n  */\n-void verify_merge_signature(struct commit *commit, int verbose);\n+void verify_merge_signature(struct commit *commit, int verbose,\n+\t\t\t    int check_trust);\n \n int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused);\n int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 5134ce2780..2d538bcd6e 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -7,6 +7,8 @@\n #include \"tempfile.h\"\n \n static char *configured_signing_key;\n+static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n+\n struct gpg_format {\n \tconst char *name;\n \tconst char *program;\n@@ -85,6 +87,8 @@ void signature_check_clear(struct signature_check *sigc)\n #define GPG_STATUS_UID\t\t(1<<2)\n /* The status includes key fingerprints */\n #define GPG_STATUS_FINGERPRINT\t(1<<3)\n+/* The status includes trust level */\n+#define GPG_STATUS_TRUST_LEVEL\t(1<<4)\n \n /* Short-hand for standard exclusive *SIG status with keyid & UID */\n #define GPG_STATUS_STDSIG\t(GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID|GPG_STATUS_UID)\n@@ -96,13 +100,23 @@ static struct {\n } sigcheck_gpg_status[] = {\n \t{ 'G', \"GOODSIG \", GPG_STATUS_STDSIG },\n \t{ 'B', \"BADSIG \", GPG_STATUS_STDSIG },\n-\t{ 'U', \"TRUST_NEVER\", 0 },\n-\t{ 'U', \"TRUST_UNDEFINED\", 0 },\n \t{ 'E', \"ERRSIG \", GPG_STATUS_EXCLUSIVE|GPG_STATUS_KEYID },\n \t{ 'X', \"EXPSIG \", GPG_STATUS_STDSIG },\n \t{ 'Y', \"EXPKEYSIG \", GPG_STATUS_STDSIG },\n \t{ 'R', \"REVKEYSIG \", GPG_STATUS_STDSIG },\n \t{ 0, \"VALIDSIG \", GPG_STATUS_FINGERPRINT },\n+\t{ 0, \"TRUST_\", GPG_STATUS_TRUST_LEVEL },\n+};\n+\n+static struct {\n+\tconst char *key;\n+\tenum signature_trust_level value;\n+} sigcheck_gpg_trust_level[] = {\n+\t{ \"UNDEFINED\", TRUST_UNDEFINED },\n+\t{ \"NEVER\", TRUST_NEVER },\n+\t{ \"MARGINAL\", TRUST_MARGINAL },\n+\t{ \"FULLY\", TRUST_FULLY },\n+\t{ \"ULTIMATE\", TRUST_ULTIMATE },\n };\n \n static void replace_cstring(char **field, const char *line, const char *next)\n@@ -115,6 +129,20 @@ static void replace_cstring(char **field, const char *line, const char *next)\n \t\t*field = NULL;\n }\n \n+static int parse_gpg_trust_level(const char *level,\n+\t\t\t\t enum signature_trust_level *res)\n+{\n+\tsize_t i;\n+\n+\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_trust_level); i++) {\n+\t\tif (!strcmp(sigcheck_gpg_trust_level[i].key, level)) {\n+\t\t\t*res = sigcheck_gpg_trust_level[i].value;\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\treturn 1;\n+}\n+\n static void parse_gpg_output(struct signature_check *sigc)\n {\n \tconst char *buf = sigc->gpg_status;\n@@ -136,9 +164,18 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t\t/* Iterate over all search strings */\n \t\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {\n \t\t\tif (skip_prefix(line, sigcheck_gpg_status[i].check, &line)) {\n+\t\t\t\t/*\n+\t\t\t\t * GOODSIG, BADSIG etc. can occur only once for\n+\t\t\t\t * each signature.  Therefore, if we had more\n+\t\t\t\t * than one then we're dealing with multiple\n+\t\t\t\t * signatures.  We don't support them\n+\t\t\t\t * currently, and they're rather hard to\n+\t\t\t\t * create, so something is likely fishy and we\n+\t\t\t\t * should reject them altogether.\n+\t\t\t\t */\n \t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_EXCLUSIVE) {\n \t\t\t\t\tif (seen_exclusive_status++)\n-\t\t\t\t\t\tgoto found_duplicate_status;\n+\t\t\t\t\t\tgoto error;\n \t\t\t\t}\n \n \t\t\t\tif (sigcheck_gpg_status[i].result)\n@@ -154,6 +191,25 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t\t\t\t\t\treplace_cstring(&sigc->signer, line, next);\n \t\t\t\t\t}\n \t\t\t\t}\n+\n+\t\t\t\t/* Do we have trust level? */\n+\t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_TRUST_LEVEL) {\n+\t\t\t\t\t/*\n+\t\t\t\t\t * GPG v1 and v2 differs in how the\n+\t\t\t\t\t * TRUST_ lines are written.  Some\n+\t\t\t\t\t * trust lines contain no additional\n+\t\t\t\t\t * space-separated information for v1.\n+\t\t\t\t\t */\n+\t\t\t\t\tsize_t trust_size = strcspn(line, \" \\n\");\n+\t\t\t\t\tchar *trust = xmemdupz(line, trust_size);\n+\n+\t\t\t\t\tif (parse_gpg_trust_level(trust, &sigc->trust_level)) {\n+\t\t\t\t\t\tfree(trust);\n+\t\t\t\t\t\tgoto error;\n+\t\t\t\t\t}\n+\t\t\t\t\tfree(trust);\n+\t\t\t\t}\n+\n \t\t\t\t/* Do we have fingerprint? */\n \t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_FINGERPRINT) {\n \t\t\t\t\tconst char *limit;\n@@ -191,14 +247,7 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t}\n \treturn;\n \n-found_duplicate_status:\n-\t/*\n-\t * GOODSIG, BADSIG etc. can occur only once for each signature.\n-\t * Therefore, if we had more than one then we're dealing with multiple\n-\t * signatures.  We don't support them currently, and they're rather\n-\t * hard to create, so something is likely fishy and we should reject\n-\t * them altogether.\n-\t */\n+error:\n \tsigc->result = 'E';\n \t/* Clear partial data to avoid confusion */\n \tFREE_AND_NULL(sigc->primary_key_fingerprint);\n@@ -264,6 +313,7 @@ int check_signature(const char *payload, size_t plen, const char *signature,\n \tint status;\n \n \tsigc->result = 'N';\n+\tsigc->trust_level = -1;\n \n \tstatus = verify_signed_buffer(payload, plen, signature, slen,\n \t\t\t\t      &gpg_output, &gpg_status);\n@@ -273,7 +323,8 @@ int check_signature(const char *payload, size_t plen, const char *signature,\n \tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n \tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n \tparse_gpg_output(sigc);\n-\tstatus |= sigc->result != 'G' && sigc->result != 'U';\n+\tstatus |= sigc->result != 'G';\n+\tstatus |= sigc->trust_level < configured_min_trust_level;\n \n  out:\n \tstrbuf_release(&gpg_status);\n@@ -320,6 +371,8 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n {\n \tstruct gpg_format *fmt = NULL;\n \tchar *fmtname = NULL;\n+\tchar *trust;\n+\tint ret;\n \n \tif (!strcmp(var, \"user.signingkey\")) {\n \t\tif (!value)\n@@ -339,6 +392,20 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.mintrustlevel\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\n+\t\ttrust = xstrdup_toupper(value);\n+\t\tret = parse_gpg_trust_level(trust, &configured_min_trust_level);\n+\t\tfree(trust);\n+\n+\t\tif (ret)\n+\t\t\treturn error(\"unsupported value for %s: %s\", var,\n+\t\t\t\t     value);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n \t\tfmtname = \"openpgp\";\n \ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 93cc3aff5c..f4e9b4f371 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -7,6 +7,14 @@ struct strbuf;\n #define GPG_VERIFY_RAW\t\t\t2\n #define GPG_VERIFY_OMIT_STATUS\t4\n \n+enum signature_trust_level {\n+\tTRUST_UNDEFINED,\n+\tTRUST_NEVER,\n+\tTRUST_MARGINAL,\n+\tTRUST_FULLY,\n+\tTRUST_ULTIMATE,\n+};\n+\n struct signature_check {\n \tchar *payload;\n \tchar *gpg_output;\n@@ -16,7 +24,6 @@ struct signature_check {\n \t * possible \"result\":\n \t * 0 (not checked)\n \t * N (checked but no further result)\n-\t * U (untrusted good)\n \t * G (good)\n \t * B (bad)\n \t */\n@@ -25,6 +32,7 @@ struct signature_check {\n \tchar *key;\n \tchar *fingerprint;\n \tchar *primary_key_fingerprint;\n+\tenum signature_trust_level trust_level;\n };\n \n void signature_check_clear(struct signature_check *sigc);\ndiff --git a/pretty.c b/pretty.c\nindex 305e903192..f5fbbc5ffb 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1311,9 +1311,18 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\tcase '?':\n \t\t\tswitch (c->signature_check.result) {\n \t\t\tcase 'G':\n+\t\t\t\tswitch (c->signature_check.trust_level) {\n+\t\t\t\tcase TRUST_UNDEFINED:\n+\t\t\t\tcase TRUST_NEVER:\n+\t\t\t\t\tstrbuf_addch(sb, 'U');\n+\t\t\t\t\tbreak;\n+\t\t\t\tdefault:\n+\t\t\t\t\tstrbuf_addch(sb, 'G');\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t\tbreak;\n \t\t\tcase 'B':\n \t\t\tcase 'E':\n-\t\t\tcase 'U':\n \t\t\tcase 'N':\n \t\t\tcase 'X':\n \t\t\tcase 'Y':\n@@ -1337,6 +1346,25 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tif (c->signature_check.primary_key_fingerprint)\n \t\t\t\tstrbuf_addstr(sb, c->signature_check.primary_key_fingerprint);\n \t\t\tbreak;\n+\t\tcase 'T':\n+\t\t\tswitch (c->signature_check.trust_level) {\n+\t\t\tcase TRUST_UNDEFINED:\n+\t\t\t\tstrbuf_addstr(sb, \"undefined\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_NEVER:\n+\t\t\t\tstrbuf_addstr(sb, \"never\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_MARGINAL:\n+\t\t\t\tstrbuf_addstr(sb, \"marginal\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_FULLY:\n+\t\t\t\tstrbuf_addstr(sb, \"fully\");\n+\t\t\t\tbreak;\n+\t\t\tcase TRUST_ULTIMATE:\n+\t\t\t\tstrbuf_addstr(sb, \"ultimate\");\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tbreak;\n \t\tdefault:\n \t\t\treturn 0;\n \t\t}\ndiff --git a/t/t5573-pull-verify-signatures.sh b/t/t5573-pull-verify-signatures.sh\nindex 3e9876e197..a53dd8550d 100755\n--- a/t/t5573-pull-verify-signatures.sh\n+++ b/t/t5573-pull-verify-signatures.sh\n@@ -60,6 +60,27 @@ test_expect_success GPG 'pull commit with untrusted signature with --verify-sign\n \ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n '\n \n+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=ultimate' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=marginal' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\ttest_must_fail git pull --ff-only --verify-signatures untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit with untrusted signature with --verify-signatures and minTrustLevel=undefined' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel undefined &&\n+\tgit pull --ff-only --verify-signatures untrusted >pulloutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" pulloutput\n+'\n+\n test_expect_success GPG 'pull signed commit with --verify-signatures' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n \tgit pull --verify-signatures signed >pulloutput &&\n@@ -79,10 +100,53 @@ test_expect_success GPG 'pull commit with bad signature with --no-verify-signatu\n '\n \n test_expect_success GPG 'pull unsigned commit into unborn branch' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n \tgit init empty-repo &&\n \ttest_must_fail \\\n \t\tgit -C empty-repo pull --verify-signatures ..  2>pullerror &&\n \ttest_i18ngrep \"does not have a GPG signature\" pullerror\n '\n \n+test_expect_success GPG 'pull commit into unborn branch with bad signature and --verify-signatures' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../bad 2>pullerror &&\n+\ttest_i18ngrep \"has a bad GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=ultimate' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_config_global gpg.minTrustLevel ultimate &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=marginal' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_config_global gpg.minTrustLevel marginal &&\n+\ttest_must_fail \\\n+\t\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted 2>pullerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" pullerror\n+'\n+\n+test_expect_success GPG 'pull commit into unborn branch with untrusted signature and --verify-signatures and minTrustLevel=undefined' '\n+\ttest_when_finished \"rm -rf empty-repo\" &&\n+\tgit init empty-repo &&\n+\ttest_config_global gpg.minTrustLevel undefined &&\n+\tgit -C empty-repo pull --ff-only --verify-signatures ../untrusted >pulloutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" pulloutput\n+'\n+\n test_done\ndiff --git a/t/t7030-verify-tag.sh b/t/t7030-verify-tag.sh\nindex 8f077bea60..5c5bc32ccb 100755\n--- a/t/t7030-verify-tag.sh\n+++ b/t/t7030-verify-tag.sh\n@@ -86,6 +86,30 @@ test_expect_success GPGSM 'verify and show signatures x509' '\n \techo ninth-signed-x509 OK\n '\n \n+test_expect_success GPGSM 'verify and show signatures x509 with low minTrustLevel' '\n+\ttest_config gpg.minTrustLevel undefined &&\n+\tgit verify-tag ninth-signed-x509 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual &&\n+\techo ninth-signed-x509 OK\n+'\n+\n+test_expect_success GPGSM 'verify and show signatures x509 with matching minTrustLevel' '\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-tag ninth-signed-x509 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual &&\n+\techo ninth-signed-x509 OK\n+'\n+\n+test_expect_success GPGSM 'verify and show signatures x509 with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-tag ninth-signed-x509 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual &&\n+\techo ninth-signed-x509 OK\n+'\n+\n test_expect_success GPG 'detect fudged signature' '\n \tgit cat-file tag seventh-signed >raw &&\n \tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\ndiff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\nindex 682b23a068..0c06d22a00 100755\n--- a/t/t7510-signed-commit.sh\n+++ b/t/t7510-signed-commit.sh\n@@ -109,6 +109,21 @@ test_expect_success GPG 'verify-commit exits success on untrusted signature' '\n \tgrep \"not certified\" actual\n '\n \n+test_expect_success GPG 'verify-commit exits success with matching minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPG 'verify-commit exits success with low minTrustLevel' '\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPG 'verify-commit exits failure with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-commit eighth-signed-alt\n+'\n+\n test_expect_success GPG 'verify signatures with --raw' '\n \t(\n \t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n@@ -219,6 +234,30 @@ test_expect_success GPG 'show untrusted signature with custom format' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPG 'show untrusted signature with undefined trust level' '\n+\tcat >expect <<-\\EOF &&\n+\tundefined\n+\t65A0EEA02E30CAD7\n+\tEris Discordia <discord@example.net>\n+\tF8364A59E07FFE9F4D63005A65A0EEA02E30CAD7\n+\tD4BE22311AD3131E5EDA29A461092E85B7227189\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPG 'show untrusted signature with ultimate trust level' '\n+\tcat >expect <<-\\EOF &&\n+\tultimate\n+\t13B6F51ECDDE430D\n+\tC O Mitter <committer@example.com>\n+\t73D758744BE721698EC54E8713B6F51ECDDE430D\n+\t73D758744BE721698EC54E8713B6F51ECDDE430D\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success GPG 'show unknown signature with custom format' '\n \tcat >expect <<-\\EOF &&\n \tE\ndiff --git a/t/t7612-merge-verify-signatures.sh b/t/t7612-merge-verify-signatures.sh\nindex d99218a725..a426f3a89a 100755\n--- a/t/t7612-merge-verify-signatures.sh\n+++ b/t/t7612-merge-verify-signatures.sh\n@@ -66,6 +66,20 @@ test_expect_success GPG 'merge commit with untrusted signature with verification\n \ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n '\n \n+test_expect_success GPG 'merge commit with untrusted signature with verification and high minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\ttest_must_fail git merge --ff-only --verify-signatures side-untrusted 2>mergeerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n+'\n+\n+test_expect_success GPG 'merge commit with untrusted signature with verification and low minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config gpg.minTrustLevel undefined &&\n+\tgit merge --ff-only --verify-signatures side-untrusted >mergeoutput &&\n+\ttest_i18ngrep \"has a good GPG signature\" mergeoutput\n+'\n+\n test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n \ttest_config merge.verifySignatures true &&\n@@ -73,6 +87,14 @@ test_expect_success GPG 'merge commit with untrusted signature with merge.verify\n \ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n '\n \n+test_expect_success GPG 'merge commit with untrusted signature with merge.verifySignatures=true and minTrustLevel' '\n+\ttest_when_finished \"git reset --hard && git checkout initial\" &&\n+\ttest_config merge.verifySignatures true &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\ttest_must_fail git merge --ff-only side-untrusted 2>mergeerror &&\n+\ttest_i18ngrep \"has an untrusted GPG signature\" mergeerror\n+'\n+\n test_expect_success GPG 'merge signed commit with verification' '\n \ttest_when_finished \"git reset --hard && git checkout initial\" &&\n \tgit merge --verbose --ff-only --verify-signatures side-signed >mergeoutput &&\n-- \n2.24.1.591.g64816733a6\n\n"},{"id":"388989","messageId":"87r20pkhir.hji@dyntopia.com","threadId":"52467","inReplyTo":"xmqqblrx5yxu.fsf@gitster-ct.c.googlers.com","subject":"Re: [PATCH v2 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Hans Jerry Illikainen","fromEmail":"hji@dyntopia.com","sentAt":"2019-12-27T13:46:52Z","receivedAt":"2019-12-27T13:56:41Z","isPatch":true,"sender":{"key":"hji@dyntopia.com","avatar":"https://avatars.githubusercontent.com/u/8175517?v=4"},"body":"On Tue, Dec 24 2019, Junio C Hamano wrote:\n> Hans Jerry Illikainen <hji@dyntopia.com> writes:\n>\n>> +\t\t\t\t/* Do we have trust level? */\n>> +\t\t\t\tif (sigcheck_gpg_status[i].flags & GPG_STATUS_TRUST_LEVEL) {\n>> +\t\t\t\t\t/*\n>> +\t\t\t\t\t * GPG v1 and v2 differs in how the\n>> +\t\t\t\t\t * TRUST_ lines are written.  Some\n>> +\t\t\t\t\t * trust lines contain no additional\n>> +\t\t\t\t\t * space-separated information for v1.\n>> +\t\t\t\t\t */\n>> +\t\t\t\t\tnext = strchr(line, ' ');\n>> +\t\t\t\t\tif (!next)\n>> +\t\t\t\t\t\tnext = strchrnul(line, '\\n');\n>> +\t\t\t\t\ttrust = xmemdupz(line, next - line);\n>\n> I wonder if telling strcspn() to stop at either SP or LF is more in\n> line with the existing codebase [*1*] and/or more readable.  It\n> would make this part to:\n>\n> \t\tsize_t trust_size = strcspn(line, \" \\n\");\n> \t\ttrust = xmemdupz(line, trust_size);\n>\n> without the need to use or update the 'next' variable, if I am not\n> mistaken?\n\nI agree; fixed in v3.\n\n> By the way, while we are looking at this patch, I notice that,\n> throughout the function, the use of variable 'next' feels rather\n> misleading, at least to me.\n>\n> [...]\n>\n> I wonder if the code becomes less misleading if we either (1)\n> renamed 'next' to a name that hints more strongly that it is not the\n> 'next' line but the end of the current token we are interested in,\n> or (2) get rid of the pointer and instead counted size of the\n> current token we are interested in, or perhaps both?  \n\nYeah the name 'next' does seem a bit counter-intuitive when used in\nrelation to 'line'.  Looking through the function it seems that both (1)\nand (2) would work.\n\n> This is not the fault of this patch, but I just mention it before I\n> forget.\n>\n> Thanks.\n\nThanks for the review!\n\n-- \nhji\n"},{"id":"389010","messageId":"xmqq7e2hjtpe.fsf@gitster-ct.c.googlers.com","threadId":"52467","inReplyTo":"87r20pkhir.hji@dyntopia.com","subject":"Re: [PATCH v2 1/1] gpg-interface: add minTrustLevel as a configuration option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2019-12-27T22:21:17Z","receivedAt":"2019-12-27T22:21:24Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Hans Jerry Illikainen <hji@dyntopia.com> writes:\n\n>> I wonder if the code becomes less misleading if we either (1)\n>> renamed 'next' to a name that hints more strongly that it is not the\n>> 'next' line but the end of the current token we are interested in,\n>> or (2) get rid of the pointer and instead counted size of the\n>> current token we are interested in, or perhaps both?  \n>\n> Yeah the name 'next' does seem a bit counter-intuitive when used in\n> relation to 'line'.  Looking through the function it seems that both (1)\n> and (2) would work.\n\nThanks for thinking the code a bit more than necessary for the\npurpose of this topic.  Let's leave such a clean-up outside the\nscope of this topic, but perhaps a #leftoverbits marker may help us\nremember it as something we could do when we have nothing else\nbetter to do ;-)\n\n"}]}