In order to prevent a valid push certificate for pushing into an repository from getting replayed to push to an unrelated one, send a nonce string from the receive-pack process and have the signer include it in the push certificate. The original nonce is exported as GIT_PUSH_CERT_NONCE for the hooks to examine and match against the value on the "nonce" header in the certificate to notice a replay. Signed-off-by: Junio C Hamano <gitster@xxxxxxxxx> --- Documentation/git-receive-pack.txt | 8 ++++++++ Documentation/technical/pack-protocol.txt | 6 ++++++ Documentation/technical/protocol-capabilities.txt | 7 ++++--- builtin/receive-pack.c | 21 +++++++++++++++++++-- send-pack.c | 19 +++++++++++++++---- t/t5534-push-signed.sh | 17 +++++++++++------ 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt index 60151a6..983b24e 100644 --- a/Documentation/git-receive-pack.txt +++ b/Documentation/git-receive-pack.txt @@ -72,6 +72,13 @@ GIT_PUSH_CERT_STATUS:: using the same mnemonic as used in `%G?` format of `git log` family of commands (see linkgit:git-log[1]). +GIT_PUSH_CERT_NONCE:: + The nonce string the process asked the signer to include + in the push certificate. If this does not match the value + recorded on the "nonce" header in the push certificate, it + may indicate that the certificate is a valid one that is + being replayed from a separate "git push" session. + This hook is called before any refname is updated and before any fast-forward checks are performed. @@ -147,6 +154,7 @@ service: if test -n "${GIT_PUSH_CERT-}" && test ${GIT_PUSH_CERT_STATUS} = G then ( + echo expected nonce is ${GIT_PUSH_NONCE} git cat-file blob ${GIT_PUSH_CERT} ) | mail -s "push from $GIT_PUSH_CERT_SIGNER" push-log@mydomain fi diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt index 7b543dc..dda1206 100644 --- a/Documentation/technical/pack-protocol.txt +++ b/Documentation/technical/pack-protocol.txt @@ -485,6 +485,7 @@ references. PKT-LINE("certificate version 0.1" LF) PKT-LINE("pusher" SP ident LF) PKT-LINE("pushee" SP url LF) + PKT-LINE("nonce" SP nonce LF) PKT-LINE(LF) *PKT-LINE(command LF) *PKT-LINE(gpg-signature-lines LF) @@ -533,6 +534,11 @@ Currently, the following header fields are defined: authentication material) the user who ran `git push` intended to push into. +`nonce` nonce:: + The 'nonce' string the receiving repository asked the + pushing user to include in the certificate, to prevent + replay attacks. + The GPG signature lines are a detached signature for the contents recorded in the push certificate before the signature block begins. The detached signature is used to certify that the commands were diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt index a478cc4..0c92dee 100644 --- a/Documentation/technical/protocol-capabilities.txt +++ b/Documentation/technical/protocol-capabilities.txt @@ -251,10 +251,11 @@ If the upload-pack server advertises this capability, fetch-pack may send "want" lines with SHA-1s that exist at the server but are not advertised by upload-pack. -push-cert ---------- +push-cert=<nonce> +----------------- The receive-pack server that advertises this capability is willing -to accept a signed push certificate. A send-pack client MUST NOT +to accept a signed push certificate, and asks the <nonce> to be +included in the push certificate. A send-pack client MUST NOT send a push-cert packet unless the receive-pack server advertises this capability. diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 2d93c87..9abfc94 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -52,6 +52,7 @@ static int accept_push_cert = 1; static struct strbuf push_cert = STRBUF_INIT; static unsigned char push_cert_sha1[20]; static struct signature_check sigcheck; +static const char *push_cert_nonce; static enum deny_action parse_deny_action(const char *var, const char *value) { @@ -157,8 +158,8 @@ static void show_ref(const char *path, const unsigned char *sha1) "report-status delete-refs side-band-64k quiet"); if (prefer_ofs_delta) strbuf_addstr(&cap, " ofs-delta"); - if (accept_push_cert) - strbuf_addstr(&cap, " push-cert"); + if (accept_push_cert && push_cert_nonce) + strbuf_addf(&cap, " push-cert=%s", push_cert_nonce); strbuf_addf(&cap, " agent=%s", git_user_agent_sanitized()); packet_write(1, "%s %s%c%s\n", sha1_to_hex(sha1), path, 0, cap.buf); @@ -313,6 +314,8 @@ static void prepare_push_cert_sha1(struct child_process *proc) argv_array_pushf(&env, "GIT_PUSH_CERT_KEY=%s", sigcheck.key ? sigcheck.key : ""); argv_array_pushf(&env, "GIT_PUSH_CERT_STATUS=%c", sigcheck.result); + if (push_cert_nonce) + argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE=%s", push_cert_nonce); proc->env = env.argv; } @@ -1245,6 +1248,17 @@ static int delete_only(struct command *commands) return 1; } +static char *prepare_push_cert_nonce(const char *dir) +{ + struct strbuf buf = STRBUF_INIT; + unsigned char sha1[20]; + + strbuf_addf(&buf, "%s:%lu", dir, time(NULL)); + hash_sha1_file(buf.buf, buf.len, "blob", sha1); + strbuf_release(&buf); + return xstrdup(sha1_to_hex(sha1)); +} + int cmd_receive_pack(int argc, const char **argv, const char *prefix) { int advertise_refs = 0; @@ -1304,6 +1318,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix) die("'%s' does not appear to be a git repository", dir); git_config(receive_pack_config, NULL); + if (accept_push_cert && !push_cert_nonce) + push_cert_nonce = prepare_push_cert_nonce(dir); if (0 <= transfer_unpack_limit) unpack_limit = transfer_unpack_limit; @@ -1348,5 +1364,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix) packet_flush(1); sha1_array_clear(&shallow); sha1_array_clear(&ref); + free((void *)push_cert_nonce); return 0; } diff --git a/send-pack.c b/send-pack.c index 642ebc8..141a6b2 100644 --- a/send-pack.c +++ b/send-pack.c @@ -228,7 +228,8 @@ static const char *next_line(const char *line, size_t len) static int generate_push_cert(struct strbuf *req_buf, const struct ref *remote_refs, struct send_pack_args *args, - const char *cap_string) + const char *cap_string, + const char *push_cert_nonce) { const struct ref *ref; char stamp[60]; @@ -245,6 +246,8 @@ static int generate_push_cert(struct strbuf *req_buf, strbuf_addf(&cert, "pushee %s\n", anon_url); free(anon_url); } + if (push_cert_nonce[0]) + strbuf_addf(&cert, "nonce %s\n", push_cert_nonce); strbuf_addstr(&cert, "\n"); for (ref = remote_refs; ref; ref = ref->next) { @@ -295,6 +298,8 @@ int send_pack(struct send_pack_args *args, unsigned cmds_sent = 0; int ret; struct async demux; + const char *push_cert_nonce = NULL; + /* Does the other end support the reporting? */ if (server_supports("report-status")) @@ -311,8 +316,14 @@ int send_pack(struct send_pack_args *args, agent_supported = 1; if (server_supports("no-thin")) args->use_thin_pack = 0; - if (args->push_cert && !server_supports("push-cert")) - die(_("the receiving end does not support --signed push")); + if (args->push_cert) { + int len; + + push_cert_nonce = server_feature_value("push-cert", &len); + if (!push_cert_nonce) + die(_("the receiving end does not support --signed push")); + push_cert_nonce = xmemdupz(push_cert_nonce, len); + } if (!remote_refs) { fprintf(stderr, "No refs in common and none specified; doing nothing.\n" @@ -343,7 +354,7 @@ int send_pack(struct send_pack_args *args, if (!args->dry_run && args->push_cert) cmds_sent = generate_push_cert(&req_buf, remote_refs, args, - cap_buf.buf); + cap_buf.buf, push_cert_nonce); /* * Clear the status for each ref and see if we need to send diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh index 35fe704..66fdaf2 100755 --- a/t/t5534-push-signed.sh +++ b/t/t5534-push-signed.sh @@ -72,17 +72,22 @@ test_expect_success GPG 'signed push sends push certificate' ' SIGNER=${GIT_PUSH_CERT_SIGNER-nobody} KEY=${GIT_PUSH_CERT_KEY-nokey} STATUS=${GIT_PUSH_CERT_STATUS-nostatus} + NONCE=${GIT_PUSH_CERT_NONCE-nononce} E_O_F EOF - cat >expect <<-\EOF && - SIGNER=C O Mitter <committer@xxxxxxxxxxx> - KEY=13B6F51ECDDE430D - STATUS=G - EOF - git push --signed dst noop ff +noff && + + ( + cat <<-\EOF && + SIGNER=C O Mitter <committer@xxxxxxxxxxx> + KEY=13B6F51ECDDE430D + STATUS=G + EOF + sed -n -e "s/^nonce /NONCE=/p" -e "/^$/q" dst/push-cert + ) >expect && + grep "$(git rev-parse noop ff) refs/heads/ff" dst/push-cert && grep "$(git rev-parse noop noff) refs/heads/noff" dst/push-cert && test_cmp expect dst/push-cert-status -- 2.1.0-399-g1364b4d -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html