diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc index 85ed7a727038bbe501f3780dced200a51fe9ea28..b74179a6c891d5ad72ef34b89e47de8cdf53b100 100644 --- a/Documentation/git-fast-import.adoc +++ b/Documentation/git-fast-import.adoc @@ -66,6 +66,11 @@ fast-import stream! This option is enabled automatically for remote-helpers that use the `import` capability, as they are already trusted to run their own code. +--signed-tags=(verbatim|warn-verbatim|warn-strip|strip|abort):: + Specify how to handle signed tags. Behaves in the same way + as the same option in linkgit:git-fast-export[1], except that + default is 'verbatim' (instead of 'abort'). + --signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort):: Specify how to handle signed commits. Behaves in the same way as the same option in linkgit:git-fast-export[1], except that diff --git a/Documentation/git-tag.adoc b/Documentation/git-tag.adoc index a4b1c0ec05a6c956d90016ac526358b8b2604c65..3519e5b9b232e0b5be6bb5e1f02f0f99dcac79ce 100644 --- a/Documentation/git-tag.adoc +++ b/Documentation/git-tag.adoc @@ -3,7 +3,7 @@ git-tag(1) NAME ---- -git-tag - Create, list, delete or verify a tag object signed with GPG +git-tag - Create, list, delete or verify tags SYNOPSIS @@ -38,15 +38,17 @@ and `-a`, `-s`, and `-u ` are absent, `-a` is implied. Otherwise, a tag reference that points directly at the given object (i.e., a lightweight tag) is created. -A GnuPG signed tag object will be created when `-s` or `-u -` is used. When `-u ` is not used, the -committer identity for the current user is used to find the -GnuPG key for signing. The configuration variable `gpg.program` -is used to specify custom GnuPG binary. +A cryptographically signed tag object will be created when `-s` or +`-u ` is used. The signing backend (GPG, X.509, SSH, etc.) is +controlled by the `gpg.format` configuration variable, defaulting to +OpenPGP. When `-u ` is not used, the committer identity for +the current user is used to find the key for signing. The +configuration variable `gpg.program` is used to specify a custom +signing binary. Tag objects (created with `-a`, `-s`, or `-u`) are called "annotated" tags; they contain a creation date, the tagger name and e-mail, a -tagging message, and an optional GnuPG signature. Whereas a +tagging message, and an optional cryptographic signature. Whereas a "lightweight" tag is simply a name for an object (usually a commit object). @@ -64,10 +66,12 @@ OPTIONS -s:: --sign:: - Make a GPG-signed tag, using the default e-mail address's key. - The default behavior of tag GPG-signing is controlled by `tag.gpgSign` - configuration variable if it exists, or disabled otherwise. - See linkgit:git-config[1]. + Make a cryptographically signed tag, using the default signing + key. The signing backend used depends on the `gpg.format` + configuration variable. The default key is determined by the + backend. For GPG, it's based on the committer's email address, + while for SSH it may be a specific key file or agent + identity. See linkgit:git-config[1]. --no-sign:: Override `tag.gpgSign` configuration variable that is @@ -75,7 +79,10 @@ OPTIONS -u :: --local-user=:: - Make a GPG-signed tag, using the given key. + Make a cryptographically signed tag using the given key. The + format of the and the backend used depend on the + `gpg.format` configuration variable. See + linkgit:git-config[1]. -f:: --force:: @@ -87,7 +94,7 @@ OPTIONS -v:: --verify:: - Verify the GPG signature of the given tag names. + Verify the signature of the given tag names. -n:: specifies how many lines from the annotation, if any, @@ -236,12 +243,23 @@ it in the repository configuration as follows: ------------------------------------- [user] - signingKey = + signingKey = ------------------------------------- +The signing backend can be chosen via the `gpg.format` configuration +variable, which defaults to `openpgp`. See linkgit:git-config[1] +for a list of other supported formats. + +The path to the program used for each signing backend can be specified +with the `gpg..program` configuration variable. For the +`openpgp` backend, `gpg.program` can be used as a synonym for +`gpg.openpgp.program`. See linkgit:git-config[1] for details. + `pager.tag` is only respected when listing tags, i.e., when `-l` is used or implied. The default is to use a pager. -See linkgit:git-config[1]. + +See linkgit:git-config[1] for more details and other configuration +variables. DISCUSSION ---------- diff --git a/builtin/fast-export.c b/builtin/fast-export.c index dc2486f9a83a9b00632c68148b5f2277c58cfa74..7adbc55f0dccb1878071aeb8af45f9b39ce3bf4c 100644 --- a/builtin/fast-export.c +++ b/builtin/fast-export.c @@ -931,9 +931,8 @@ static void handle_tag(const char *name, struct tag *tag) /* handle signed tags */ if (message) { - const char *signature = strstr(message, - "\n-----BEGIN PGP SIGNATURE-----\n"); - if (signature) + size_t sig_offset = parse_signed_buffer(message, message_size); + if (sig_offset < message_size) switch (signed_tag_mode) { case SIGN_ABORT: die("encountered signed tag %s; use " @@ -950,7 +949,7 @@ static void handle_tag(const char *name, struct tag *tag) oid_to_hex(&tag->object.oid)); /* fallthru */ case SIGN_STRIP: - message_size = signature + 1 - message; + message_size = sig_offset; break; } } diff --git a/builtin/fast-import.c b/builtin/fast-import.c index 2010e78475b32e2090336ae96a27af4786ca2a2e..60d6faa46550e588df0708fc0d3e2d182c4f9e4e 100644 --- a/builtin/fast-import.c +++ b/builtin/fast-import.c @@ -188,6 +188,7 @@ static int global_argc; static const char **global_argv; static const char *global_prefix; +static enum sign_mode signed_tag_mode = SIGN_VERBATIM; static enum sign_mode signed_commit_mode = SIGN_VERBATIM; /* Memory pools */ @@ -2961,6 +2962,43 @@ static void parse_new_commit(const char *arg) b->last_commit = object_count_by_type[OBJ_COMMIT]; } +static void handle_tag_signature(struct strbuf *msg, const char *name) +{ + size_t sig_offset = parse_signed_buffer(msg->buf, msg->len); + + /* If there is no signature, there is nothing to do. */ + if (sig_offset >= msg->len) + return; + + switch (signed_tag_mode) { + + /* First, modes that don't change anything */ + case SIGN_ABORT: + die(_("encountered signed tag; use " + "--signed-tags= to handle it")); + case SIGN_WARN_VERBATIM: + warning(_("importing a tag signature verbatim for tag '%s'"), name); + /* fallthru */ + case SIGN_VERBATIM: + /* Nothing to do, the signature will be put into the imported tag. */ + break; + + /* Second, modes that remove the signature */ + case SIGN_WARN_STRIP: + warning(_("stripping a tag signature for tag '%s'"), name); + /* fallthru */ + case SIGN_STRIP: + /* Truncate the buffer to remove the signature */ + strbuf_setlen(msg, sig_offset); + break; + + /* Third, BUG */ + default: + BUG("invalid signed_tag_mode value %d from tag '%s'", + signed_tag_mode, name); + } +} + static void parse_new_tag(const char *arg) { static struct strbuf msg = STRBUF_INIT; @@ -3024,6 +3062,8 @@ static void parse_new_tag(const char *arg) /* tag payload/message */ parse_data(&msg, 0, NULL); + handle_tag_signature(&msg, t->name); + /* build the tag object */ strbuf_reset(&new_data); @@ -3544,6 +3584,9 @@ static int parse_one_option(const char *option) } else if (skip_prefix(option, "signed-commits=", &option)) { if (parse_sign_mode(option, &signed_commit_mode)) usagef(_("unknown --signed-commits mode '%s'"), option); + } else if (skip_prefix(option, "signed-tags=", &option)) { + if (parse_sign_mode(option, &signed_tag_mode)) + usagef(_("unknown --signed-tags mode '%s'"), option); } else if (!strcmp(option, "quiet")) { show_stats = 0; quiet = 1; diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh index 937b876bd05281c7ab6830d5930c63a1b1f906fe..b99ae39a06b68395e3288c15d494f6637c724cd4 100644 --- a/t/lib-gpg.sh +++ b/t/lib-gpg.sh @@ -9,6 +9,16 @@ GNUPGHOME="$(pwd)/gpghome" export GNUPGHOME +# All the "test_lazy_prereq GPG*" below should use +# `prepare_gnupghome()` either directly or through a call to +# `test_have_prereq GPG*`. That's because `gpg` and `gpgsm` +# only create the directory specified using "$GNUPGHOME" or +# `--homedir` if it's the default (usually "~/.gnupg"). +prepare_gnupghome() { + mkdir -p "$GNUPGHOME" && + chmod 0700 "$GNUPGHOME" +} + test_lazy_prereq GPG ' gpg_version=$(gpg --version 2>&1) test $? != 127 || exit 1 @@ -38,8 +48,7 @@ test_lazy_prereq GPG ' # To export ownertrust: # gpg --homedir /tmp/gpghome --export-ownertrust \ # > lib-gpg/ownertrust - mkdir "$GNUPGHOME" && - chmod 0700 "$GNUPGHOME" && + prepare_gnupghome && (gpgconf --kill all || : ) && gpg --homedir "${GNUPGHOME}" --import \ "$TEST_DIRECTORY"/lib-gpg/keyring.gpg && @@ -63,6 +72,14 @@ test_lazy_prereq GPG2 ' ;; *) (gpgconf --kill all || : ) && + + # NEEDSWORK: prepare_gnupghome() should definitely be + # called here, but it looks like it exposes a + # pre-existing, hidden bug by allowing some tests in + # t1016-compatObjectFormat.sh to run instead of being + # skipped. See: + # https://lore.kernel.org/git/ZoV8b2RvYxLOotSJ@teonanacatl.net/ + gpg --homedir "${GNUPGHOME}" --import \ "$TEST_DIRECTORY"/lib-gpg/keyring.gpg && gpg --homedir "${GNUPGHOME}" --import-ownertrust \ @@ -132,8 +149,7 @@ test_lazy_prereq GPGSSH ' test $? = 0 || exit 1; # Setup some keys and an allowed signers file - mkdir -p "${GNUPGHOME}" && - chmod 0700 "${GNUPGHOME}" && + prepare_gnupghome && (setfacl -k "${GNUPGHOME}" 2>/dev/null || true) && ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_PRIMARY}" >/dev/null && ssh-keygen -t rsa -b 2048 -N "" -C "git rsa2048 key" -f "${GPGSSH_KEY_SECONDARY}" >/dev/null && diff --git a/t/meson.build b/t/meson.build index 11376b9e256dd610cda42c045408d08ea7b44950..cb8c2b4b30b06aaf508971cc8ee56b718a39f008 100644 --- a/t/meson.build +++ b/t/meson.build @@ -1036,6 +1036,7 @@ integration_tests = [ 't9303-fast-import-compression.sh', 't9304-fast-import-marks.sh', 't9305-fast-import-signatures.sh', + 't9306-fast-import-signed-tags.sh', 't9350-fast-export.sh', 't9351-fast-export-anonymize.sh', 't9400-git-cvsserver-server.sh', diff --git a/t/t9306-fast-import-signed-tags.sh b/t/t9306-fast-import-signed-tags.sh new file mode 100755 index 0000000000000000000000000000000000000000..363619e7d1abf13574d825a61462bc1017bfc8f1 --- /dev/null +++ b/t/t9306-fast-import-signed-tags.sh @@ -0,0 +1,80 @@ +#!/bin/sh + +test_description='git fast-import --signed-tags=' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main + +. ./test-lib.sh +. "$TEST_DIRECTORY/lib-gpg.sh" + +test_expect_success 'set up unsigned initial commit and import repo' ' + test_commit first && + git init new +' + +test_expect_success 'import no signed tag with --signed-tags=abort' ' + git fast-export --signed-tags=verbatim >output && + git -C new fast-import --quiet --signed-tags=abort output +' + +test_expect_success GPG 'import OpenPGP signed tag with --signed-tags=abort' ' + test_must_fail git -C new fast-import --quiet --signed-tags=abort log 2>&1 && + IMPORTED=$(git -C new rev-parse --verify refs/tags/openpgp-signed) && + test $OPENPGP_SIGNED = $IMPORTED && + test_must_be_empty log +' + +test_expect_success GPGSM 'setup X.509 signed tag' ' + test_config gpg.format x509 && + test_config user.signingkey $GIT_COMMITTER_EMAIL && + + git tag -s -m "X.509 signed tag" x509-signed first && + X509_SIGNED=$(git rev-parse --verify refs/tags/x509-signed) && + git fast-export --signed-tags=verbatim x509-signed >output +' + +test_expect_success GPGSM 'import X.509 signed tag with --signed-tags=warn-strip' ' + git -C new fast-import --quiet --signed-tags=warn-strip log 2>&1 && + test_grep "stripping a tag signature for tag '\''x509-signed'\''" log && + IMPORTED=$(git -C new rev-parse --verify refs/tags/x509-signed) && + test $X509_SIGNED != $IMPORTED && + git -C new cat-file -p x509-signed >out && + test_grep ! "SIGNED MESSAGE" out +' + +test_expect_success GPGSSH 'setup SSH signed tag' ' + test_config gpg.format ssh && + test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" && + + git tag -s -m "SSH signed tag" ssh-signed first && + SSH_SIGNED=$(git rev-parse --verify refs/tags/ssh-signed) && + git fast-export --signed-tags=verbatim ssh-signed >output +' + +test_expect_success GPGSSH 'import SSH signed tag with --signed-tags=warn-verbatim' ' + git -C new fast-import --quiet --signed-tags=warn-verbatim log 2>&1 && + test_grep "importing a tag signature verbatim for tag '\''ssh-signed'\''" log && + IMPORTED=$(git -C new rev-parse --verify refs/tags/ssh-signed) && + test $SSH_SIGNED = $IMPORTED +' + +test_expect_success GPGSSH 'import SSH signed tag with --signed-tags=strip' ' + git -C new fast-import --quiet --signed-tags=strip log 2>&1 && + test_must_be_empty log && + IMPORTED=$(git -C new rev-parse --verify refs/tags/ssh-signed) && + test $SSH_SIGNED != $IMPORTED && + git -C new cat-file -p ssh-signed >out && + test_grep ! "SSH SIGNATURE" out +' + +test_done diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh index 8f85c69d62f17a4d72c112cfbe33bd2ac2037ba0..3d153a4805bbfc22c561c8526b449c6297e86669 100755 --- a/t/t9350-fast-export.sh +++ b/t/t9350-fast-export.sh @@ -35,6 +35,7 @@ test_expect_success 'setup' ' git commit -m sitzt file2 && test_tick && git tag -a -m valentin muss && + ANNOTATED_TAG_COUNT=1 && git merge -s ours main ' @@ -229,7 +230,8 @@ EOF test_expect_success 'set up faked signed tag' ' - git fast-import output && + test_grep "SIGNED MESSAGE" output +' + +test_expect_success GPGSM 'signed-tags=strip with X.509' ' + git fast-export --signed-tags=strip x509-signed > output && + test_grep ! "SIGNED MESSAGE" output +' + +test_expect_success GPGSSH 'setup SSH signed tag' ' + test_config gpg.format ssh && + test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" && + + git tag -s -m "SSH signed tag" ssh-signed $(git rev-parse HEAD) && + ANNOTATED_TAG_COUNT=$((ANNOTATED_TAG_COUNT + 1)) +' + +test_expect_success GPGSSH 'signed-tags=verbatim with SSH' ' + git fast-export --signed-tags=verbatim ssh-signed > output && + test_grep "SSH SIGNATURE" output +' + +test_expect_success GPGSSH 'signed-tags=strip with SSH' ' + git fast-export --signed-tags=strip ssh-signed > output && + test_grep ! "SSH SIGNATURE" output +' + test_expect_success GPG 'set up signed commit' ' # Generate a commit with both "gpgsig" and "encoding" set, so @@ -491,8 +529,9 @@ test_expect_success 'fast-export -C -C | fast-import' ' test_expect_success 'fast-export | fast-import when main is tagged' ' git tag -m msg last && + ANNOTATED_TAG_COUNT=$((ANNOTATED_TAG_COUNT + 1)) && git fast-export -C -C --signed-tags=strip --all > output && - test $(grep -c "^tag " output) = 3 + test $(grep -c "^tag " output) = $ANNOTATED_TAG_COUNT ' @@ -506,12 +545,13 @@ test_expect_success 'cope with tagger-less tags' ' TAG=$(git hash-object --literally -t tag -w tag-content) && git update-ref refs/tags/sonnenschein $TAG && + ANNOTATED_TAG_COUNT=$((ANNOTATED_TAG_COUNT + 1)) && git fast-export -C -C --signed-tags=strip --all > output && - test $(grep -c "^tag " output) = 4 && + test $(grep -c "^tag " output) = $ANNOTATED_TAG_COUNT && ! grep "Unspecified Tagger" output && git fast-export -C -C --signed-tags=strip --all \ --fake-missing-tagger > output && - test $(grep -c "^tag " output) = 4 && + test $(grep -c "^tag " output) = $ANNOTATED_TAG_COUNT && grep "Unspecified Tagger" output '