{"thread":{"id":"37490","subject":"[PATCH v3 01/21] receive-pack: do not overallocate command structure","startedAt":"2014-09-04T20:04:36Z","lastAt":"2014-09-04T21:13:57Z","messageCount":23,"participants":["Junio C Hamano","Shawn Pearce"],"isPatch":true,"patchVersion":3,"patchTotal":21},"messages":[{"id":"248855","messageId":"1409861097-19151-1-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":null,"subject":"[PATCH v3 00/21] Signed push","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:36Z","receivedAt":"2014-09-04T20:04:36Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The first round is found at $gmane/255520.\nThe second round is found at $gmane/255701.\n\nWhile signed tags and commits assert that the objects thusly signed\ncame from you, who signed these objects, there is not a good way to\nassert that you wanted to have a particular object at the tip of a\nparticular branch.  My signing v2.0.1 tag only means I want to call\nthe version v2.0.1, and it does not mean I want to push it out to my\n'master' branch---it is likely that I only want it in 'maint', so\nthe signature on the object alone is insufficient.\n\nThe only assurance to you that 'maint' points at what I wanted to\nplace there comes from your trust on the hosting site and my\nauthentication with it, which cannot easily audited later.\n\nThis series introduces a cryptographic assurance for ref updates\ndone by \"git push\" by introducing a mechanism that allows you to\nsign a \"push certificate\" (for the lack of better name) every time\nyou push.  Think of it as working on an axis orthogonal to the\ntraditional \"signed tags\".\n\nThe second round exportedt GIT_PUSH_CERT, pointing at an empty blob,\neven when there was not any signed push going on, which has been\ncorrected in this version.\n\nThe last step of adding \"nonce\" still needs further thinking and\ntweaking (the issue is that inherently smart-http is not as secure\nas the native from replay-ability point of view; it needs to loosen\n\"has any of our ref changed since I started talking with this 'git\npush'\" check in receive-pack without this series already), but I\nthink everything else is in pretty good shape.\n\nJunio C Hamano (21):\n  receive-pack: do not overallocate command structure\n  receive-pack: parse feature request a bit earlier\n  receive-pack: do not reuse old_sha1[] for other things\n  receive-pack: factor out queueing of command\n  send-pack: move REF_STATUS_REJECT_NODELETE logic a bit higher\n  send-pack: refactor decision to send update per ref\n  send-pack: always send capabilities\n  send-pack: factor out capability string generation\n  receive-pack: factor out capability string generation\n  send-pack: rename \"new_refs\" to \"need_pack_data\"\n  send-pack: refactor inspecting and resetting status and sending commands\n  send-pack: clarify that cmds_sent is a boolean\n  gpg-interface: move parse_gpg_output() to where it should be\n  gpg-interface: move parse_signature() to where it should be\n  pack-protocol doc: typofix for PKT-LINE\n  push: the beginning of \"git push --signed\"\n  receive-pack: GPG-validate push certificates\n  send-pack: send feature request on push-cert packet\n  signed push: remove duplicated protocol info\n  signed push: add \"pushee\" header to push certificate\n  signed push: fortify against replay attacks\n\n Documentation/config.txt                          |   5 +\n Documentation/git-push.txt                        |   9 +-\n Documentation/git-receive-pack.txt                |  41 +++-\n Documentation/technical/pack-protocol.txt         |  49 ++++-\n Documentation/technical/protocol-capabilities.txt |  13 +-\n builtin/push.c                                    |   1 +\n builtin/receive-pack.c                            | 219 ++++++++++++++++++----\n commit.c                                          |  36 ----\n gpg-interface.c                                   |  57 ++++++\n gpg-interface.h                                   |  18 +-\n send-pack.c                                       | 202 +++++++++++++++-----\n send-pack.h                                       |   2 +\n t/t5534-push-signed.sh                            |  96 ++++++++++\n tag.c                                             |  20 --\n tag.h                                             |   1 -\n transport.c                                       |   5 +\n transport.h                                       |   5 +\n 17 files changed, 630 insertions(+), 149 deletions(-)\n create mode 100755 t/t5534-push-signed.sh\n\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248850","messageId":"1409861097-19151-2-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 01/21] receive-pack: do not overallocate command structure","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:37Z","receivedAt":"2014-09-04T20:04:37Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"An \"update\" command in the protocol exchange consists of 40-hex old\nobject name, SP, 40-hex new object name, SP, and a refname, but the\nfirst instance is further followed by a NUL with feature requests.\n\nThe command structure, which has a flex-array member that stores the\nrefname at the end, was allocated based on the whole length of the\nupdate command, without excluding the trailing feature requests.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/receive-pack.c | 5 +++--\n 1 file changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex f93ac45..1663beb 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -872,10 +872,11 @@ static struct command *read_head_info(struct sha1_array *shallow)\n \t\t\tif (parse_feature_request(feature_list, \"quiet\"))\n \t\t\t\tquiet = 1;\n \t\t}\n-\t\tcmd = xcalloc(1, sizeof(struct command) + len - 80);\n+\t\tcmd = xcalloc(1, sizeof(struct command) + reflen + 1);\n \t\thashcpy(cmd->old_sha1, old_sha1);\n \t\thashcpy(cmd->new_sha1, new_sha1);\n-\t\tmemcpy(cmd->ref_name, line + 82, len - 81);\n+\t\tmemcpy(cmd->ref_name, refname, reflen);\n+\t\tcmd->ref_name[reflen] = '\\0';\n \t\t*p = cmd;\n \t\tp = &cmd->next;\n \t}\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248851","messageId":"1409861097-19151-3-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 02/21] receive-pack: parse feature request a bit earlier","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:38Z","receivedAt":"2014-09-04T20:04:38Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ideally, we should have also allowed the first \"shallow\" to carry\nthe feature request trailer, but that is water under the bridge\nnow.  This makes the next step to factor out the queuing of commands\neasier to review.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/receive-pack.c | 26 ++++++++++++++------------\n 1 file changed, 14 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 1663beb..a91eec8 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -840,7 +840,7 @@ static struct command *read_head_info(struct sha1_array *shallow)\n \t\tunsigned char old_sha1[20], new_sha1[20];\n \t\tstruct command *cmd;\n \t\tchar *refname;\n-\t\tint len, reflen;\n+\t\tint len, reflen, linelen;\n \n \t\tline = packet_read_line(0, &len);\n \t\tif (!line)\n@@ -853,7 +853,18 @@ static struct command *read_head_info(struct sha1_array *shallow)\n \t\t\tcontinue;\n \t\t}\n \n-\t\tif (len < 83 ||\n+\t\tlinelen = strlen(line);\n+\t\tif (linelen < len) {\n+\t\t\tconst char *feature_list = line + linelen + 1;\n+\t\t\tif (parse_feature_request(feature_list, \"report-status\"))\n+\t\t\t\treport_status = 1;\n+\t\t\tif (parse_feature_request(feature_list, \"side-band-64k\"))\n+\t\t\t\tuse_sideband = LARGE_PACKET_MAX;\n+\t\t\tif (parse_feature_request(feature_list, \"quiet\"))\n+\t\t\t\tquiet = 1;\n+\t\t}\n+\n+\t\tif (linelen < 83 ||\n \t\t    line[40] != ' ' ||\n \t\t    line[81] != ' ' ||\n \t\t    get_sha1_hex(line, old_sha1) ||\n@@ -862,16 +873,7 @@ static struct command *read_head_info(struct sha1_array *shallow)\n \t\t\t    line);\n \n \t\trefname = line + 82;\n-\t\treflen = strlen(refname);\n-\t\tif (reflen + 82 < len) {\n-\t\t\tconst char *feature_list = refname + reflen + 1;\n-\t\t\tif (parse_feature_request(feature_list, \"report-status\"))\n-\t\t\t\treport_status = 1;\n-\t\t\tif (parse_feature_request(feature_list, \"side-band-64k\"))\n-\t\t\t\tuse_sideband = LARGE_PACKET_MAX;\n-\t\t\tif (parse_feature_request(feature_list, \"quiet\"))\n-\t\t\t\tquiet = 1;\n-\t\t}\n+\t\treflen = linelen - 82;\n \t\tcmd = xcalloc(1, sizeof(struct command) + reflen + 1);\n \t\thashcpy(cmd->old_sha1, old_sha1);\n \t\thashcpy(cmd->new_sha1, new_sha1);\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248852","messageId":"1409861097-19151-4-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 03/21] receive-pack: do not reuse old_sha1[] for other things","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:39Z","receivedAt":"2014-09-04T20:04:39Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"This piece of code reads object names of shallow boundaries, not\nold_sha1[], i.e. the current value the ref points at, which is to be\nreplaced by what is in new_sha1[].\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/receive-pack.c | 8 +++++---\n 1 file changed, 5 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a91eec8..c9b92bf 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -847,9 +847,11 @@ static struct command *read_head_info(struct sha1_array *shallow)\n \t\t\tbreak;\n \n \t\tif (len == 48 && starts_with(line, \"shallow \")) {\n-\t\t\tif (get_sha1_hex(line + 8, old_sha1))\n-\t\t\t\tdie(\"protocol error: expected shallow sha, got '%s'\", line + 8);\n-\t\t\tsha1_array_append(shallow, old_sha1);\n+\t\t\tunsigned char sha1[20];\n+\t\t\tif (get_sha1_hex(line + 8, sha1))\n+\t\t\t\tdie(\"protocol error: expected shallow sha, got '%s'\",\n+\t\t\t\t    line + 8);\n+\t\t\tsha1_array_append(shallow, sha1);\n \t\t\tcontinue;\n \t\t}\n \n-- \n2.1.0-399-g1364b4d\n"},{"id":"248857","messageId":"1409861097-19151-5-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 04/21] receive-pack: factor out queueing of command","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:40Z","receivedAt":"2014-09-04T20:04:40Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Make a helper function to accept a line of a protocol message and\nqueue an update command out of the code from read_head_info().\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/receive-pack.c | 50 +++++++++++++++++++++++++++++---------------------\n 1 file changed, 29 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex c9b92bf..341bb46 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -831,16 +831,40 @@ static void execute_commands(struct command *commands,\n \t\t      \"the reported refs above\");\n }\n \n+static struct command **queue_command(struct command **p,\n+\t\t\t\t      const char *line,\n+\t\t\t\t      int linelen)\n+{\n+\tunsigned char old_sha1[20], new_sha1[20];\n+\tstruct command *cmd;\n+\tconst char *refname;\n+\tint reflen;\n+\n+\tif (linelen < 83 ||\n+\t    line[40] != ' ' ||\n+\t    line[81] != ' ' ||\n+\t    get_sha1_hex(line, old_sha1) ||\n+\t    get_sha1_hex(line + 41, new_sha1))\n+\t\tdie(\"protocol error: expected old/new/ref, got '%s'\", line);\n+\n+\trefname = line + 82;\n+\treflen = linelen - 82;\n+\tcmd = xcalloc(1, sizeof(struct command) + reflen + 1);\n+\thashcpy(cmd->old_sha1, old_sha1);\n+\thashcpy(cmd->new_sha1, new_sha1);\n+\tmemcpy(cmd->ref_name, refname, reflen);\n+\tcmd->ref_name[reflen] = '\\0';\n+\t*p = cmd;\n+\treturn &cmd->next;\n+}\n+\n static struct command *read_head_info(struct sha1_array *shallow)\n {\n \tstruct command *commands = NULL;\n \tstruct command **p = &commands;\n \tfor (;;) {\n \t\tchar *line;\n-\t\tunsigned char old_sha1[20], new_sha1[20];\n-\t\tstruct command *cmd;\n-\t\tchar *refname;\n-\t\tint len, reflen, linelen;\n+\t\tint len, linelen;\n \n \t\tline = packet_read_line(0, &len);\n \t\tif (!line)\n@@ -866,23 +890,7 @@ static struct command *read_head_info(struct sha1_array *shallow)\n \t\t\t\tquiet = 1;\n \t\t}\n \n-\t\tif (linelen < 83 ||\n-\t\t    line[40] != ' ' ||\n-\t\t    line[81] != ' ' ||\n-\t\t    get_sha1_hex(line, old_sha1) ||\n-\t\t    get_sha1_hex(line + 41, new_sha1))\n-\t\t\tdie(\"protocol error: expected old/new/ref, got '%s'\",\n-\t\t\t    line);\n-\n-\t\trefname = line + 82;\n-\t\treflen = linelen - 82;\n-\t\tcmd = xcalloc(1, sizeof(struct command) + reflen + 1);\n-\t\thashcpy(cmd->old_sha1, old_sha1);\n-\t\thashcpy(cmd->new_sha1, new_sha1);\n-\t\tmemcpy(cmd->ref_name, refname, reflen);\n-\t\tcmd->ref_name[reflen] = '\\0';\n-\t\t*p = cmd;\n-\t\tp = &cmd->next;\n+\t\tp = queue_command(p, line, linelen);\n \t}\n \treturn commands;\n }\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248854","messageId":"1409861097-19151-6-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 05/21] send-pack: move REF_STATUS_REJECT_NODELETE logic a bit higher","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:41Z","receivedAt":"2014-09-04T20:04:41Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"20e8b465 (refactor ref status logic for pushing, 2010-01-08)\nrestructured the code to set status for each ref to be pushed, but\ndid not quite go far enough.  We inspect the status set earlier by\nset_refs_status_for_push() and then perform yet another update to\nthe status of a ref with an otherwise OK status to be deleted to\nmark it with REF_STATUS_REJECT_NODELETE when the protocol tells us\nnever to delete.\n\nSplit the latter into a separate loop that comes before we enter the\nper-ref loop.  This way we would have one less condition to check in\nthe main loop.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n send-pack.c | 15 ++++++++++-----\n 1 file changed, 10 insertions(+), 5 deletions(-)\n\ndiff --git a/send-pack.c b/send-pack.c\nindex 6129b0f..7428ae6 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -231,6 +231,15 @@ int send_pack(struct send_pack_args *args,\n \t\treturn 0;\n \t}\n \n+\t/*\n+\t * NEEDSWORK: why is delete-refs so specific to send-pack\n+\t * machinery that set_ref_status_for_push() cannot set this\n+\t * bit for us???\n+\t */\n+\tfor (ref = remote_refs; ref; ref = ref->next)\n+\t\tif (ref->deletion && !allow_deleting_refs)\n+\t\t\tref->status = REF_STATUS_REJECT_NODELETE;\n+\n \tif (!args->dry_run)\n \t\tadvertise_shallow_grafts_buf(&req_buf);\n \n@@ -249,17 +258,13 @@ int send_pack(struct send_pack_args *args,\n \t\tcase REF_STATUS_REJECT_FETCH_FIRST:\n \t\tcase REF_STATUS_REJECT_NEEDS_FORCE:\n \t\tcase REF_STATUS_REJECT_STALE:\n+\t\tcase REF_STATUS_REJECT_NODELETE:\n \t\tcase REF_STATUS_UPTODATE:\n \t\t\tcontinue;\n \t\tdefault:\n \t\t\t; /* do nothing */\n \t\t}\n \n-\t\tif (ref->deletion && !allow_deleting_refs) {\n-\t\t\tref->status = REF_STATUS_REJECT_NODELETE;\n-\t\t\tcontinue;\n-\t\t}\n-\n \t\tif (!ref->deletion)\n \t\t\tnew_refs++;\n \n-- \n2.1.0-399-g1364b4d\n"},{"id":"248856","messageId":"1409861097-19151-7-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 06/21] send-pack: refactor decision to send update per ref","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:42Z","receivedAt":"2014-09-04T20:04:42Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A new helper function ref_update_to_be_sent() decides for each ref\nif the update is to be sent based on the status previously set by\nset_ref_status_for_push() and also if this is a mirrored push.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n send-pack.c | 36 +++++++++++++++++++++---------------\n 1 file changed, 21 insertions(+), 15 deletions(-)\n\ndiff --git a/send-pack.c b/send-pack.c\nindex 7428ae6..f3c5ebe 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -190,6 +190,26 @@ static void advertise_shallow_grafts_buf(struct strbuf *sb)\n \tfor_each_commit_graft(advertise_shallow_grafts_cb, sb);\n }\n \n+static int ref_update_to_be_sent(const struct ref *ref, const struct send_pack_args *args)\n+{\n+\tif (!ref->peer_ref && !args->send_mirror)\n+\t\treturn 0;\n+\n+\t/* Check for statuses set by set_ref_status_for_push() */\n+\tswitch (ref->status) {\n+\tcase REF_STATUS_REJECT_NONFASTFORWARD:\n+\tcase REF_STATUS_REJECT_ALREADY_EXISTS:\n+\tcase REF_STATUS_REJECT_FETCH_FIRST:\n+\tcase REF_STATUS_REJECT_NEEDS_FORCE:\n+\tcase REF_STATUS_REJECT_STALE:\n+\tcase REF_STATUS_REJECT_NODELETE:\n+\tcase REF_STATUS_UPTODATE:\n+\t\treturn 0;\n+\tdefault:\n+\t\treturn 1;\n+\t}\n+}\n+\n int send_pack(struct send_pack_args *args,\n \t      int fd[], struct child_process *conn,\n \t      struct ref *remote_refs,\n@@ -248,23 +268,9 @@ int send_pack(struct send_pack_args *args,\n \t */\n \tnew_refs = 0;\n \tfor (ref = remote_refs; ref; ref = ref->next) {\n-\t\tif (!ref->peer_ref && !args->send_mirror)\n+\t\tif (!ref_update_to_be_sent(ref, args))\n \t\t\tcontinue;\n \n-\t\t/* Check for statuses set by set_ref_status_for_push() */\n-\t\tswitch (ref->status) {\n-\t\tcase REF_STATUS_REJECT_NONFASTFORWARD:\n-\t\tcase REF_STATUS_REJECT_ALREADY_EXISTS:\n-\t\tcase REF_STATUS_REJECT_FETCH_FIRST:\n-\t\tcase REF_STATUS_REJECT_NEEDS_FORCE:\n-\t\tcase REF_STATUS_REJECT_STALE:\n-\t\tcase REF_STATUS_REJECT_NODELETE:\n-\t\tcase REF_STATUS_UPTODATE:\n-\t\t\tcontinue;\n-\t\tdefault:\n-\t\t\t; /* do nothing */\n-\t\t}\n-\n \t\tif (!ref->deletion)\n \t\t\tnew_refs++;\n \n-- \n2.1.0-399-g1364b4d\n"},{"id":"248858","messageId":"1409861097-19151-8-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 07/21] send-pack: always send capabilities","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:43Z","receivedAt":"2014-09-04T20:04:43Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"We tried to avoid sending one extra byte, NUL and nothing behind it\nto signal there is no protocol capabilities being sent, on the first\ncommand packet on the wire, but it just made the code look ugly.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n send-pack.c | 4 +---\n 1 file changed, 1 insertion(+), 3 deletions(-)\n\ndiff --git a/send-pack.c b/send-pack.c\nindex f3c5ebe..2fa6c34 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -281,8 +281,7 @@ int send_pack(struct send_pack_args *args,\n \t\t\tchar *new_hex = sha1_to_hex(ref->new_sha1);\n \t\t\tint quiet = quiet_supported && (args->quiet || !args->progress);\n \n-\t\t\tif (!cmds_sent && (status_report || use_sideband ||\n-\t\t\t\t\t   quiet || agent_supported)) {\n+\t\t\tif (!cmds_sent)\n \t\t\t\tpacket_buf_write(&req_buf,\n \t\t\t\t\t\t \"%s %s %s%c%s%s%s%s%s\",\n \t\t\t\t\t\t old_hex, new_hex, ref->name, 0,\n@@ -292,7 +291,6 @@ int send_pack(struct send_pack_args *args,\n \t\t\t\t\t\t agent_supported ? \" agent=\" : \"\",\n \t\t\t\t\t\t agent_supported ? git_user_agent_sanitized() : \"\"\n \t\t\t\t\t\t);\n-\t\t\t}\n \t\t\telse\n \t\t\t\tpacket_buf_write(&req_buf, \"%s %s %s\",\n \t\t\t\t\t\t old_hex, new_hex, ref->name);\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248853","messageId":"1409861097-19151-9-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 08/21] send-pack: factor out capability string generation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:44Z","receivedAt":"2014-09-04T20:04:44Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A run of 'var ? \" var\" : \"\"' fed to a long printf string in a deeply\nnested block was hard to read.  Move it outside the loop and format\nit into a strbuf.\n\nAs an added bonus, the trick to add \"agent=<agent-name>\" by using\ntwo conditionals is replaced by a more readable version.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n send-pack.c | 21 +++++++++++++--------\n 1 file changed, 13 insertions(+), 8 deletions(-)\n\ndiff --git a/send-pack.c b/send-pack.c\nindex 2fa6c34..c1c64ee 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -218,6 +218,7 @@ int send_pack(struct send_pack_args *args,\n \tint in = fd[0];\n \tint out = fd[1];\n \tstruct strbuf req_buf = STRBUF_INIT;\n+\tstruct strbuf cap_buf = STRBUF_INIT;\n \tstruct ref *ref;\n \tint new_refs;\n \tint allow_deleting_refs = 0;\n@@ -251,6 +252,15 @@ int send_pack(struct send_pack_args *args,\n \t\treturn 0;\n \t}\n \n+\tif (status_report)\n+\t\tstrbuf_addstr(&cap_buf, \" report-status\");\n+\tif (use_sideband)\n+\t\tstrbuf_addstr(&cap_buf, \" side-band-64k\");\n+\tif (quiet_supported && (args->quiet || !args->progress))\n+\t\tstrbuf_addstr(&cap_buf, \" quiet\");\n+\tif (agent_supported)\n+\t\tstrbuf_addf(&cap_buf, \" agent=%s\", git_user_agent_sanitized());\n+\n \t/*\n \t * NEEDSWORK: why is delete-refs so specific to send-pack\n \t * machinery that set_ref_status_for_push() cannot set this\n@@ -279,18 +289,12 @@ int send_pack(struct send_pack_args *args,\n \t\t} else {\n \t\t\tchar *old_hex = sha1_to_hex(ref->old_sha1);\n \t\t\tchar *new_hex = sha1_to_hex(ref->new_sha1);\n-\t\t\tint quiet = quiet_supported && (args->quiet || !args->progress);\n \n \t\t\tif (!cmds_sent)\n \t\t\t\tpacket_buf_write(&req_buf,\n-\t\t\t\t\t\t \"%s %s %s%c%s%s%s%s%s\",\n+\t\t\t\t\t\t \"%s %s %s%c%s\",\n \t\t\t\t\t\t old_hex, new_hex, ref->name, 0,\n-\t\t\t\t\t\t status_report ? \" report-status\" : \"\",\n-\t\t\t\t\t\t use_sideband ? \" side-band-64k\" : \"\",\n-\t\t\t\t\t\t quiet ? \" quiet\" : \"\",\n-\t\t\t\t\t\t agent_supported ? \" agent=\" : \"\",\n-\t\t\t\t\t\t agent_supported ? git_user_agent_sanitized() : \"\"\n-\t\t\t\t\t\t);\n+\t\t\t\t\t\t cap_buf.buf);\n \t\t\telse\n \t\t\t\tpacket_buf_write(&req_buf, \"%s %s %s\",\n \t\t\t\t\t\t old_hex, new_hex, ref->name);\n@@ -311,6 +315,7 @@ int send_pack(struct send_pack_args *args,\n \t\tpacket_flush(out);\n \t}\n \tstrbuf_release(&req_buf);\n+\tstrbuf_release(&cap_buf);\n \n \tif (use_sideband && cmds_sent) {\n \t\tmemset(&demux, 0, sizeof(demux));\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248860","messageId":"1409861097-19151-10-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 09/21] receive-pack: factor out capability string generation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:45Z","receivedAt":"2014-09-04T20:04:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Similar to the previous one for send-pack, make it easier and\ncleaner to add to capability advertisement.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/receive-pack.c | 22 ++++++++++++++--------\n 1 file changed, 14 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 341bb46..9c39fb7 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -137,15 +137,21 @@ static void show_ref(const char *path, const unsigned char *sha1)\n \tif (ref_is_hidden(path))\n \t\treturn;\n \n-\tif (sent_capabilities)\n+\tif (sent_capabilities) {\n \t\tpacket_write(1, \"%s %s\\n\", sha1_to_hex(sha1), path);\n-\telse\n-\t\tpacket_write(1, \"%s %s%c%s%s agent=%s\\n\",\n-\t\t\t     sha1_to_hex(sha1), path, 0,\n-\t\t\t     \" report-status delete-refs side-band-64k quiet\",\n-\t\t\t     prefer_ofs_delta ? \" ofs-delta\" : \"\",\n-\t\t\t     git_user_agent_sanitized());\n-\tsent_capabilities = 1;\n+\t} else {\n+\t\tstruct strbuf cap = STRBUF_INIT;\n+\n+\t\tstrbuf_addstr(&cap,\n+\t\t\t      \"report-status delete-refs side-band-64k quiet\");\n+\t\tif (prefer_ofs_delta)\n+\t\t\tstrbuf_addstr(&cap, \" ofs-delta\");\n+\t\tstrbuf_addf(&cap, \" agent=%s\", git_user_agent_sanitized());\n+\t\tpacket_write(1, \"%s %s%c%s\\n\",\n+\t\t\t     sha1_to_hex(sha1), path, 0, cap.buf);\n+\t\tstrbuf_release(&cap);\n+\t\tsent_capabilities = 1;\n+\t}\n }\n \n static int show_ref_cb(const char *path, const unsigned char *sha1, int flag, void *unused)\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248859","messageId":"1409861097-19151-11-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 10/21] send-pack: rename \"new_refs\" to \"need_pack_data\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:46Z","receivedAt":"2014-09-04T20:04:46Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The variable counts how many non-deleting command is being sent, but\nis only checked with 0-ness to decide if we need to send the pack\ndata.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n send-pack.c | 7 +++----\n 1 file changed, 3 insertions(+), 4 deletions(-)\n\ndiff --git a/send-pack.c b/send-pack.c\nindex c1c64ee..590eb0a 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -220,7 +220,7 @@ int send_pack(struct send_pack_args *args,\n \tstruct strbuf req_buf = STRBUF_INIT;\n \tstruct strbuf cap_buf = STRBUF_INIT;\n \tstruct ref *ref;\n-\tint new_refs;\n+\tint need_pack_data = 0;\n \tint allow_deleting_refs = 0;\n \tint status_report = 0;\n \tint use_sideband = 0;\n@@ -276,13 +276,12 @@ int send_pack(struct send_pack_args *args,\n \t/*\n \t * Finally, tell the other end!\n \t */\n-\tnew_refs = 0;\n \tfor (ref = remote_refs; ref; ref = ref->next) {\n \t\tif (!ref_update_to_be_sent(ref, args))\n \t\t\tcontinue;\n \n \t\tif (!ref->deletion)\n-\t\t\tnew_refs++;\n+\t\t\tneed_pack_data = 1;\n \n \t\tif (args->dry_run) {\n \t\t\tref->status = REF_STATUS_OK;\n@@ -327,7 +326,7 @@ int send_pack(struct send_pack_args *args,\n \t\tin = demux.out;\n \t}\n \n-\tif (new_refs && cmds_sent) {\n+\tif (need_pack_data && cmds_sent) {\n \t\tif (pack_objects(out, remote_refs, extra_have, args) < 0) {\n \t\t\tfor (ref = remote_refs; ref; ref = ref->next)\n \t\t\t\tref->status = REF_STATUS_NONE;\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248863","messageId":"1409861097-19151-12-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 11/21] send-pack: refactor inspecting and resetting status and sending commands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:47Z","receivedAt":"2014-09-04T20:04:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The main loop over remote_refs list inspects the ref status\nto see if we need to generate pack data (i.e. a delete-only push\ndoes not need to send any additional data), resets it to \"expecting\nthe status report\" state, and formats the actual update commands\nto be sent.\n\nSplit the former two out of the main loop, as it will become\nconditional in later steps.\n\nBesides, we should have code that does real thing here, before the\n\"Finally, tell the other end!\" part ;-)\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n send-pack.c | 49 ++++++++++++++++++++++++++++++-------------------\n 1 file changed, 30 insertions(+), 19 deletions(-)\n\ndiff --git a/send-pack.c b/send-pack.c\nindex 590eb0a..f3262f2 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -274,7 +274,8 @@ int send_pack(struct send_pack_args *args,\n \t\tadvertise_shallow_grafts_buf(&req_buf);\n \n \t/*\n-\t * Finally, tell the other end!\n+\t * Clear the status for each ref and see if we need to send\n+\t * the pack data.\n \t */\n \tfor (ref = remote_refs; ref; ref = ref->next) {\n \t\tif (!ref_update_to_be_sent(ref, args))\n@@ -283,25 +284,35 @@ int send_pack(struct send_pack_args *args,\n \t\tif (!ref->deletion)\n \t\t\tneed_pack_data = 1;\n \n-\t\tif (args->dry_run) {\n+\t\tif (args->dry_run || !status_report)\n \t\t\tref->status = REF_STATUS_OK;\n-\t\t} else {\n-\t\t\tchar *old_hex = sha1_to_hex(ref->old_sha1);\n-\t\t\tchar *new_hex = sha1_to_hex(ref->new_sha1);\n-\n-\t\t\tif (!cmds_sent)\n-\t\t\t\tpacket_buf_write(&req_buf,\n-\t\t\t\t\t\t \"%s %s %s%c%s\",\n-\t\t\t\t\t\t old_hex, new_hex, ref->name, 0,\n-\t\t\t\t\t\t cap_buf.buf);\n-\t\t\telse\n-\t\t\t\tpacket_buf_write(&req_buf, \"%s %s %s\",\n-\t\t\t\t\t\t old_hex, new_hex, ref->name);\n-\t\t\tref->status = status_report ?\n-\t\t\t\tREF_STATUS_EXPECTING_REPORT :\n-\t\t\t\tREF_STATUS_OK;\n-\t\t\tcmds_sent++;\n-\t\t}\n+\t\telse\n+\t\t\tref->status = REF_STATUS_EXPECTING_REPORT;\n+\t}\n+\n+\t/*\n+\t * Finally, tell the other end!\n+\t */\n+\tfor (ref = remote_refs; ref; ref = ref->next) {\n+\t\tchar *old_hex, *new_hex;\n+\n+\t\tif (args->dry_run)\n+\t\t\tcontinue;\n+\n+\t\tif (!ref_update_to_be_sent(ref, args))\n+\t\t\tcontinue;\n+\n+\t\told_hex = sha1_to_hex(ref->old_sha1);\n+\t\tnew_hex = sha1_to_hex(ref->new_sha1);\n+\t\tif (!cmds_sent)\n+\t\t\tpacket_buf_write(&req_buf,\n+\t\t\t\t\t \"%s %s %s%c%s\",\n+\t\t\t\t\t old_hex, new_hex, ref->name, 0,\n+\t\t\t\t\t cap_buf.buf);\n+\t\telse\n+\t\t\tpacket_buf_write(&req_buf, \"%s %s %s\",\n+\t\t\t\t\t old_hex, new_hex, ref->name);\n+\t\tcmds_sent++;\n \t}\n \n \tif (args->stateless_rpc) {\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248871","messageId":"1409861097-19151-13-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 12/21] send-pack: clarify that cmds_sent is a boolean","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:48Z","receivedAt":"2014-09-04T20:04:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"We use it to make sure that the feature request is sent only once on\nthe very first request packet (ignoring the \"shallow \" line, which\nwas an unfortunate mistake we cannot retroactively fix with existing\nreceive-pack already deployed in the field) and we set it to \"true\"\nwith cmds_sent++, not because we care about the actual number of\nupdates sent but because it is merely an old idiomatic way.\n\nSet it explicitly to one to clarify that the code that uses this\nvariable only cares about its zero-ness.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n send-pack.c | 7 ++++---\n 1 file changed, 4 insertions(+), 3 deletions(-)\n\ndiff --git a/send-pack.c b/send-pack.c\nindex f3262f2..05926d2 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -304,15 +304,16 @@ int send_pack(struct send_pack_args *args,\n \n \t\told_hex = sha1_to_hex(ref->old_sha1);\n \t\tnew_hex = sha1_to_hex(ref->new_sha1);\n-\t\tif (!cmds_sent)\n+\t\tif (!cmds_sent) {\n \t\t\tpacket_buf_write(&req_buf,\n \t\t\t\t\t \"%s %s %s%c%s\",\n \t\t\t\t\t old_hex, new_hex, ref->name, 0,\n \t\t\t\t\t cap_buf.buf);\n-\t\telse\n+\t\t\tcmds_sent = 1;\n+\t\t} else {\n \t\t\tpacket_buf_write(&req_buf, \"%s %s %s\",\n \t\t\t\t\t old_hex, new_hex, ref->name);\n-\t\tcmds_sent++;\n+\t\t}\n \t}\n \n \tif (args->stateless_rpc) {\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248865","messageId":"1409861097-19151-14-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 13/21] gpg-interface: move parse_gpg_output() to where it should be","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:49Z","receivedAt":"2014-09-04T20:04:49Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Earlier, ffb6d7d5 (Move commit GPG signature verification to\ncommit.c, 2013-03-31) moved this helper that used to be in pretty.c\n(i.e. the output code path) to commit.c for better reusability.\n\nIt was a good first step in the right direction, but still suffers a\nmyopic view that commits will be the only thing we would ever want\nto sign---we would actually want to be able to reuse it even wider.\n\nThe function interprets what GPG said; gpg-interface is obviously a\nbetter place.  Move it there.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n commit.c        | 36 ------------------------------------\n gpg-interface.c | 36 ++++++++++++++++++++++++++++++++++++\n gpg-interface.h | 17 ++++++++++++-----\n 3 files changed, 48 insertions(+), 41 deletions(-)\n\ndiff --git a/commit.c b/commit.c\nindex ae7f2b1..01cdad2 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1220,42 +1220,6 @@ free_return:\n \tfree(buf);\n }\n \n-static struct {\n-\tchar result;\n-\tconst char *check;\n-} sigcheck_gpg_status[] = {\n-\t{ 'G', \"\\n[GNUPG:] GOODSIG \" },\n-\t{ 'B', \"\\n[GNUPG:] BADSIG \" },\n-\t{ 'U', \"\\n[GNUPG:] TRUST_NEVER\" },\n-\t{ 'U', \"\\n[GNUPG:] TRUST_UNDEFINED\" },\n-};\n-\n-static void parse_gpg_output(struct signature_check *sigc)\n-{\n-\tconst char *buf = sigc->gpg_status;\n-\tint i;\n-\n-\t/* Iterate over all search strings */\n-\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {\n-\t\tconst char *found, *next;\n-\n-\t\tif (!skip_prefix(buf, sigcheck_gpg_status[i].check + 1, &found)) {\n-\t\t\tfound = strstr(buf, sigcheck_gpg_status[i].check);\n-\t\t\tif (!found)\n-\t\t\t\tcontinue;\n-\t\t\tfound += strlen(sigcheck_gpg_status[i].check);\n-\t\t}\n-\t\tsigc->result = sigcheck_gpg_status[i].result;\n-\t\t/* The trust messages are not followed by key/signer information */\n-\t\tif (sigc->result != 'U') {\n-\t\t\tsigc->key = xmemdupz(found, 16);\n-\t\t\tfound += 17;\n-\t\t\tnext = strchrnul(found, '\\n');\n-\t\t\tsigc->signer = xmemdupz(found, next - found);\n-\t\t}\n-\t}\n-}\n-\n void check_commit_signature(const struct commit* commit, struct signature_check *sigc)\n {\n \tstruct strbuf payload = STRBUF_INIT;\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex ff07012..3c9624c 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -21,6 +21,42 @@ void signature_check_clear(struct signature_check *sigc)\n \tsigc->key = NULL;\n }\n \n+static struct {\n+\tchar result;\n+\tconst char *check;\n+} sigcheck_gpg_status[] = {\n+\t{ 'G', \"\\n[GNUPG:] GOODSIG \" },\n+\t{ 'B', \"\\n[GNUPG:] BADSIG \" },\n+\t{ 'U', \"\\n[GNUPG:] TRUST_NEVER\" },\n+\t{ 'U', \"\\n[GNUPG:] TRUST_UNDEFINED\" },\n+};\n+\n+void parse_gpg_output(struct signature_check *sigc)\n+{\n+\tconst char *buf = sigc->gpg_status;\n+\tint i;\n+\n+\t/* Iterate over all search strings */\n+\tfor (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {\n+\t\tconst char *found, *next;\n+\n+\t\tif (!skip_prefix(buf, sigcheck_gpg_status[i].check + 1, &found)) {\n+\t\t\tfound = strstr(buf, sigcheck_gpg_status[i].check);\n+\t\t\tif (!found)\n+\t\t\t\tcontinue;\n+\t\t\tfound += strlen(sigcheck_gpg_status[i].check);\n+\t\t}\n+\t\tsigc->result = sigcheck_gpg_status[i].result;\n+\t\t/* The trust messages are not followed by key/signer information */\n+\t\tif (sigc->result != 'U') {\n+\t\t\tsigc->key = xmemdupz(found, 16);\n+\t\t\tfound += 17;\n+\t\t\tnext = strchrnul(found, '\\n');\n+\t\t\tsigc->signer = xmemdupz(found, next - found);\n+\t\t}\n+\t}\n+}\n+\n void set_signing_key(const char *key)\n {\n \tfree(configured_signing_key);\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 37c23da..8d677cc 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -5,16 +5,23 @@ struct signature_check {\n \tchar *payload;\n \tchar *gpg_output;\n \tchar *gpg_status;\n-\tchar result; /* 0 (not checked),\n-\t\t      * N (checked but no further result),\n-\t\t      * U (untrusted good),\n-\t\t      * G (good)\n-\t\t      * B (bad) */\n+\n+\t/*\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+\tchar result;\n \tchar *signer;\n \tchar *key;\n };\n \n extern void signature_check_clear(struct signature_check *sigc);\n+extern void parse_gpg_output(struct signature_check *);\n+\n extern int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key);\n extern int verify_signed_buffer(const char *payload, size_t payload_size, const char *signature, size_t signature_size, struct strbuf *gpg_output, struct strbuf *gpg_status);\n extern int git_gpg_config(const char *, const char *, void *);\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248866","messageId":"1409861097-19151-15-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 14/21] gpg-interface: move parse_signature() to where it should be","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:50Z","receivedAt":"2014-09-04T20:04:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Our signed-tag objects set the standard format used by Git to store\nGPG-signed payload (i.e. the payload followed by its detached\nsignature), and it made sense to have a helper to find the boundary\nbetween the payload and its signature in tag.c back then.\n\nNewer code added later to parse other kinds of objects that learned\nto use the same format to store GPG-signed payload (e.g. signed\ncommits), however, kept using the helper from the same location.\n\nMove it to gpg-interface; the helper is no longer about signed tag,\nbut it is how our code and data interact with GPG.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n gpg-interface.c | 21 +++++++++++++++++++++\n gpg-interface.h |  1 +\n tag.c           | 20 --------------------\n tag.h           |  1 -\n 4 files changed, 22 insertions(+), 21 deletions(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 3c9624c..0dd11ea 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -7,6 +7,9 @@\n static char *configured_signing_key;\n static const char *gpg_program = \"gpg\";\n \n+#define PGP_SIGNATURE \"-----BEGIN PGP SIGNATURE-----\"\n+#define PGP_MESSAGE \"-----BEGIN PGP MESSAGE-----\"\n+\n void signature_check_clear(struct signature_check *sigc)\n {\n \tfree(sigc->payload);\n@@ -57,6 +60,24 @@ void parse_gpg_output(struct signature_check *sigc)\n \t}\n }\n \n+/*\n+ * Look at GPG signed content (e.g. a signed tag object), whose\n+ * payload is followed by a detached signature on it.  Return the\n+ * offset where the embedded detached signature begins, or the end of\n+ * the data when there is no such signature.\n+ */\n+size_t parse_signature(const char *buf, unsigned long size)\n+{\n+\tchar *eol;\n+\tsize_t len = 0;\n+\twhile (len < size && !starts_with(buf + len, PGP_SIGNATURE) &&\n+\t\t\t!starts_with(buf + len, PGP_MESSAGE)) {\n+\t\teol = memchr(buf + len, '\\n', size - len);\n+\t\tlen += eol ? eol - (buf + len) + 1 : size - len;\n+\t}\n+\treturn len;\n+}\n+\n void set_signing_key(const char *key)\n {\n \tfree(configured_signing_key);\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 8d677cc..93c76c2 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -20,6 +20,7 @@ struct signature_check {\n };\n \n extern void signature_check_clear(struct signature_check *sigc);\n+extern size_t parse_signature(const char *buf, unsigned long size);\n extern void parse_gpg_output(struct signature_check *);\n \n extern int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key);\ndiff --git a/tag.c b/tag.c\nindex 82d841b..5b0ac62 100644\n--- a/tag.c\n+++ b/tag.c\n@@ -4,9 +4,6 @@\n #include \"tree.h\"\n #include \"blob.h\"\n \n-#define PGP_SIGNATURE \"-----BEGIN PGP SIGNATURE-----\"\n-#define PGP_MESSAGE \"-----BEGIN PGP MESSAGE-----\"\n-\n const char *tag_type = \"tag\";\n \n struct object *deref_tag(struct object *o, const char *warn, int warnlen)\n@@ -143,20 +140,3 @@ int parse_tag(struct tag *item)\n \tfree(data);\n \treturn ret;\n }\n-\n-/*\n- * Look at a signed tag object, and return the offset where\n- * the embedded detached signature begins, or the end of the\n- * data when there is no such signature.\n- */\n-size_t parse_signature(const char *buf, unsigned long size)\n-{\n-\tchar *eol;\n-\tsize_t len = 0;\n-\twhile (len < size && !starts_with(buf + len, PGP_SIGNATURE) &&\n-\t\t\t!starts_with(buf + len, PGP_MESSAGE)) {\n-\t\teol = memchr(buf + len, '\\n', size - len);\n-\t\tlen += eol ? eol - (buf + len) + 1 : size - len;\n-\t}\n-\treturn len;\n-}\ndiff --git a/tag.h b/tag.h\nindex bc8a1e4..f4580ae 100644\n--- a/tag.h\n+++ b/tag.h\n@@ -17,6 +17,5 @@ extern int parse_tag_buffer(struct tag *item, const void *data, unsigned long si\n extern int parse_tag(struct tag *item);\n extern struct object *deref_tag(struct object *, const char *, int);\n extern struct object *deref_tag_noverify(struct object *);\n-extern size_t parse_signature(const char *buf, unsigned long size);\n \n #endif /* TAG_H */\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248861","messageId":"1409861097-19151-16-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 15/21] pack-protocol doc: typofix for PKT-LINE","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:51Z","receivedAt":"2014-09-04T20:04:51Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Everywhere else we use PKT-LINE to denote the pkt-line formatted\ndata, but \"shallow/deepen\" messages are described with PKT_LINE().\n\nFix them.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/technical/pack-protocol.txt | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt\nindex 18dea8d..a845d51 100644\n--- a/Documentation/technical/pack-protocol.txt\n+++ b/Documentation/technical/pack-protocol.txt\n@@ -212,9 +212,9 @@ out of what the server said it could do with the first 'want' line.\n   want-list         =  first-want\n \t\t       *additional-want\n \n-  shallow-line      =  PKT_LINE(\"shallow\" SP obj-id)\n+  shallow-line      =  PKT-LINE(\"shallow\" SP obj-id)\n \n-  depth-request     =  PKT_LINE(\"deepen\" SP depth)\n+  depth-request     =  PKT-LINE(\"deepen\" SP depth)\n \n   first-want        =  PKT-LINE(\"want\" SP obj-id SP capability-list LF)\n   additional-want   =  PKT-LINE(\"want\" SP obj-id LF)\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248867","messageId":"1409861097-19151-17-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 16/21] push: the beginning of \"git push --signed\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:52Z","receivedAt":"2014-09-04T20:04:52Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"While signed tags and commits assert that the objects thusly signed\ncame from you, who signed these objects, there is not a good way to\nassert that you wanted to have a particular object at the tip of a\nparticular branch.  My signing v2.0.1 tag only means I want to call\nthe version v2.0.1, and it does not mean I want to push it out to my\n'master' branch---it is likely that I only want it in 'maint'.\n\nIntroduce a mechanism that allows you to sign a \"push certificate\"\n(for the lack of better name) every time you push, asserting that\nwhat object you are pushing to update which ref that used to point\nat what other object.  Think of it as a cryptographic protection for\nref updates, similar to signed tags/commits but working on an\northogonal axis.\n\nThe basic flow based on this mechanism goes like this:\n\n 1. You push out your work with \"git push --signed\".\n\n 2. The sending side learns where the remote refs are as usual,\n    together with what protocol extension the receiving end\n    supports.  If the receiving end does not advertise the protocol\n    extension \"push-cert\", an attempt to \"git push --signed\" fails.\n\n    Otherwise, a text file, that looks like the following, is\n    prepared in core:\n\n\tcertificate version 0.1\n\tpusher Junio C Hamano <gitster@pobox.com> 1315427886 -0700\n\n\t7339ca65... 21580ecb... refs/heads/master\n\t3793ac56... 12850bec... refs/heads/next\n\n    The file begins with a few header lines, which may grow as we\n    gain more experience.  The 'pusher' header records the name of\n    the signer (the value of user.signingkey configuration variable,\n    falling back to GIT_COMMITTER_{NAME|EMAIL}) and the time of the\n    certificate generation.  After the header, a blank line follows,\n    followed by a copy of the protocol message lines.\n\n    Each line shows the old and the new object name at the tip of\n    the ref this push tries to update, in the way identical to how\n    the underlying \"git push\" protocol exchange tells the ref\n    updates to the receiving end (by recording the \"old\" object\n    name, the push certificate also protects against replaying).  It\n    is expected that new command packet types other than the\n    old-new-refname kind will be included in push certificate in the\n    same way as would appear in the plain vanilla command packets in\n    unsigned pushes.\n\n    The user then is asked to sign this push certificate using GPG,\n    formatted in a way similar to how signed tag objects are signed,\n    and the result is sent to the other side (i.e. receive-pack).\n\n    In the protocol exchange, this step comes immediately before the\n    sender tells what the result of the push should be, which in\n    turn comes before it sends the pack data.\n\n 3. When the receiving end sees a push certificate, the certificate\n    is written out as a blob.  The pre-receive hook can learn about\n    the certificate by checking GIT_PUSH_CERT environment variable,\n    which, if present, tells the object name of this blob, and make\n    the decision to allow or reject this push.  Additionally, the\n    post-receive hook can also look at the certificate, which may be\n    a good place to log all the received certificates for later\n    audits.\n\nBecause a push certificate carry the same information as the usual\ncommand packets in the protocol exchange, we can omit the latter\nwhen a push certificate is in use and reduce the protocol overhead.\nThis however is not included in this patch to make it easier to\nreview (in other words, the series at this step should never be\nreleased without the remainder of the series, as it implements an\ninterim protocol that will be incompatible with the final one,\nmerely for reviewing purposes).  As such, the documentation update\nfor the protocol is left out of this step.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/config.txt           |  5 +++\n Documentation/git-push.txt         |  9 ++++-\n Documentation/git-receive-pack.txt | 16 +++++++-\n builtin/push.c                     |  1 +\n builtin/receive-pack.c             | 60 +++++++++++++++++++++++++++++\n send-pack.c                        | 64 +++++++++++++++++++++++++++++++\n send-pack.h                        |  1 +\n t/t5534-push-signed.sh             | 77 ++++++++++++++++++++++++++++++++++++++\n transport.c                        |  4 ++\n transport.h                        |  5 +++\n 10 files changed, 240 insertions(+), 2 deletions(-)\n create mode 100755 t/t5534-push-signed.sh\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex c55c22a..a8938aa 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -2038,6 +2038,11 @@ rebase.autostash::\n \tsuccessful rebase might result in non-trivial conflicts.\n \tDefaults to false.\n \n+receive.acceptpushcert::\n+\tBy default, `git receive-pack` will advertise that it\n+\taccepts `git push --signed`.  Setting this variable to\n+\tfalse disables it.\n+\n receive.autogc::\n \tBy default, git-receive-pack will run \"git-gc --auto\" after\n \treceiving data from git-push and updating refs.  You can stop\ndiff --git a/Documentation/git-push.txt b/Documentation/git-push.txt\nindex 21cd455..21b3f29 100644\n--- a/Documentation/git-push.txt\n+++ b/Documentation/git-push.txt\n@@ -10,7 +10,8 @@ SYNOPSIS\n --------\n [verse]\n 'git push' [--all | --mirror | --tags] [--follow-tags] [-n | --dry-run] [--receive-pack=<git-receive-pack>]\n-\t   [--repo=<repository>] [-f | --force] [--prune] [-v | --verbose] [-u | --set-upstream]\n+\t   [--repo=<repository>] [-f | --force] [--prune] [-v | --verbose]\n+\t   [-u | --set-upstream] [--signed]\n \t   [--force-with-lease[=<refname>[:<expect>]]]\n \t   [--no-verify] [<repository> [<refspec>...]]\n \n@@ -129,6 +130,12 @@ already exists on the remote side.\n \tfrom the remote but are pointing at commit-ish that are\n \treachable from the refs being pushed.\n \n+--signed::\n+\tGPG-sign the push request to update refs on the receiving\n+\tside, to allow it to be checked by the hooks and/or be\n+\tlogged.  See linkgit:git-receive-pack[1] for the details\n+\ton the receiving end.\n+\n --receive-pack=<git-receive-pack>::\n --exec=<git-receive-pack>::\n \tPath to the 'git-receive-pack' program on the remote\ndiff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt\nindex b1f7dc6..6c458af 100644\n--- a/Documentation/git-receive-pack.txt\n+++ b/Documentation/git-receive-pack.txt\n@@ -53,6 +53,11 @@ the update.  Refs to be created will have sha1-old equal to 0\\{40},\n while refs to be deleted will have sha1-new equal to 0\\{40}, otherwise\n sha1-old and sha1-new should be valid objects in the repository.\n \n+When accepting a signed push (see linkgit:git-push[1]), the signed\n+push certificate is stored in a blob and an environment variable\n+`GIT_PUSH_CERT` can be consulted for its object name.  See the\n+description of `post-receive` hook for an example.\n+\n This hook is called before any refname is updated and before any\n fast-forward checks are performed.\n \n@@ -101,9 +106,13 @@ the update.  Refs that were created will have sha1-old equal to\n 0\\{40}, otherwise sha1-old and sha1-new should be valid objects in\n the repository.\n \n+The `GIT_PUSH_CERT` environment variable can be inspected, just as\n+in `pre-receive` hook, after accepting a signed push.\n+\n Using this hook, it is easy to generate mails describing the updates\n to the repository.  This example script sends one mail message per\n-ref listing the commits pushed to the repository:\n+ref listing the commits pushed to the repository, and logs the push\n+certificates of signed pushes to a file:\n \n \t#!/bin/sh\n \t# mail out commit update information.\n@@ -119,6 +128,11 @@ ref listing the commits pushed to the repository:\n \t\tfi |\n \t\tmail -s \"Changes to ref $ref\" commit-list@mydomain\n \tdone\n+\t# log signed push certificate, if any\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob ${GIT_PUSH_CERT} >>/var/log/push-log\n+\tfi\n \texit 0\n \n The exit code from this hook invocation is ignored, however a\ndiff --git a/builtin/push.c b/builtin/push.c\nindex f50e3d5..ae56f73 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -506,6 +506,7 @@ int cmd_push(int argc, const char **argv, const char *prefix)\n \t\tOPT_BIT(0, \"no-verify\", &flags, N_(\"bypass pre-push hook\"), TRANSPORT_PUSH_NO_HOOK),\n \t\tOPT_BIT(0, \"follow-tags\", &flags, N_(\"push missing but relevant tags\"),\n \t\t\tTRANSPORT_PUSH_FOLLOW_TAGS),\n+\t\tOPT_BIT(0, \"signed\", &flags, N_(\"GPG sign the push\"), TRANSPORT_PUSH_CERT),\n \t\tOPT_END()\n \t};\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 9c39fb7..4335309 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -46,6 +46,9 @@ static void *head_name_to_free;\n static int sent_capabilities;\n static int shallow_update;\n static const char *alt_shallow_file;\n+static int accept_push_cert = 1;\n+static struct strbuf push_cert = STRBUF_INIT;\n+static unsigned char push_cert_sha1[20];\n \n static enum deny_action parse_deny_action(const char *var, const char *value)\n {\n@@ -129,6 +132,11 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (strcmp(var, \"receive.acceptpushcert\") == 0) {\n+\t\taccept_push_cert = git_config_bool(var, value);\n+\t\treturn 0;\n+\t}\n+\n \treturn git_default_config(var, value, cb);\n }\n \n@@ -146,6 +154,8 @@ static void show_ref(const char *path, const unsigned char *sha1)\n \t\t\t      \"report-status delete-refs side-band-64k quiet\");\n \t\tif (prefer_ofs_delta)\n \t\t\tstrbuf_addstr(&cap, \" ofs-delta\");\n+\t\tif (accept_push_cert)\n+\t\t\tstrbuf_addstr(&cap, \" push-cert\");\n \t\tstrbuf_addf(&cap, \" agent=%s\", git_user_agent_sanitized());\n \t\tpacket_write(1, \"%s %s%c%s\\n\",\n \t\t\t     sha1_to_hex(sha1), path, 0, cap.buf);\n@@ -258,6 +268,25 @@ static int copy_to_sideband(int in, int out, void *arg)\n \treturn 0;\n }\n \n+static void prepare_push_cert_sha1(struct child_process *proc)\n+{\n+\tstatic int already_done;\n+\tstruct argv_array env = ARGV_ARRAY_INIT;\n+\n+\tif (!push_cert.len)\n+\t\treturn;\n+\n+\tif (!already_done) {\n+\t\talready_done = 1;\n+\t\tif (write_sha1_file(push_cert.buf, push_cert.len, \"blob\", push_cert_sha1))\n+\t\t\thashclr(push_cert_sha1);\n+\t}\n+\tif (!is_null_sha1(push_cert_sha1)) {\n+\t\targv_array_pushf(&env, \"GIT_PUSH_CERT=%s\", sha1_to_hex(push_cert_sha1));\n+\t\tproc->env = env.argv;\n+\t}\n+}\n+\n typedef int (*feed_fn)(void *, const char **, size_t *);\n static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_state)\n {\n@@ -277,6 +306,8 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_sta\n \tproc.in = -1;\n \tproc.stdout_to_stderr = 1;\n \n+\tprepare_push_cert_sha1(&proc);\n+\n \tif (use_sideband) {\n \t\tmemset(&muxer, 0, sizeof(muxer));\n \t\tmuxer.proc = copy_to_sideband;\n@@ -896,6 +927,27 @@ static struct command *read_head_info(struct sha1_array *shallow)\n \t\t\t\tquiet = 1;\n \t\t}\n \n+\t\tif (!strcmp(line, \"push-cert\")) {\n+\t\t\tint true_flush = 0;\n+\t\t\tchar certbuf[1024];\n+\n+\t\t\tfor (;;) {\n+\t\t\t\tlen = packet_read(0, NULL, NULL,\n+\t\t\t\t\t\t  certbuf, sizeof(certbuf), 0);\n+\t\t\t\tif (!len) {\n+\t\t\t\t\ttrue_flush = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t\tif (!strcmp(certbuf, \"push-cert-end\\n\"))\n+\t\t\t\t\tbreak; /* end of cert */\n+\t\t\t\tstrbuf_addstr(&push_cert, certbuf);\n+\t\t\t}\n+\n+\t\t\tif (true_flush)\n+\t\t\t\tbreak;\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\tp = queue_command(p, line, linelen);\n \t}\n \treturn commands;\n@@ -1171,6 +1223,14 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)\n \t\t\t\tfix_thin = 0;\n \t\t\t\tcontinue;\n \t\t\t}\n+\t\t\tif (!strcmp(arg, \"--push-cert\")) {\n+\t\t\t\taccept_push_cert = 1;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tif (!strcmp(arg, \"--no-push-cert\")) {\n+\t\t\t\taccept_push_cert = 0;\n+\t\t\t\tcontinue;\n+\t\t\t}\n \n \t\t\tusage(receive_pack_usage);\n \t\t}\ndiff --git a/send-pack.c b/send-pack.c\nindex 05926d2..10c907f 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -11,6 +11,7 @@\n #include \"transport.h\"\n #include \"version.h\"\n #include \"sha1-array.h\"\n+#include \"gpg-interface.h\"\n \n static int feed_object(const unsigned char *sha1, int fd, int negative)\n {\n@@ -210,6 +211,64 @@ static int ref_update_to_be_sent(const struct ref *ref, const struct send_pack_a\n \t}\n }\n \n+/*\n+ * the beginning of the next line, or the end of buffer.\n+ *\n+ * NEEDSWORK: perhaps move this to git-compat-util.h or somewhere and\n+ * convert many similar uses found by \"git grep -A4 memchr\".\n+ */\n+static const char *next_line(const char *line, size_t len)\n+{\n+\tconst char *nl = memchr(line, '\\n', len);\n+\tif (!nl)\n+\t\treturn line + len; /* incomplete line */\n+\treturn nl + 1;\n+}\n+\n+static void generate_push_cert(struct strbuf *req_buf,\n+\t\t\t       const struct ref *remote_refs,\n+\t\t\t       struct send_pack_args *args)\n+{\n+\tconst struct ref *ref;\n+\tchar stamp[60];\n+\tchar *signing_key = xstrdup(get_signing_key());\n+\tconst char *cp, *np;\n+\tstruct strbuf cert = STRBUF_INIT;\n+\tint update_seen = 0;\n+\n+\tdatestamp(stamp, sizeof(stamp));\n+\tstrbuf_addf(&cert, \"certificate version 0.1\\n\");\n+\tstrbuf_addf(&cert, \"pusher %s %s\\n\", signing_key, stamp);\n+\tstrbuf_addstr(&cert, \"\\n\");\n+\n+\tfor (ref = remote_refs; ref; ref = ref->next) {\n+\t\tif (!ref_update_to_be_sent(ref, args))\n+\t\t\tcontinue;\n+\t\tupdate_seen = 1;\n+\t\tstrbuf_addf(&cert, \"%s %s %s\\n\",\n+\t\t\t    sha1_to_hex(ref->old_sha1),\n+\t\t\t    sha1_to_hex(ref->new_sha1),\n+\t\t\t    ref->name);\n+\t}\n+\tif (!update_seen)\n+\t\tgoto free_return;\n+\n+\tif (sign_buffer(&cert, &cert, signing_key))\n+\t\tdie(_(\"failed to sign the push certificate\"));\n+\n+\tpacket_buf_write(req_buf, \"push-cert\\n\");\n+\tfor (cp = cert.buf; cp < cert.buf + cert.len; cp = np) {\n+\t\tnp = next_line(cp, cert.buf + cert.len - cp);\n+\t\tpacket_buf_write(req_buf,\n+\t\t\t\t \"%.*s\", (int)(np - cp), cp);\n+\t}\n+\tpacket_buf_write(req_buf, \"push-cert-end\\n\");\n+\n+free_return:\n+\tfree(signing_key);\n+\tstrbuf_release(&cert);\n+}\n+\n int send_pack(struct send_pack_args *args,\n \t      int fd[], struct child_process *conn,\n \t      struct ref *remote_refs,\n@@ -245,6 +304,8 @@ int send_pack(struct send_pack_args *args,\n \t\tagent_supported = 1;\n \tif (server_supports(\"no-thin\"))\n \t\targs->use_thin_pack = 0;\n+\tif (args->push_cert && !server_supports(\"push-cert\"))\n+\t\tdie(_(\"the receiving end does not support --signed push\"));\n \n \tif (!remote_refs) {\n \t\tfprintf(stderr, \"No refs in common and none specified; doing nothing.\\n\"\n@@ -273,6 +334,9 @@ int send_pack(struct send_pack_args *args,\n \tif (!args->dry_run)\n \t\tadvertise_shallow_grafts_buf(&req_buf);\n \n+\tif (!args->dry_run && args->push_cert)\n+\t\tgenerate_push_cert(&req_buf, remote_refs, args);\n+\n \t/*\n \t * Clear the status for each ref and see if we need to send\n \t * the pack data.\ndiff --git a/send-pack.h b/send-pack.h\nindex 8e84392..3555d8e 100644\n--- a/send-pack.h\n+++ b/send-pack.h\n@@ -11,6 +11,7 @@ struct send_pack_args {\n \t\tuse_thin_pack:1,\n \t\tuse_ofs_delta:1,\n \t\tdry_run:1,\n+\t\tpush_cert:1,\n \t\tstateless_rpc:1;\n };\n \ndiff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh\nnew file mode 100755\nindex 0000000..97323bb\n--- /dev/null\n+++ b/t/t5534-push-signed.sh\n@@ -0,0 +1,77 @@\n+#!/bin/sh\n+\n+test_description='signed push'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-gpg.sh\n+\n+prepare_dst () {\n+\trm -fr dst &&\n+\ttest_create_repo dst &&\n+\n+\tgit push dst master:noop master:ff master:noff\n+}\n+\n+test_expect_success setup '\n+\t# master, ff and noff branches pointing at the same commit\n+\ttest_tick &&\n+\tgit commit --allow-empty -m initial &&\n+\n+\tgit checkout -b noop &&\n+\tgit checkout -b ff &&\n+\tgit checkout -b noff &&\n+\n+\t# noop stays the same, ff advances, noff rewrites\n+\ttest_tick &&\n+\tgit commit --allow-empty --amend -m rewritten &&\n+\tgit checkout ff &&\n+\n+\ttest_tick &&\n+\tgit commit --allow-empty -m second\n+'\n+\n+test_expect_success 'unsigned push does not send push certificate' '\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi\n+\tEOF\n+\n+\tgit push dst noop ff +noff &&\n+\t! test -f dst/push-cert\n+'\n+\n+test_expect_success 'talking with a receiver without push certificate support' '\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config receive.acceptpushcert no &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi\n+\tEOF\n+\n+\tgit push dst noop ff +noff &&\n+\t! test -f dst/push-cert\n+'\n+\n+test_expect_success GPG 'signed push sends push certificate' '\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi\n+\tEOF\n+\n+\tgit push --signed dst noop ff +noff &&\n+\tgrep \"$(git rev-parse noop ff) refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$(git rev-parse noop noff) refs/heads/noff\" dst/push-cert\n+'\n+\n+test_done\ndiff --git a/transport.c b/transport.c\nindex 662421b..07fdf86 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -480,6 +480,9 @@ static int set_git_option(struct git_transport_options *opts,\n \t\t\t\tdie(\"transport: invalid depth option '%s'\", value);\n \t\t}\n \t\treturn 0;\n+\t} else if (!strcmp(name, TRANS_OPT_PUSH_CERT)) {\n+\t\topts->push_cert = !!value;\n+\t\treturn 0;\n \t}\n \treturn 1;\n }\n@@ -823,6 +826,7 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re\n \targs.progress = transport->progress;\n \targs.dry_run = !!(flags & TRANSPORT_PUSH_DRY_RUN);\n \targs.porcelain = !!(flags & TRANSPORT_PUSH_PORCELAIN);\n+\targs.push_cert = !!(flags & TRANSPORT_PUSH_CERT);\n \n \tret = send_pack(&args, data->fd, data->conn, remote_refs,\n \t\t\t&data->extra_have);\ndiff --git a/transport.h b/transport.h\nindex 02ea248..3e0091e 100644\n--- a/transport.h\n+++ b/transport.h\n@@ -12,6 +12,7 @@ struct git_transport_options {\n \tunsigned check_self_contained_and_connected : 1;\n \tunsigned self_contained_and_connected : 1;\n \tunsigned update_shallow : 1;\n+\tunsigned push_cert : 1;\n \tint depth;\n \tconst char *uploadpack;\n \tconst char *receivepack;\n@@ -123,6 +124,7 @@ struct transport {\n #define TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND 256\n #define TRANSPORT_PUSH_NO_HOOK 512\n #define TRANSPORT_PUSH_FOLLOW_TAGS 1024\n+#define TRANSPORT_PUSH_CERT 2048\n \n #define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)\n #define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)\n@@ -156,6 +158,9 @@ struct transport *transport_get(struct remote *, const char *);\n /* Accept refs that may update .git/shallow without --depth */\n #define TRANS_OPT_UPDATE_SHALLOW \"updateshallow\"\n \n+/* Send push certificates */\n+#define TRANS_OPT_PUSH_CERT \"pushcert\"\n+\n /**\n  * Returns 0 if the option was used, non-zero otherwise. Prints a\n  * message to stderr if the option is not used.\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248868","messageId":"1409861097-19151-18-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 17/21] receive-pack: GPG-validate push certificates","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:53Z","receivedAt":"2014-09-04T20:04:53Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Reusing the GPG signature check helpers we already have, verify\nthe signature in receive-pack and give the results to the hooks\nvia GIT_PUSH_CERT_{SIGNER,KEY,STATUS} environment variables.\n\nPolicy decisions, such as accepting or rejecting a good signature by\na key that is not fully trusted, is left to the hook and kept\noutside of the core.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/git-receive-pack.txt | 27 ++++++++++++++++++++++-----\n builtin/receive-pack.c             | 31 +++++++++++++++++++++++++++++++\n t/t5534-push-signed.sh             | 18 ++++++++++++++++--\n 3 files changed, 69 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt\nindex 6c458af..60151a6 100644\n--- a/Documentation/git-receive-pack.txt\n+++ b/Documentation/git-receive-pack.txt\n@@ -56,7 +56,21 @@ sha1-old and sha1-new should be valid objects in the repository.\n When accepting a signed push (see linkgit:git-push[1]), the signed\n push certificate is stored in a blob and an environment variable\n `GIT_PUSH_CERT` can be consulted for its object name.  See the\n-description of `post-receive` hook for an example.\n+description of `post-receive` hook for an example.  In addition, the\n+certificate is verified using GPG and the result is exported with\n+the following environment variables:\n+\n+GIT_PUSH_CERT_SIGNER::\n+\tThe name and the e-mail address of the owner of the key that\n+\tsigned the push certificate.\n+\n+GIT_PUSH_CERT_KEY::\n+\tThe GPG key ID of the key that signed the push certificate.\n+\n+GIT_PUSH_CERT_STATUS::\n+\tThe status of GPG verification of the push certificate,\n+\tusing the same mnemonic as used in `%G?` format of `git log`\n+\tfamily of commands (see linkgit:git-log[1]).\n \n This hook is called before any refname is updated and before any\n fast-forward checks are performed.\n@@ -106,13 +120,14 @@ the update.  Refs that were created will have sha1-old equal to\n 0\\{40}, otherwise sha1-old and sha1-new should be valid objects in\n the repository.\n \n-The `GIT_PUSH_CERT` environment variable can be inspected, just as\n+The `GIT_PUSH_CERT*` environment variables can be inspected, just as\n in `pre-receive` hook, after accepting a signed push.\n \n Using this hook, it is easy to generate mails describing the updates\n to the repository.  This example script sends one mail message per\n ref listing the commits pushed to the repository, and logs the push\n-certificates of signed pushes to a file:\n+certificates of signed pushes with good signatures to a logger\n+service:\n \n \t#!/bin/sh\n \t# mail out commit update information.\n@@ -129,9 +144,11 @@ certificates of signed pushes to a file:\n \t\tmail -s \"Changes to ref $ref\" commit-list@mydomain\n \tdone\n \t# log signed push certificate, if any\n-\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tif test -n \"${GIT_PUSH_CERT-}\" && test ${GIT_PUSH_CERT_STATUS} = G\n \tthen\n-\t\tgit cat-file blob ${GIT_PUSH_CERT} >>/var/log/push-log\n+\t\t(\n+\t\t\tgit cat-file blob ${GIT_PUSH_CERT}\n+\t\t) | mail -s \"push from $GIT_PUSH_CERT_SIGNER\" push-log@mydomain\n \tfi\n \texit 0\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 4335309..bfd078c 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -15,6 +15,8 @@\n #include \"connected.h\"\n #include \"argv-array.h\"\n #include \"version.h\"\n+#include \"tag.h\"\n+#include \"gpg-interface.h\"\n \n static const char receive_pack_usage[] = \"git receive-pack <git-dir>\";\n \n@@ -49,6 +51,7 @@ static const char *alt_shallow_file;\n static int accept_push_cert = 1;\n static struct strbuf push_cert = STRBUF_INIT;\n static unsigned char push_cert_sha1[20];\n+static struct signature_check sigcheck;\n \n static enum deny_action parse_deny_action(const char *var, const char *value)\n {\n@@ -277,12 +280,40 @@ static void prepare_push_cert_sha1(struct child_process *proc)\n \t\treturn;\n \n \tif (!already_done) {\n+\t\tstruct strbuf gpg_output = STRBUF_INIT;\n+\t\tstruct strbuf gpg_status = STRBUF_INIT;\n+\t\tint bogs /* beginning_of_gpg_sig */;\n+\n \t\talready_done = 1;\n \t\tif (write_sha1_file(push_cert.buf, push_cert.len, \"blob\", push_cert_sha1))\n \t\t\thashclr(push_cert_sha1);\n+\n+\t\tmemset(&sigcheck, '\\0', sizeof(sigcheck));\n+\t\tsigcheck.result = 'N';\n+\n+\t\tbogs = parse_signature(push_cert.buf, push_cert.len);\n+\t\tif (verify_signed_buffer(push_cert.buf, bogs,\n+\t\t\t\t\t push_cert.buf + bogs, push_cert.len - bogs,\n+\t\t\t\t\t &gpg_output, &gpg_status) < 0) {\n+\t\t\t; /* error running gpg */\n+\t\t} else {\n+\t\t\tsigcheck.payload = push_cert.buf;\n+\t\t\tsigcheck.gpg_output = gpg_output.buf;\n+\t\t\tsigcheck.gpg_status = gpg_status.buf;\n+\t\t\tparse_gpg_output(&sigcheck);\n+\t\t}\n+\n+\t\tstrbuf_release(&gpg_output);\n+\t\tstrbuf_release(&gpg_status);\n \t}\n \tif (!is_null_sha1(push_cert_sha1)) {\n \t\targv_array_pushf(&env, \"GIT_PUSH_CERT=%s\", sha1_to_hex(push_cert_sha1));\n+\t\targv_array_pushf(&env, \"GIT_PUSH_CERT_SIGNER=%s\",\n+\t\t\t\t sigcheck.signer ? sigcheck.signer : \"\");\n+\t\targv_array_pushf(&env, \"GIT_PUSH_CERT_KEY=%s\",\n+\t\t\t\t sigcheck.key ? sigcheck.key : \"\");\n+\t\targv_array_pushf(&env, \"GIT_PUSH_CERT_STATUS=%c\", sigcheck.result);\n+\n \t\tproc->env = env.argv;\n \t}\n }\ndiff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh\nindex 97323bb..35fe704 100755\n--- a/t/t5534-push-signed.sh\n+++ b/t/t5534-push-signed.sh\n@@ -66,12 +66,26 @@ test_expect_success GPG 'signed push sends push certificate' '\n \tif test -n \"${GIT_PUSH_CERT-}\"\n \tthen\n \t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n-\tfi\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tE_O_F\n+\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tSIGNER=C O Mitter <committer@example.com>\n+\tKEY=13B6F51ECDDE430D\n+\tSTATUS=G\n \tEOF\n \n \tgit push --signed dst noop ff +noff &&\n \tgrep \"$(git rev-parse noop ff) refs/heads/ff\" dst/push-cert &&\n-\tgrep \"$(git rev-parse noop noff) refs/heads/noff\" dst/push-cert\n+\tgrep \"$(git rev-parse noop noff) refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n '\n \n test_done\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248870","messageId":"1409861097-19151-19-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 18/21] send-pack: send feature request on push-cert packet","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:54Z","receivedAt":"2014-09-04T20:04:54Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"We would want to update the interim protocol so that we do not send\nthe usual update commands when the push certificate feature is in\nuse, as the same information is in the certificate.  Once that\nhappens, the push-cert packet may become the only protocol command,\nbut then there is no packet to put the feature request behind, like\nwe always did.\n\nAs we have prepared the receiving end that understands the push-cert\nfeature to accept the feature request on the first protocol packet\n(other than \"shallow \", which was an unfortunate historical mistake\nthat has to come before everything else), we can give the feature\nrequest on the push-cert packet instead of the first update protocol\npacket, in preparation for the next step to actually update to the\nfinal protocol.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n send-pack.c | 13 ++++++++-----\n 1 file changed, 8 insertions(+), 5 deletions(-)\n\ndiff --git a/send-pack.c b/send-pack.c\nindex 10c907f..4125892 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -225,9 +225,10 @@ static const char *next_line(const char *line, size_t len)\n \treturn nl + 1;\n }\n \n-static void generate_push_cert(struct strbuf *req_buf,\n-\t\t\t       const struct ref *remote_refs,\n-\t\t\t       struct send_pack_args *args)\n+static int generate_push_cert(struct strbuf *req_buf,\n+\t\t\t      const struct ref *remote_refs,\n+\t\t\t      struct send_pack_args *args,\n+\t\t\t      const char *cap_string)\n {\n \tconst struct ref *ref;\n \tchar stamp[60];\n@@ -256,7 +257,7 @@ static void generate_push_cert(struct strbuf *req_buf,\n \tif (sign_buffer(&cert, &cert, signing_key))\n \t\tdie(_(\"failed to sign the push certificate\"));\n \n-\tpacket_buf_write(req_buf, \"push-cert\\n\");\n+\tpacket_buf_write(req_buf, \"push-cert%c%s\", 0, cap_string);\n \tfor (cp = cert.buf; cp < cert.buf + cert.len; cp = np) {\n \t\tnp = next_line(cp, cert.buf + cert.len - cp);\n \t\tpacket_buf_write(req_buf,\n@@ -267,6 +268,7 @@ static void generate_push_cert(struct strbuf *req_buf,\n free_return:\n \tfree(signing_key);\n \tstrbuf_release(&cert);\n+\treturn 1;\n }\n \n int send_pack(struct send_pack_args *args,\n@@ -335,7 +337,8 @@ int send_pack(struct send_pack_args *args,\n \t\tadvertise_shallow_grafts_buf(&req_buf);\n \n \tif (!args->dry_run && args->push_cert)\n-\t\tgenerate_push_cert(&req_buf, remote_refs, args);\n+\t\tcmds_sent = generate_push_cert(&req_buf, remote_refs, args,\n+\t\t\t\t\t       cap_buf.buf);\n \n \t/*\n \t * Clear the status for each ref and see if we need to send\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248862","messageId":"1409861097-19151-20-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 19/21] signed push: remove duplicated protocol info","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:55Z","receivedAt":"2014-09-04T20:04:55Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"With the interim protocol, we used to send the update commands even\nthough we already send a signed copy of the same information when\npush certificate is in use.  Update the send-pack/receive-pack pair\nnot to do so.\n\nThe notable thing on the receive-pack side is that it makes sure\nthat there is no command sent over the traditional protocol packet\noutside the push certificate.  Otherwise a pusher can claim to be\npushing one set of ref updates in the signed certificate while\nissuing commands to update unrelated refs, and such an update will\nevade later audits.\n\nFinally, start documenting the protocol.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/technical/pack-protocol.txt         | 33 ++++++++++++++++++++++-\n Documentation/technical/protocol-capabilities.txt | 12 +++++++--\n builtin/receive-pack.c                            | 30 +++++++++++++++++++--\n send-pack.c                                       |  2 +-\n 4 files changed, 71 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt\nindex a845d51..4a5c2e8 100644\n--- a/Documentation/technical/pack-protocol.txt\n+++ b/Documentation/technical/pack-protocol.txt\n@@ -465,7 +465,7 @@ contain all the objects that the server will need to complete the new\n references.\n \n ----\n-  update-request    =  *shallow command-list [pack-file]\n+  update-request    =  *shallow ( command-list | push-cert ) [pack-file]\n \n   shallow           =  PKT-LINE(\"shallow\" SP obj-id)\n \n@@ -481,12 +481,25 @@ references.\n   old-id            =  obj-id\n   new-id            =  obj-id\n \n+  push-cert         = PKT-LINE(\"push-cert\" NUL capability-list LF)\n+\t\t      PKT-LINE(\"certificate version 0.1\" LF)\n+\t\t      PKT-LINE(\"pusher\" SP ident LF)\n+\t\t      PKT-LINE(LF)\n+\t\t      *PKT-LINE(command LF)\n+\t\t      *PKT-LINE(gpg-signature-lines LF)\n+\t\t      PKT-LINE(\"push-cert-end\" LF)\n+\n   pack-file         = \"PACK\" 28*(OCTET)\n ----\n \n If the receiving end does not support delete-refs, the sending end MUST\n NOT ask for delete command.\n \n+If the receiving end does not support push-cert, the sending end\n+MUST NOT send a push-cert command.  When a push-cert command is\n+sent, command-list MUST NOT be sent; the commands recorded in the\n+push certificate is used instead.\n+\n The pack-file MUST NOT be sent if the only command used is 'delete'.\n \n A pack-file MUST be sent if either create or update command is used,\n@@ -501,6 +514,24 @@ was being processed (the obj-id is still the same as the old-id), and\n it will run any update hooks to make sure that the update is acceptable.\n If all of that is fine, the server will then update the references.\n \n+Push Certificate\n+----------------\n+\n+A push certificate begins with a set of header lines.  After the\n+header and an empty line, the protocol commands follow, one per\n+line.\n+\n+Currently, the following header fields are defined:\n+\n+`pusher` ident::\n+\tIdentify the GPG key in \"Human Readable Name <email@address>\"\n+\tformat.\n+\n+The GPG signature lines are a detached signature for the contents\n+recorded in the push certificate before the signature block begins.\n+The detached signature is used to certify that the commands were\n+given by the pusher, who must be the signer.\n+\n Report Status\n -------------\n \ndiff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt\nindex e174343..a478cc4 100644\n--- a/Documentation/technical/protocol-capabilities.txt\n+++ b/Documentation/technical/protocol-capabilities.txt\n@@ -18,8 +18,8 @@ was sent.  Server MUST NOT ignore capabilities that client requested\n and server advertised.  As a consequence of these rules, server MUST\n NOT advertise capabilities it does not understand.\n \n-The 'report-status', 'delete-refs', and 'quiet' capabilities are sent and\n-recognized by the receive-pack (push to server) process.\n+The 'report-status', 'delete-refs', 'quiet', and 'push-cert' capabilities\n+are sent and recognized by the receive-pack (push to server) process.\n \n The 'ofs-delta' and 'side-band-64k' capabilities are sent and recognized\n by both upload-pack and receive-pack protocols.  The 'agent' capability\n@@ -250,3 +250,11 @@ allow-tip-sha1-in-want\n If the upload-pack server advertises this capability, fetch-pack may\n send \"want\" lines with SHA-1s that exist at the server but are not\n advertised by upload-pack.\n+\n+push-cert\n+---------\n+\n+The receive-pack server that advertises this capability is willing\n+to accept a signed push certificate.  A send-pack client MUST NOT\n+send a push-cert packet unless the receive-pack server advertises\n+this capability.\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex bfd078c..2d93c87 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -899,7 +899,7 @@ static void execute_commands(struct command *commands,\n \t\t      \"the reported refs above\");\n }\n \n-static struct command **queue_command(struct command **p,\n+static struct command **queue_command(struct command **tail,\n \t\t\t\t      const char *line,\n \t\t\t\t      int linelen)\n {\n@@ -922,10 +922,32 @@ static struct command **queue_command(struct command **p,\n \thashcpy(cmd->new_sha1, new_sha1);\n \tmemcpy(cmd->ref_name, refname, reflen);\n \tcmd->ref_name[reflen] = '\\0';\n-\t*p = cmd;\n+\t*tail = cmd;\n \treturn &cmd->next;\n }\n \n+static void queue_commands_from_cert(struct command **tail,\n+\t\t\t\t     struct strbuf *push_cert)\n+{\n+\tconst char *boc, *eoc;\n+\n+\tif (*tail)\n+\t\tdie(\"protocol error: got both push certificate and unsigned commands\");\n+\n+\tboc = strstr(push_cert->buf, \"\\n\\n\");\n+\tif (!boc)\n+\t\tdie(\"malformed push certificate %.*s\", 100, push_cert->buf);\n+\telse\n+\t\tboc += 2;\n+\teoc = push_cert->buf + parse_signature(push_cert->buf, push_cert->len);\n+\n+\twhile (boc < eoc) {\n+\t\tconst char *eol = memchr(boc, '\\n', eoc - boc);\n+\t\ttail = queue_command(tail, boc, eol ? eol - boc : eoc - eol);\n+\t\tboc = eol ? eol + 1 : eoc;\n+\t}\n+}\n+\n static struct command *read_head_info(struct sha1_array *shallow)\n {\n \tstruct command *commands = NULL;\n@@ -981,6 +1003,10 @@ static struct command *read_head_info(struct sha1_array *shallow)\n \n \t\tp = queue_command(p, line, linelen);\n \t}\n+\n+\tif (push_cert.len)\n+\t\tqueue_commands_from_cert(p, &push_cert);\n+\n \treturn commands;\n }\n \ndiff --git a/send-pack.c b/send-pack.c\nindex 4125892..61f321d 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -363,7 +363,7 @@ int send_pack(struct send_pack_args *args,\n \tfor (ref = remote_refs; ref; ref = ref->next) {\n \t\tchar *old_hex, *new_hex;\n \n-\t\tif (args->dry_run)\n+\t\tif (args->dry_run || args->push_cert)\n \t\t\tcontinue;\n \n \t\tif (!ref_update_to_be_sent(ref, args))\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248864","messageId":"1409861097-19151-21-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 20/21] signed push: add \"pushee\" header to push certificate","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:56Z","receivedAt":"2014-09-04T20:04:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Record the URL of the intended recipient for a push (after\nanonymizing it if it has authentication material) on a new \"pushee\nURL\" header.  Because the networking configuration (SSH-tunnels,\nproxies, etc.) on the pushing user's side varies, the receiving\nrepository may not know the single canonical URL all the pushing\nusers would refer it as (besides, many sites allow pushing over\nssh://host/path and https://host/path protocols to the same\nrepository but with different local part of the path).  So this\nvalue may not be reliably used for replay-attack prevention\npurposes, but this will still serve as a human readable hint to\nidentify the repository the certificate refers to.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/technical/pack-protocol.txt | 6 ++++++\n send-pack.c                               | 5 +++++\n send-pack.h                               | 1 +\n transport.c                               | 1 +\n 4 files changed, 13 insertions(+)\n\ndiff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt\nindex 4a5c2e8..7b543dc 100644\n--- a/Documentation/technical/pack-protocol.txt\n+++ b/Documentation/technical/pack-protocol.txt\n@@ -484,6 +484,7 @@ references.\n   push-cert         = PKT-LINE(\"push-cert\" NUL capability-list LF)\n \t\t      PKT-LINE(\"certificate version 0.1\" LF)\n \t\t      PKT-LINE(\"pusher\" SP ident LF)\n+\t\t      PKT-LINE(\"pushee\" SP url LF)\n \t\t      PKT-LINE(LF)\n \t\t      *PKT-LINE(command LF)\n \t\t      *PKT-LINE(gpg-signature-lines LF)\n@@ -527,6 +528,11 @@ Currently, the following header fields are defined:\n \tIdentify the GPG key in \"Human Readable Name <email@address>\"\n \tformat.\n \n+`pushee` url::\n+\tThe repository URL (anonymized, if the URL contains\n+\tauthentication material) the user who ran `git push`\n+\tintended to push into.\n+\n The GPG signature lines are a detached signature for the contents\n recorded in the push certificate before the signature block begins.\n The detached signature is used to certify that the commands were\ndiff --git a/send-pack.c b/send-pack.c\nindex 61f321d..642ebc8 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -240,6 +240,11 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tdatestamp(stamp, sizeof(stamp));\n \tstrbuf_addf(&cert, \"certificate version 0.1\\n\");\n \tstrbuf_addf(&cert, \"pusher %s %s\\n\", signing_key, stamp);\n+\tif (args->url && *args->url) {\n+\t\tchar *anon_url = transport_anonymize_url(args->url);\n+\t\tstrbuf_addf(&cert, \"pushee %s\\n\", anon_url);\n+\t\tfree(anon_url);\n+\t}\n \tstrbuf_addstr(&cert, \"\\n\");\n \n \tfor (ref = remote_refs; ref; ref = ref->next) {\ndiff --git a/send-pack.h b/send-pack.h\nindex 3555d8e..5635457 100644\n--- a/send-pack.h\n+++ b/send-pack.h\n@@ -2,6 +2,7 @@\n #define SEND_PACK_H\n \n struct send_pack_args {\n+\tconst char *url;\n \tunsigned verbose:1,\n \t\tquiet:1,\n \t\tporcelain:1,\ndiff --git a/transport.c b/transport.c\nindex 07fdf86..1df1375 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -827,6 +827,7 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re\n \targs.dry_run = !!(flags & TRANSPORT_PUSH_DRY_RUN);\n \targs.porcelain = !!(flags & TRANSPORT_PUSH_PORCELAIN);\n \targs.push_cert = !!(flags & TRANSPORT_PUSH_CERT);\n+\targs.url = transport->url;\n \n \tret = send_pack(&args, data->fd, data->conn, remote_refs,\n \t\t\t&data->extra_have);\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248869","messageId":"1409861097-19151-22-git-send-email-gitster@pobox.com","threadId":"37490","inReplyTo":"1409861097-19151-1-git-send-email-gitster@pobox.com","subject":"[PATCH v3 21/21] signed push: fortify against replay attacks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-04T20:04:57Z","receivedAt":"2014-09-04T20:04:57Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"In order to prevent a valid push certificate for pushing into an\nrepository from getting replayed to push to an unrelated one, send a\nnonce string from the receive-pack process and have the signer\ninclude it in the push certificate.  The original nonce is exported\nas GIT_PUSH_CERT_NONCE for the hooks to examine and match against\nthe value on the \"nonce\" header in the certificate to notice a\nreplay.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/git-receive-pack.txt                |  8 ++++++++\n Documentation/technical/pack-protocol.txt         |  6 ++++++\n Documentation/technical/protocol-capabilities.txt |  7 ++++---\n builtin/receive-pack.c                            | 21 +++++++++++++++++++--\n send-pack.c                                       | 19 +++++++++++++++----\n t/t5534-push-signed.sh                            | 17 +++++++++++------\n 6 files changed, 63 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt\nindex 60151a6..983b24e 100644\n--- a/Documentation/git-receive-pack.txt\n+++ b/Documentation/git-receive-pack.txt\n@@ -72,6 +72,13 @@ GIT_PUSH_CERT_STATUS::\n \tusing the same mnemonic as used in `%G?` format of `git log`\n \tfamily of commands (see linkgit:git-log[1]).\n \n+GIT_PUSH_CERT_NONCE::\n+\tThe nonce string the process asked the signer to include\n+\tin the push certificate.  If this does not match the value\n+\trecorded on the \"nonce\" header in the push certificate, it\n+\tmay indicate that the certificate is a valid one that is\n+\tbeing replayed from a separate \"git push\" session.\n+\n This hook is called before any refname is updated and before any\n fast-forward checks are performed.\n \n@@ -147,6 +154,7 @@ service:\n \tif test -n \"${GIT_PUSH_CERT-}\" && test ${GIT_PUSH_CERT_STATUS} = G\n \tthen\n \t\t(\n+\t\t\techo expected nonce is ${GIT_PUSH_NONCE}\n \t\t\tgit cat-file blob ${GIT_PUSH_CERT}\n \t\t) | mail -s \"push from $GIT_PUSH_CERT_SIGNER\" push-log@mydomain\n \tfi\ndiff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt\nindex 7b543dc..dda1206 100644\n--- a/Documentation/technical/pack-protocol.txt\n+++ b/Documentation/technical/pack-protocol.txt\n@@ -485,6 +485,7 @@ references.\n \t\t      PKT-LINE(\"certificate version 0.1\" LF)\n \t\t      PKT-LINE(\"pusher\" SP ident LF)\n \t\t      PKT-LINE(\"pushee\" SP url LF)\n+\t\t      PKT-LINE(\"nonce\" SP nonce LF)\n \t\t      PKT-LINE(LF)\n \t\t      *PKT-LINE(command LF)\n \t\t      *PKT-LINE(gpg-signature-lines LF)\n@@ -533,6 +534,11 @@ Currently, the following header fields are defined:\n \tauthentication material) the user who ran `git push`\n \tintended to push into.\n \n+`nonce` nonce::\n+\tThe 'nonce' string the receiving repository asked the\n+\tpushing user to include in the certificate, to prevent\n+\treplay attacks.\n+\n The GPG signature lines are a detached signature for the contents\n recorded in the push certificate before the signature block begins.\n The detached signature is used to certify that the commands were\ndiff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt\nindex a478cc4..0c92dee 100644\n--- a/Documentation/technical/protocol-capabilities.txt\n+++ b/Documentation/technical/protocol-capabilities.txt\n@@ -251,10 +251,11 @@ If the upload-pack server advertises this capability, fetch-pack may\n send \"want\" lines with SHA-1s that exist at the server but are not\n advertised by upload-pack.\n \n-push-cert\n----------\n+push-cert=<nonce>\n+-----------------\n \n The receive-pack server that advertises this capability is willing\n-to accept a signed push certificate.  A send-pack client MUST NOT\n+to accept a signed push certificate, and asks the <nonce> to be\n+included in the push certificate.  A send-pack client MUST NOT\n send a push-cert packet unless the receive-pack server advertises\n this capability.\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 2d93c87..9abfc94 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -52,6 +52,7 @@ static int accept_push_cert = 1;\n static struct strbuf push_cert = STRBUF_INIT;\n static unsigned char push_cert_sha1[20];\n static struct signature_check sigcheck;\n+static const char *push_cert_nonce;\n \n static enum deny_action parse_deny_action(const char *var, const char *value)\n {\n@@ -157,8 +158,8 @@ static void show_ref(const char *path, const unsigned char *sha1)\n \t\t\t      \"report-status delete-refs side-band-64k quiet\");\n \t\tif (prefer_ofs_delta)\n \t\t\tstrbuf_addstr(&cap, \" ofs-delta\");\n-\t\tif (accept_push_cert)\n-\t\t\tstrbuf_addstr(&cap, \" push-cert\");\n+\t\tif (accept_push_cert && push_cert_nonce)\n+\t\t\tstrbuf_addf(&cap, \" push-cert=%s\", push_cert_nonce);\n \t\tstrbuf_addf(&cap, \" agent=%s\", git_user_agent_sanitized());\n \t\tpacket_write(1, \"%s %s%c%s\\n\",\n \t\t\t     sha1_to_hex(sha1), path, 0, cap.buf);\n@@ -313,6 +314,8 @@ static void prepare_push_cert_sha1(struct child_process *proc)\n \t\targv_array_pushf(&env, \"GIT_PUSH_CERT_KEY=%s\",\n \t\t\t\t sigcheck.key ? sigcheck.key : \"\");\n \t\targv_array_pushf(&env, \"GIT_PUSH_CERT_STATUS=%c\", sigcheck.result);\n+\t\tif (push_cert_nonce)\n+\t\t\targv_array_pushf(&env, \"GIT_PUSH_CERT_NONCE=%s\", push_cert_nonce);\n \n \t\tproc->env = env.argv;\n \t}\n@@ -1245,6 +1248,17 @@ static int delete_only(struct command *commands)\n \treturn 1;\n }\n \n+static char *prepare_push_cert_nonce(const char *dir)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tunsigned char sha1[20];\n+\n+\tstrbuf_addf(&buf, \"%s:%lu\", dir, time(NULL));\n+\thash_sha1_file(buf.buf, buf.len, \"blob\", sha1);\n+\tstrbuf_release(&buf);\n+\treturn xstrdup(sha1_to_hex(sha1));\n+}\n+\n int cmd_receive_pack(int argc, const char **argv, const char *prefix)\n {\n \tint advertise_refs = 0;\n@@ -1304,6 +1318,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)\n \t\tdie(\"'%s' does not appear to be a git repository\", dir);\n \n \tgit_config(receive_pack_config, NULL);\n+\tif (accept_push_cert && !push_cert_nonce)\n+\t\tpush_cert_nonce = prepare_push_cert_nonce(dir);\n \n \tif (0 <= transfer_unpack_limit)\n \t\tunpack_limit = transfer_unpack_limit;\n@@ -1348,5 +1364,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)\n \t\tpacket_flush(1);\n \tsha1_array_clear(&shallow);\n \tsha1_array_clear(&ref);\n+\tfree((void *)push_cert_nonce);\n \treturn 0;\n }\ndiff --git a/send-pack.c b/send-pack.c\nindex 642ebc8..141a6b2 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -228,7 +228,8 @@ static const char *next_line(const char *line, size_t len)\n static int generate_push_cert(struct strbuf *req_buf,\n \t\t\t      const struct ref *remote_refs,\n \t\t\t      struct send_pack_args *args,\n-\t\t\t      const char *cap_string)\n+\t\t\t      const char *cap_string,\n+\t\t\t      const char *push_cert_nonce)\n {\n \tconst struct ref *ref;\n \tchar stamp[60];\n@@ -245,6 +246,8 @@ static int generate_push_cert(struct strbuf *req_buf,\n \t\tstrbuf_addf(&cert, \"pushee %s\\n\", anon_url);\n \t\tfree(anon_url);\n \t}\n+\tif (push_cert_nonce[0])\n+\t\tstrbuf_addf(&cert, \"nonce %s\\n\", push_cert_nonce);\n \tstrbuf_addstr(&cert, \"\\n\");\n \n \tfor (ref = remote_refs; ref; ref = ref->next) {\n@@ -295,6 +298,8 @@ int send_pack(struct send_pack_args *args,\n \tunsigned cmds_sent = 0;\n \tint ret;\n \tstruct async demux;\n+\tconst char *push_cert_nonce = NULL;\n+\n \n \t/* Does the other end support the reporting? */\n \tif (server_supports(\"report-status\"))\n@@ -311,8 +316,14 @@ int send_pack(struct send_pack_args *args,\n \t\tagent_supported = 1;\n \tif (server_supports(\"no-thin\"))\n \t\targs->use_thin_pack = 0;\n-\tif (args->push_cert && !server_supports(\"push-cert\"))\n-\t\tdie(_(\"the receiving end does not support --signed push\"));\n+\tif (args->push_cert) {\n+\t\tint len;\n+\n+\t\tpush_cert_nonce = server_feature_value(\"push-cert\", &len);\n+\t\tif (!push_cert_nonce)\n+\t\t\tdie(_(\"the receiving end does not support --signed push\"));\n+\t\tpush_cert_nonce = xmemdupz(push_cert_nonce, len);\n+\t}\n \n \tif (!remote_refs) {\n \t\tfprintf(stderr, \"No refs in common and none specified; doing nothing.\\n\"\n@@ -343,7 +354,7 @@ int send_pack(struct send_pack_args *args,\n \n \tif (!args->dry_run && args->push_cert)\n \t\tcmds_sent = generate_push_cert(&req_buf, remote_refs, args,\n-\t\t\t\t\t       cap_buf.buf);\n+\t\t\t\t\t       cap_buf.buf, push_cert_nonce);\n \n \t/*\n \t * Clear the status for each ref and see if we need to send\ndiff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh\nindex 35fe704..66fdaf2 100755\n--- a/t/t5534-push-signed.sh\n+++ b/t/t5534-push-signed.sh\n@@ -72,17 +72,22 @@ test_expect_success GPG 'signed push sends push certificate' '\n \tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n \tKEY=${GIT_PUSH_CERT_KEY-nokey}\n \tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n \tE_O_F\n \n \tEOF\n \n-\tcat >expect <<-\\EOF &&\n-\tSIGNER=C O Mitter <committer@example.com>\n-\tKEY=13B6F51ECDDE430D\n-\tSTATUS=G\n-\tEOF\n-\n \tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=C O Mitter <committer@example.com>\n+\t\tKEY=13B6F51ECDDE430D\n+\t\tSTATUS=G\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) >expect &&\n+\n \tgrep \"$(git rev-parse noop ff) refs/heads/ff\" dst/push-cert &&\n \tgrep \"$(git rev-parse noop noff) refs/heads/noff\" dst/push-cert &&\n \ttest_cmp expect dst/push-cert-status\n-- \n2.1.0-399-g1364b4d\n"},{"id":"248873","messageId":"CAJo=hJsd8wfqvxBMThwh76BqTD9eZza98PRKOfgtr=-eQkUh3A@mail.gmail.com","threadId":"37490","inReplyTo":"1409861097-19151-21-git-send-email-gitster@pobox.com","subject":"Re: [PATCH v3 20/21] signed push: add \"pushee\" header to push certificate","fromName":"Shawn Pearce","fromEmail":"spearce@spearce.org","sentAt":"2014-09-04T21:13:57Z","receivedAt":"2014-09-04T21:13:57Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"On Thu, Sep 4, 2014 at 1:04 PM, Junio C Hamano <gitster@pobox.com> wrote:\n> Record the URL of the intended recipient for a push (after\n> anonymizing it if it has authentication material) on a new \"pushee\n> URL\" header.  Because the networking configuration (SSH-tunnels,\n> proxies, etc.) on the pushing user's side varies, the receiving\n> repository may not know the single canonical URL all the pushing\n> users would refer it as (besides, many sites allow pushing over\n> ssh://host/path and https://host/path protocols to the same\n> repository but with different local part of the path).  So this\n> value may not be reliably used for replay-attack prevention\n> purposes, but this will still serve as a human readable hint to\n> identify the repository the certificate refers to.\n\nWell, a push cert validator could require the pushee use specific\nURL(s) known to it. The server operator knows their URL space and\ncould just demand that users use ssh://host/path and https://host/path\nand nothing fancy like aliases in ~/.ssh/config.\n\nBut I think you are right to punt on that and let the cert validator\nhook written by the server operator to decide if pushee should be\nverified.\n"}]}