From 57a8dd75df4e1d716d60fc95bd0acaab45465d68 Mon Sep 17 00:00:00 2001 From: Henning Schild Date: Tue, 17 Jul 2018 14:50:07 +0200 Subject: gpg-interface: add new config to select how to sign a commit Add "gpg.format" where the user can specify which type of signature to use for commits. At the moment only "openpgp" is supported and the value is not even used. This commit prepares for a new types of signatures. Signed-off-by: Henning Schild Signed-off-by: Junio C Hamano diff --git a/Documentation/config.txt b/Documentation/config.txt index 1cc18a8..ac373e3 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -1828,6 +1828,10 @@ gpg.program:: signed, and the program is expected to send the result to its standard output. +gpg.format:: + Specifies which key format to use when signing with `--gpg-sign`. + Default is "openpgp", that is also the only supported value. + gui.commitMsgWidth:: Defines how wide the commit message window is in the linkgit:git-gui[1]. "75" is the default. diff --git a/gpg-interface.c b/gpg-interface.c index 09ddfbc..b39a279 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -7,6 +7,7 @@ #include "tempfile.h" static char *configured_signing_key; +static const char *gpg_format = "openpgp"; static const char *gpg_program = "gpg"; #define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----" @@ -138,6 +139,15 @@ int git_gpg_config(const char *var, const char *value, void *cb) return 0; } + if (!strcmp(var, "gpg.format")) { + if (!value) + return config_error_nonbool(var); + if (strcmp(value, "openpgp")) + return error("unsupported value for %s: %s", + var, value); + return git_config_string(&gpg_format, var, value); + } + if (!strcmp(var, "gpg.program")) { if (!value) return config_error_nonbool(var); -- cgit v0.10.2-6-g49f6 From 1865a647c3d8bb6367be4b799dd95217ccfbe26e Mon Sep 17 00:00:00 2001 From: Henning Schild Date: Wed, 18 Jul 2018 11:30:10 +0200 Subject: t/t7510: check the validation of the new config gpg.format Test setting gpg.format to both invalid and valid values. Signed-off-by: Henning Schild Signed-off-by: Junio C Hamano diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh index 6e2015e..4e37ff8 100755 --- a/t/t7510-signed-commit.sh +++ b/t/t7510-signed-commit.sh @@ -227,4 +227,11 @@ test_expect_success GPG 'log.showsignature behaves like --show-signature' ' grep "gpg: Good signature" actual ' +test_expect_success GPG 'check config gpg.format values' ' + test_config gpg.format openpgp && + git commit -S --amend -m "success" && + test_config gpg.format OpEnPgP && + test_must_fail git commit -S --amend -m "fail" +' + test_done -- cgit v0.10.2-6-g49f6 From 58af57e1c886a7c9fed7fb35f2dd8dd14cc5e4e0 Mon Sep 17 00:00:00 2001 From: Henning Schild Date: Tue, 17 Jul 2018 14:50:09 +0200 Subject: gpg-interface: introduce an abstraction for multiple gpg formats Create a struct that holds the format details for the supported formats. At the moment that is still just "openpgp". This commit prepares for the introduction of more formats, that might use other programs and match other signatures. Signed-off-by: Henning Schild Signed-off-by: Junio C Hamano diff --git a/gpg-interface.c b/gpg-interface.c index b39a279..a02db76 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -7,11 +7,52 @@ #include "tempfile.h" static char *configured_signing_key; -static const char *gpg_format = "openpgp"; -static const char *gpg_program = "gpg"; +struct gpg_format { + const char *name; + const char *program; + const char **verify_args; + const char **sigs; +}; + +static const char *openpgp_verify_args[] = { + "--keyid-format=long", + NULL +}; +static const char *openpgp_sigs[] = { + "-----BEGIN PGP SIGNATURE-----", + "-----BEGIN PGP MESSAGE-----", + NULL +}; + +static struct gpg_format gpg_format[] = { + { .name = "openpgp", .program = "gpg", + .verify_args = openpgp_verify_args, + .sigs = openpgp_sigs + }, +}; + +static struct gpg_format *use_format = &gpg_format[0]; -#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----" -#define PGP_MESSAGE "-----BEGIN PGP MESSAGE-----" +static struct gpg_format *get_format_by_name(const char *str) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(gpg_format); i++) + if (!strcmp(gpg_format[i].name, str)) + return gpg_format + i; + return NULL; +} + +static struct gpg_format *get_format_by_sig(const char *sig) +{ + int i, j; + + for (i = 0; i < ARRAY_SIZE(gpg_format); i++) + for (j = 0; gpg_format[i].sigs[j]; j++) + if (starts_with(sig, gpg_format[i].sigs[j])) + return gpg_format + i; + return NULL; +} void signature_check_clear(struct signature_check *sigc) { @@ -102,12 +143,6 @@ void print_signature_buffer(const struct signature_check *sigc, unsigned flags) fputs(output, stderr); } -static int is_gpg_start(const char *line) -{ - return starts_with(line, PGP_SIGNATURE) || - starts_with(line, PGP_MESSAGE); -} - size_t parse_signature(const char *buf, size_t size) { size_t len = 0; @@ -115,7 +150,7 @@ size_t parse_signature(const char *buf, size_t size) while (len < size) { const char *eol; - if (is_gpg_start(buf + len)) + if (get_format_by_sig(buf + len)) match = len; eol = memchr(buf + len, '\n', size - len); @@ -132,6 +167,9 @@ void set_signing_key(const char *key) int git_gpg_config(const char *var, const char *value, void *cb) { + struct gpg_format *fmt = NULL; + char *fmtname = NULL; + if (!strcmp(var, "user.signingkey")) { if (!value) return config_error_nonbool(var); @@ -142,17 +180,20 @@ int git_gpg_config(const char *var, const char *value, void *cb) if (!strcmp(var, "gpg.format")) { if (!value) return config_error_nonbool(var); - if (strcmp(value, "openpgp")) + fmt = get_format_by_name(value); + if (!fmt) return error("unsupported value for %s: %s", var, value); - return git_config_string(&gpg_format, var, value); + use_format = fmt; + return 0; } - if (!strcmp(var, "gpg.program")) { - if (!value) - return config_error_nonbool(var); - gpg_program = xstrdup(value); - return 0; + if (!strcmp(var, "gpg.program")) + fmtname = "openpgp"; + + if (fmtname) { + fmt = get_format_by_name(fmtname); + return git_config_string(&fmt->program, var, value); } return 0; @@ -173,7 +214,7 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig struct strbuf gpg_status = STRBUF_INIT; argv_array_pushl(&gpg.args, - gpg_program, + use_format->program, "--status-fd=2", "-bsau", signing_key, NULL); @@ -211,6 +252,7 @@ int verify_signed_buffer(const char *payload, size_t payload_size, struct strbuf *gpg_output, struct strbuf *gpg_status) { struct child_process gpg = CHILD_PROCESS_INIT; + struct gpg_format *fmt; struct tempfile *temp; int ret; struct strbuf buf = STRBUF_INIT; @@ -226,10 +268,14 @@ int verify_signed_buffer(const char *payload, size_t payload_size, return -1; } + fmt = get_format_by_sig(signature); + if (!fmt) + BUG("bad signature '%s'", signature); + + argv_array_push(&gpg.args, fmt->program); + argv_array_pushv(&gpg.args, fmt->verify_args); argv_array_pushl(&gpg.args, - gpg_program, "--status-fd=1", - "--keyid-format=long", "--verify", temp->filename.buf, "-", NULL); -- cgit v0.10.2-6-g49f6 From 42149d7f4b1424b29c0bae4f564d5f50a856bcea Mon Sep 17 00:00:00 2001 From: Henning Schild Date: Tue, 17 Jul 2018 14:50:10 +0200 Subject: gpg-interface: do not hardcode the key string len anymore gnupg does print the keyid followed by a space and the signer comes next. The same pattern is also used in gpgsm, but there the key length would be 40 instead of 16. Instead of hardcoding the expected length, find the first space and calculate it. Input that does not match the expected format will be ignored now, before we jumped to found+17 which might have been behind the end of an unexpected string. Signed-off-by: Henning Schild Signed-off-by: Junio C Hamano diff --git a/gpg-interface.c b/gpg-interface.c index a02db76..51cad90 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -95,10 +95,11 @@ static void parse_gpg_output(struct signature_check *sigc) sigc->result = sigcheck_gpg_status[i].result; /* The trust messages are not followed by key/signer information */ if (sigc->result != 'U') { - sigc->key = xmemdupz(found, 16); + next = strchrnul(found, ' '); + sigc->key = xmemdupz(found, next - found); /* The ERRSIG message is not followed by signer information */ - if (sigc-> result != 'E') { - found += 17; + if (*next && sigc-> result != 'E') { + found = next + 1; next = strchrnul(found, '\n'); sigc->signer = xmemdupz(found, next - found); } -- cgit v0.10.2-6-g49f6 From b02f51b196deec70867d9616872e533e2633473c Mon Sep 17 00:00:00 2001 From: Henning Schild Date: Tue, 17 Jul 2018 14:50:11 +0200 Subject: gpg-interface: introduce new config to select per gpg format program Supporting multiple signing formats we will have the need to configure a custom program each. Add a new config value to cater for that. Signed-off-by: Henning Schild Signed-off-by: Junio C Hamano diff --git a/Documentation/config.txt b/Documentation/config.txt index ac373e3..0e87134 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -1832,6 +1832,11 @@ gpg.format:: Specifies which key format to use when signing with `--gpg-sign`. Default is "openpgp", that is also the only supported value. +gpg..program:: + Use this to customize the program used for the signing format you + chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still + be used as a legacy synonym for `gpg.openpgp.program`. + gui.commitMsgWidth:: Defines how wide the commit message window is in the linkgit:git-gui[1]. "75" is the default. diff --git a/gpg-interface.c b/gpg-interface.c index 51cad90..a158f08 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -189,7 +189,7 @@ int git_gpg_config(const char *var, const char *value, void *cb) return 0; } - if (!strcmp(var, "gpg.program")) + if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program")) fmtname = "openpgp"; if (fmtname) { -- cgit v0.10.2-6-g49f6 From 1e7adb97566bff7d3431ce64b8d0d854a6863ed5 Mon Sep 17 00:00:00 2001 From: Henning Schild Date: Tue, 17 Jul 2018 14:50:12 +0200 Subject: gpg-interface: introduce new signature format "x509" using gpgsm This commit allows git to create and check x509 type signatures using gpgsm. Signed-off-by: Henning Schild Signed-off-by: Junio C Hamano diff --git a/Documentation/config.txt b/Documentation/config.txt index 0e87134..ff1d4a7 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -1830,12 +1830,13 @@ gpg.program:: gpg.format:: Specifies which key format to use when signing with `--gpg-sign`. - Default is "openpgp", that is also the only supported value. + Default is "openpgp" and another possible value is "x509". gpg..program:: Use this to customize the program used for the signing format you chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still - be used as a legacy synonym for `gpg.openpgp.program`. + be used as a legacy synonym for `gpg.openpgp.program`. The default + value for `gpg.x509.program` is "gpgsm". gui.commitMsgWidth:: Defines how wide the commit message window is in the diff --git a/gpg-interface.c b/gpg-interface.c index a158f08..bb8ea66 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -24,11 +24,23 @@ static const char *openpgp_sigs[] = { NULL }; +static const char *x509_verify_args[] = { + NULL +}; +static const char *x509_sigs[] = { + "-----BEGIN SIGNED MESSAGE-----", + NULL +}; + static struct gpg_format gpg_format[] = { { .name = "openpgp", .program = "gpg", .verify_args = openpgp_verify_args, .sigs = openpgp_sigs }, + { .name = "x509", .program = "gpgsm", + .verify_args = x509_verify_args, + .sigs = x509_sigs + }, }; static struct gpg_format *use_format = &gpg_format[0]; @@ -192,6 +204,9 @@ int git_gpg_config(const char *var, const char *value, void *cb) if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program")) fmtname = "openpgp"; + if (!strcmp(var, "gpg.x509.program")) + fmtname = "x509"; + if (fmtname) { fmt = get_format_by_name(fmtname); return git_config_string(&fmt->program, var, value); -- cgit v0.10.2-6-g49f6 From 53fc999306af60213e18b3ffd4c0347e4a88af0a Mon Sep 17 00:00:00 2001 From: Henning Schild Date: Fri, 20 Jul 2018 10:28:07 +0200 Subject: gpg-interface t: extend the existing GPG tests with GPGSM Add test cases to cover the new X509/gpgsm support. Most of them resemble existing ones. They just switch the format to x509 and set the signingkey when creating signatures. Validation of signatures does not need any configuration of git, it does need gpgsm to be configured to trust the key(-chain). Several of the testcases build on top of existing gpg testcases. The commit ships a self-signed key for committer@example.com and configures gpgsm to trust it. Signed-off-by: Henning Schild Signed-off-by: Junio C Hamano diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh index a5d3b2c..3fe0287 100755 --- a/t/lib-gpg.sh +++ b/t/lib-gpg.sh @@ -38,7 +38,33 @@ then "$TEST_DIRECTORY"/lib-gpg/ownertrust && gpg --homedir "${GNUPGHOME}" /dev/null 2>&1 \ --sign -u committer@example.com && - test_set_prereq GPG + test_set_prereq GPG && + # Available key info: + # * see t/lib-gpg/gpgsm-gen-key.in + # To generate new certificate: + # * no passphrase + # gpgsm --homedir /tmp/gpghome/ \ + # -o /tmp/gpgsm.crt.user \ + # --generate-key \ + # --batch t/lib-gpg/gpgsm-gen-key.in + # To import certificate: + # gpgsm --homedir /tmp/gpghome/ \ + # --import /tmp/gpgsm.crt.user + # To export into a .p12 we can later import: + # gpgsm --homedir /tmp/gpghome/ \ + # -o t/lib-gpg/gpgsm_cert.p12 \ + # --export-secret-key-p12 "committer@example.com" + echo | gpgsm --homedir "${GNUPGHOME}" 2>/dev/null \ + --passphrase-fd 0 --pinentry-mode loopback \ + --import "$TEST_DIRECTORY"/lib-gpg/gpgsm_cert.p12 && + gpgsm --homedir "${GNUPGHOME}" 2>/dev/null -K \ + | grep fingerprint: | cut -d" " -f4 | tr -d '\n' > \ + ${GNUPGHOME}/trustlist.txt && + echo " S relax" >> ${GNUPGHOME}/trustlist.txt && + (gpgconf --kill gpg-agent >/dev/null 2>&1 || : ) && + echo hello | gpgsm --homedir "${GNUPGHOME}" >/dev/null \ + -u committer@example.com -o /dev/null --sign - 2>&1 && + test_set_prereq GPGSM ;; esac fi diff --git a/t/lib-gpg/gpgsm-gen-key.in b/t/lib-gpg/gpgsm-gen-key.in new file mode 100644 index 0000000..a7fd87c --- /dev/null +++ b/t/lib-gpg/gpgsm-gen-key.in @@ -0,0 +1,8 @@ +Key-Type: RSA +Key-Length: 2048 +Key-Usage: sign +Serial: random +Name-DN: CN=C O Mitter, O=Example, SN=C O, GN=Mitter +Name-Email: committer@example.com +Not-Before: 1970-01-01 00:00:00 +Not-After: 3000-01-01 00:00:00 diff --git a/t/lib-gpg/gpgsm_cert.p12 b/t/lib-gpg/gpgsm_cert.p12 new file mode 100644 index 0000000..94ffad0 Binary files /dev/null and b/t/lib-gpg/gpgsm_cert.p12 differ diff --git a/t/t4202-log.sh b/t/t4202-log.sh index 25b1f8c..05d3707 100755 --- a/t/t4202-log.sh +++ b/t/t4202-log.sh @@ -1556,12 +1556,28 @@ test_expect_success GPG 'setup signed branch' ' git commit -S -m signed_commit ' +test_expect_success GPGSM 'setup signed branch x509' ' + test_when_finished "git reset --hard && git checkout master" && + git checkout -b signed-x509 master && + echo foo >foo && + git add foo && + test_config gpg.format x509 && + test_config user.signingkey $GIT_COMMITTER_EMAIL && + git commit -S -m signed_commit +' + test_expect_success GPG 'log --graph --show-signature' ' git log --graph --show-signature -n1 signed >actual && grep "^| gpg: Signature made" actual && grep "^| gpg: Good signature" actual ' +test_expect_success GPGSM 'log --graph --show-signature x509' ' + git log --graph --show-signature -n1 signed-x509 >actual && + grep "^| gpgsm: Signature made" actual && + grep "^| gpgsm: Good signature" actual +' + test_expect_success GPG 'log --graph --show-signature for merged tag' ' test_when_finished "git reset --hard && git checkout master" && git checkout -b plain master && @@ -1581,6 +1597,27 @@ test_expect_success GPG 'log --graph --show-signature for merged tag' ' grep "^| | gpg: Good signature" actual ' +test_expect_success GPGSM 'log --graph --show-signature for merged tag x509' ' + test_when_finished "git reset --hard && git checkout master" && + test_config gpg.format x509 && + test_config user.signingkey $GIT_COMMITTER_EMAIL && + git checkout -b plain-x509 master && + echo aaa >bar && + git add bar && + git commit -m bar_commit && + git checkout -b tagged-x509 master && + echo bbb >baz && + git add baz && + git commit -m baz_commit && + git tag -s -m signed_tag_msg signed_tag_x509 && + git checkout plain-x509 && + git merge --no-ff -m msg signed_tag_x509 && + git log --graph --show-signature -n1 plain-x509 >actual && + grep "^|\\\ merged tag" actual && + grep "^| | gpgsm: Signature made" actual && + grep "^| | gpgsm: Good signature" actual +' + test_expect_success GPG '--no-show-signature overrides --show-signature' ' git log -1 --show-signature --no-show-signature signed >actual && ! grep "^gpg:" actual diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh index 1cea758..030331f 100755 --- a/t/t5534-push-signed.sh +++ b/t/t5534-push-signed.sh @@ -194,10 +194,12 @@ test_expect_success GPG 'fail without key and heed user.signingkey' ' EOF - unset GIT_COMMITTER_EMAIL && - git config user.email hasnokey@nowhere.com && - test_must_fail git push --signed dst noop ff +noff && - git config user.signingkey committer@example.com && + test_config user.email hasnokey@nowhere.com && + ( + sane_unset GIT_COMMITTER_EMAIL && + test_must_fail git push --signed dst noop ff +noff + ) && + test_config user.signingkey $GIT_COMMITTER_EMAIL && git push --signed dst noop ff +noff && ( @@ -218,4 +220,57 @@ test_expect_success GPG 'fail without key and heed user.signingkey' ' test_cmp expect dst/push-cert-status ' +test_expect_success GPGSM 'fail without key and heed user.signingkey x509' ' + test_config gpg.format x509 && + prepare_dst && + mkdir -p dst/.git/hooks && + git -C dst config receive.certnonceseed sekrit && + write_script dst/.git/hooks/post-receive <<-\EOF && + # discard the update list + cat >/dev/null + # record the push certificate + if test -n "${GIT_PUSH_CERT-}" + then + git cat-file blob $GIT_PUSH_CERT >../push-cert + fi && + + cat >../push-cert-status <expect.in && + key=$(cat "${GNUPGHOME}/trustlist.txt" | cut -d" " -f1 | tr -d ":") && + sed -e "s/^KEY=/KEY=${key}/" expect.in >expect && + + noop=$(git rev-parse noop) && + ff=$(git rev-parse ff) && + noff=$(git rev-parse noff) && + grep "$noop $ff refs/heads/ff" dst/push-cert && + grep "$noop $noff refs/heads/noff" dst/push-cert && + test_cmp expect dst/push-cert-status +' + test_done diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh index d7b319e..2147938 100755 --- a/t/t7004-tag.sh +++ b/t/t7004-tag.sh @@ -1354,6 +1354,19 @@ test_expect_success GPG \ 'test_config gpg.program echo && test_must_fail git tag -s -m tail tag-gpg-failure' +# try to sign with bad user.signingkey +test_expect_success GPGSM \ + 'git tag -s fails if gpgsm is misconfigured (bad key)' \ + 'test_config user.signingkey BobTheMouse && + test_config gpg.format x509 && + test_must_fail git tag -s -m tail tag-gpg-failure' + +# try to produce invalid signature +test_expect_success GPGSM \ + 'git tag -s fails if gpgsm is misconfigured (bad signature format)' \ + 'test_config gpg.x509.program echo && + test_config gpg.format x509 && + test_must_fail git tag -s -m tail tag-gpg-failure' # try to verify without gpg: diff --git a/t/t7030-verify-tag.sh b/t/t7030-verify-tag.sh index 291a1e2..99f35a5 100755 --- a/t/t7030-verify-tag.sh +++ b/t/t7030-verify-tag.sh @@ -41,6 +41,13 @@ test_expect_success GPG 'create signed tags' ' git tag -uB7227189 -m eighth eighth-signed-alt ' +test_expect_success GPGSM 'create signed tags x509 ' ' + test_config gpg.format x509 && + test_config user.signingkey $GIT_COMMITTER_EMAIL && + echo 9 >file && test_tick && git commit -a -m "nineth gpgsm-signed" && + git tag -s -m nineth nineth-signed-x509 +' + test_expect_success GPG 'verify and show signatures' ' ( for tag in initial second merge fourth-signed sixth-signed seventh-signed @@ -72,6 +79,13 @@ test_expect_success GPG 'verify and show signatures' ' ) ' +test_expect_success GPGSM 'verify and show signatures x509' ' + git verify-tag nineth-signed-x509 2>actual && + grep "Good signature from" actual && + ! grep "BAD signature from" actual && + echo nineth-signed-x509 OK +' + test_expect_success GPG 'detect fudged signature' ' git cat-file tag seventh-signed >raw && sed -e "/^tag / s/seventh/7th forged/" raw >forged1 && @@ -112,6 +126,13 @@ test_expect_success GPG 'verify signatures with --raw' ' ) ' +test_expect_success GPGSM 'verify signatures with --raw x509' ' + git verify-tag --raw nineth-signed-x509 2>actual && + grep "GOODSIG" actual && + ! grep "BADSIG" actual && + echo nineth-signed-x509 OK +' + test_expect_success GPG 'verify multiple tags' ' tags="fourth-signed sixth-signed seventh-signed" && for i in $tags @@ -125,6 +146,19 @@ test_expect_success GPG 'verify multiple tags' ' test_cmp expect.stderr actual.stderr ' +test_expect_success GPGSM 'verify multiple tags x509' ' + tags="seventh-signed nineth-signed-x509" && + for i in $tags + do + git verify-tag -v --raw $i || return 1 + done >expect.stdout 2>expect.stderr.1 && + grep "^.GNUPG:." expect.stderr && + git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 && + grep "^.GNUPG:." actual.stderr && + test_cmp expect.stdout actual.stdout && + test_cmp expect.stderr actual.stderr +' + test_expect_success GPG 'verifying tag with --format' ' cat >expect <<-\EOF && tagname : fourth-signed -- cgit v0.10.2-6-g49f6