Merge branch 'hs/gpgsm'

Teach "git tag -s" etc. a few configuration variables (gpg.format
that can be set to "openpgp" or "x509", and gpg.<format>.program
that is used to specify what program to use to deal with the format)
to allow x.509 certs with CMS via "gpgsm" to be used instead of
openpgp via "gnupg".

* hs/gpgsm:
  gpg-interface t: extend the existing GPG tests with GPGSM
  gpg-interface: introduce new signature format "x509" using gpgsm
  gpg-interface: introduce new config to select per gpg format program
  gpg-interface: do not hardcode the key string len anymore
  gpg-interface: introduce an abstraction for multiple gpg formats
  t/t7510: check the validation of the new config gpg.format
  gpg-interface: add new config to select how to sign a commit
This commit is contained in:
Junio C Hamano 2018-08-15 15:08:23 -07:00
commit 3ec5ebee15
10 changed files with 285 additions and 23 deletions

View File

@ -1884,6 +1884,16 @@ 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" and another possible value is "x509".
gpg.<format>.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`. The default
value for `gpg.x509.program` is "gpgsm".
gui.commitMsgWidth::
Defines how wide the commit message window is in the
linkgit:git-gui[1]. "75" is the default.

View File

@ -7,10 +7,64 @@
#include "tempfile.h"
static char *configured_signing_key;
static const char *gpg_program = "gpg";
struct gpg_format {
const char *name;
const char *program;
const char **verify_args;
const char **sigs;
};
#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----"
#define PGP_MESSAGE "-----BEGIN PGP MESSAGE-----"
static const char *openpgp_verify_args[] = {
"--keyid-format=long",
NULL
};
static const char *openpgp_sigs[] = {
"-----BEGIN PGP SIGNATURE-----",
"-----BEGIN PGP MESSAGE-----",
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];
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)
{
@ -53,10 +107,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);
}
@ -101,12 +156,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;
@ -114,7 +163,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);
@ -131,6 +180,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);
@ -138,13 +190,28 @@ int git_gpg_config(const char *var, const char *value, void *cb)
return 0;
}
if (!strcmp(var, "gpg.program")) {
if (!strcmp(var, "gpg.format")) {
if (!value)
return config_error_nonbool(var);
gpg_program = xstrdup(value);
fmt = get_format_by_name(value);
if (!fmt)
return error("unsupported value for %s: %s",
var, value);
use_format = fmt;
return 0;
}
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);
}
return 0;
}
@ -163,7 +230,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);
@ -201,6 +268,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;
@ -216,10 +284,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);

View File

@ -38,7 +38,33 @@ then
"$TEST_DIRECTORY"/lib-gpg/ownertrust &&
gpg --homedir "${GNUPGHOME}" </dev/null >/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

View File

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

BIN
t/lib-gpg/gpgsm_cert.p12 Normal file

Binary file not shown.

View File

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

View File

@ -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 <<E_O_F
SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}
KEY=${GIT_PUSH_CERT_KEY-nokey}
STATUS=${GIT_PUSH_CERT_STATUS-nostatus}
NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}
NONCE=${GIT_PUSH_CERT_NONCE-nononce}
E_O_F
EOF
test_config user.email hasnokey@nowhere.com &&
test_config user.signingkey "" &&
(
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 &&
(
cat <<-\EOF &&
SIGNER=/CN=C O Mitter/O=Example/SN=C O/GN=Mitter
KEY=
STATUS=G
NONCE_STATUS=OK
EOF
sed -n -e "s/^nonce /NONCE=/p" -e "/^$/q" dst/push-cert
) >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

View File

@ -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:

View File

@ -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.1 >expect.stderr &&
git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&
grep "^.GNUPG:." <actual.stderr.1 >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

View File

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