diff options
52 files changed, 1265 insertions, 259 deletions
diff --git a/Documentation/RelNotes/2.2.0.txt b/Documentation/RelNotes/2.2.0.txt index 438f92e344..5f0c421365 100644 --- a/Documentation/RelNotes/2.2.0.txt +++ b/Documentation/RelNotes/2.2.0.txt @@ -12,6 +12,8 @@ Ports UI, Workflows & Features + * "git archive" learned to filter what gets archived with pathspec. + * "git config --edit --global" starts from a skeletal per-user configuration file contents, instead of a total blank, when the user does not already have any. This immediately reduces the @@ -48,6 +50,14 @@ UI, Workflows & Features for a tagged commit, gained a cousin "%D" that just gives the "tagname" without frills. + * "git push" learned "--signed" push, that allows a push (i.e. + request to update the refs on the other side to point at a new + history, together with the transmission of necessary objects) to be + signed, so that it can be verified and audited, using the GPG + signature of the person who pushed, that the tips of branches at a + public repository really point the commits the pusher wanted to, + without having to "trust" the server. + Performance, Internal Implementation, etc. * The API to manipulate the "refs" is currently undergoing a revamp @@ -108,6 +118,9 @@ Performance, Internal Implementation, etc. * "git hash-object" was taught a "--literally" option to help debugging. + * When running a required clean filter, we do not have to mmap the + original before feeding the filter. Instead, stream the file + contents directly to the filter and process its output. Also contains various documentation updates and code clean-ups. diff --git a/Documentation/config.txt b/Documentation/config.txt index 3b5b24aeb7..04a1e2f37e 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -2044,6 +2044,25 @@ receive.autogc:: receiving data from git-push and updating refs. You can stop it by setting this variable to false. +receive.certnonceseed:: + By setting this variable to a string, `git receive-pack` + will accept a `git push --signed` and verifies it by using + a "nonce" protected by HMAC using this string as a secret + key. + +receive.certnonceslop:: + When a `git push --signed` sent a push certificate with a + "nonce" that was issued by a receive-pack serving the same + repository within this many seconds, export the "nonce" + found in the certificate to `GIT_PUSH_CERT_NONCE` to the + hooks (instead of what the receive-pack asked the sending + side to include). This may allow writing checks in + `pre-receive` and `post-receive` a bit easier. Instead of + checking `GIT_PUSH_CERT_NONCE_SLOP` environment variable + that records by how many seconds the nonce is stale to + decide if they want to accept the certificate, they only + can check `GIT_PUSH_CERT_NONCE_STATUS` is `OK`. + receive.fsckObjects:: If it is set to true, git-receive-pack will check all received objects. It will abort in the case of a malformed object or a diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt index c0d7403b9a..b17283ab7a 100644 --- a/Documentation/git-push.txt +++ b/Documentation/git-push.txt @@ -10,7 +10,8 @@ SYNOPSIS -------- [verse] 'git push' [--all | --mirror | --tags] [--follow-tags] [-n | --dry-run] [--receive-pack=<git-receive-pack>] - [--repo=<repository>] [-f | --force] [--prune] [-v | --verbose] [-u | --set-upstream] + [--repo=<repository>] [-f | --force] [--prune] [-v | --verbose] + [-u | --set-upstream] [--signed] [--force-with-lease[=<refname>[:<expect>]]] [--no-verify] [<repository> [<refspec>...]] @@ -129,6 +130,12 @@ already exists on the remote side. from the remote but are pointing at commit-ish that are reachable from the refs being pushed. +--signed:: + GPG-sign the push request to update refs on the receiving + side, to allow it to be checked by the hooks and/or be + logged. See linkgit:git-receive-pack[1] for the details + on the receiving end. + --receive-pack=<git-receive-pack>:: --exec=<git-receive-pack>:: Path to the 'git-receive-pack' program on the remote diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index 4138554912..924827dc2e 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -21,15 +21,17 @@ If <branch> is specified, 'git rebase' will perform an automatic it remains on the current branch. If <upstream> is not specified, the upstream configured in -branch.<name>.remote and branch.<name>.merge options will be used; see -linkgit:git-config[1] for details. If you are currently not on any -branch or if the current branch does not have a configured upstream, -the rebase will abort. +branch.<name>.remote and branch.<name>.merge options will be used (see +linkgit:git-config[1] for details) and the `--fork-point` option is +assumed. If you are currently not on any branch or if the current +branch does not have a configured upstream, the rebase will abort. All changes made by commits in the current branch but that are not in <upstream> are saved to a temporary area. This is the same set -of commits that would be shown by `git log <upstream>..HEAD` (or -`git log HEAD`, if --root is specified). +of commits that would be shown by `git log <upstream>..HEAD`; or by +`git log 'fork_point'..HEAD`, if `--fork-point` is active (see the +description on `--fork-point` below); or by `git log HEAD`, if the +`--root` option is specified. The current branch is reset to <upstream>, or <newbase> if the --onto option was supplied. This has the exact same effect as @@ -327,13 +329,18 @@ link:howto/revert-a-faulty-merge.html[revert-a-faulty-merge How-To] for details) --fork-point:: --no-fork-point:: - Use 'git merge-base --fork-point' to find a better common ancestor - between `upstream` and `branch` when calculating which commits have - have been introduced by `branch` (see linkgit:git-merge-base[1]). + Use reflog to find a better common ancestor between <upstream> + and <branch> when calculating which commits have been + introduced by <branch>. + -If no non-option arguments are given on the command line, then the default is -`--fork-point @{u}` otherwise the `upstream` argument is interpreted literally -unless the `--fork-point` option is specified. +When --fork-point is active, 'fork_point' will be used instead of +<upstream> to calculate the set of commits to rebase, where +'fork_point' is the result of `git merge-base --fork-point <upstream> +<branch>` command (see linkgit:git-merge-base[1]). If 'fork_point' +ends up being empty, the <upstream> will be used as a fallback. ++ +If either <upstream> or --root is given on the command line, then the +default is `--no-fork-point`, otherwise the default is `--fork-point`. --ignore-whitespace:: --whitespace=<option>:: diff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt index b1f7dc643a..9016960e27 100644 --- a/Documentation/git-receive-pack.txt +++ b/Documentation/git-receive-pack.txt @@ -53,6 +53,56 @@ the update. Refs to be created will have sha1-old equal to 0\{40}, while refs to be deleted will have sha1-new equal to 0\{40}, otherwise sha1-old and sha1-new should be valid objects in the repository. +When accepting a signed push (see linkgit:git-push[1]), the signed +push certificate is stored in a blob and an environment variable +`GIT_PUSH_CERT` can be consulted for its object name. See the +description of `post-receive` hook for an example. In addition, the +certificate is verified using GPG and the result is exported with +the following environment variables: + +`GIT_PUSH_CERT_SIGNER`:: + The name and the e-mail address of the owner of the key that + signed the push certificate. + +`GIT_PUSH_CERT_KEY`:: + The GPG key ID of the key that signed the push certificate. + +`GIT_PUSH_CERT_STATUS`:: + The status of GPG verification of the push certificate, + using the same mnemonic as used in `%G?` format of `git log` + family of commands (see linkgit:git-log[1]). + +`GIT_PUSH_CERT_NONCE`:: + The nonce string the process asked the signer to include + in the push certificate. If this does not match the value + recorded on the "nonce" header in the push certificate, it + may indicate that the certificate is a valid one that is + being replayed from a separate "git push" session. + +`GIT_PUSH_CERT_NONCE_STATUS`:: +`UNSOLICITED`;; + "git push --signed" sent a nonce when we did not ask it to + send one. +`MISSING`;; + "git push --signed" did not send any nonce header. +`BAD`;; + "git push --signed" sent a bogus nonce. +`OK`;; + "git push --signed" sent the nonce we asked it to send. +`SLOP`;; + "git push --signed" sent a nonce different from what we + asked it to send now, but in a previous session. See + `GIT_PUSH_CERT_NONCE_SLOP` environment variable. + +`GIT_PUSH_CERT_NONCE_SLOP`:: + "git push --signed" sent a nonce different from what we + asked it to send now, but in a different session whose + starting time is different by this many seconds from the + current session. Only meaningful when + `GIT_PUSH_CERT_NONCE_STATUS` says `SLOP`. + Also read about `receive.certnonceslop` variable in + linkgit:git-config[1]. + This hook is called before any refname is updated and before any fast-forward checks are performed. @@ -101,9 +151,14 @@ the update. Refs that were created will have sha1-old equal to 0\{40}, otherwise sha1-old and sha1-new should be valid objects in the repository. +The `GIT_PUSH_CERT*` environment variables can be inspected, just as +in `pre-receive` hook, after accepting a signed push. + Using this hook, it is easy to generate mails describing the updates to the repository. This example script sends one mail message per -ref listing the commits pushed to the repository: +ref listing the commits pushed to the repository, and logs the push +certificates of signed pushes with good signatures to a logger +service: #!/bin/sh # mail out commit update information. @@ -119,6 +174,14 @@ ref listing the commits pushed to the repository: fi | mail -s "Changes to ref $ref" commit-list@mydomain done + # log signed push certificate, if any + if test -n "${GIT_PUSH_CERT-}" && test ${GIT_PUSH_CERT_STATUS} = G + then + ( + echo expected nonce is ${GIT_PUSH_NONCE} + git cat-file blob ${GIT_PUSH_CERT} + ) | mail -s "push certificate from $GIT_PUSH_CERT_SIGNER" push-log@mydomain + fi exit 0 The exit code from this hook invocation is ignored, however a diff --git a/Documentation/git-tag.txt b/Documentation/git-tag.txt index 320908369f..e953ba4439 100644 --- a/Documentation/git-tag.txt +++ b/Documentation/git-tag.txt @@ -42,7 +42,7 @@ committer identity for the current user is used to find the GnuPG key for signing. The configuration variable `gpg.program` is used to specify custom GnuPG binary. -Tag objects (created with `-a`, `s`, or `-u`) are called "annotated" +Tag objects (created with `-a`, `-s`, or `-u`) are called "annotated" tags; they contain a creation date, the tagger name and e-mail, a tagging message, and an optional GnuPG signature. Whereas a "lightweight" tag is simply a name for an object (usually a commit diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt index 569c48a352..462e20645f 100644 --- a/Documentation/technical/pack-protocol.txt +++ b/Documentation/technical/pack-protocol.txt @@ -212,9 +212,9 @@ out of what the server said it could do with the first 'want' line. want-list = first-want *additional-want - shallow-line = PKT_LINE("shallow" SP obj-id) + shallow-line = PKT-LINE("shallow" SP obj-id) - depth-request = PKT_LINE("deepen" SP depth) + depth-request = PKT-LINE("deepen" SP depth) first-want = PKT-LINE("want" SP obj-id SP capability-list LF) additional-want = PKT-LINE("want" SP obj-id LF) @@ -465,7 +465,7 @@ contain all the objects that the server will need to complete the new references. ---- - update-request = *shallow command-list [pack-file] + update-request = *shallow ( command-list | push-cert ) [pack-file] shallow = PKT-LINE("shallow" SP obj-id LF) @@ -481,12 +481,27 @@ references. old-id = obj-id new-id = obj-id + push-cert = PKT-LINE("push-cert" NUL capability-list LF) + PKT-LINE("certificate version 0.1" LF) + PKT-LINE("pusher" SP ident LF) + PKT-LINE("pushee" SP url LF) + PKT-LINE("nonce" SP nonce LF) + PKT-LINE(LF) + *PKT-LINE(command LF) + *PKT-LINE(gpg-signature-lines LF) + PKT-LINE("push-cert-end" LF) + pack-file = "PACK" 28*(OCTET) ---- If the receiving end does not support delete-refs, the sending end MUST NOT ask for delete command. +If the receiving end does not support push-cert, the sending end +MUST NOT send a push-cert command. When a push-cert command is +sent, command-list MUST NOT be sent; the commands recorded in the +push certificate is used instead. + The pack-file MUST NOT be sent if the only command used is 'delete'. A pack-file MUST be sent if either create or update command is used, @@ -501,6 +516,34 @@ was being processed (the obj-id is still the same as the old-id), and it will run any update hooks to make sure that the update is acceptable. If all of that is fine, the server will then update the references. +Push Certificate +---------------- + +A push certificate begins with a set of header lines. After the +header and an empty line, the protocol commands follow, one per +line. + +Currently, the following header fields are defined: + +`pusher` ident:: + Identify the GPG key in "Human Readable Name <email@address>" + format. + +`pushee` url:: + The repository URL (anonymized, if the URL contains + authentication material) the user who ran `git push` + intended to push into. + +`nonce` nonce:: + The 'nonce' string the receiving repository asked the + pushing user to include in the certificate, to prevent + replay attacks. + +The GPG signature lines are a detached signature for the contents +recorded in the push certificate before the signature block begins. +The detached signature is used to certify that the commands were +given by the pusher, who must be the signer. + Report Status ------------- diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt index e174343847..0c92deebcc 100644 --- a/Documentation/technical/protocol-capabilities.txt +++ b/Documentation/technical/protocol-capabilities.txt @@ -18,8 +18,8 @@ was sent. Server MUST NOT ignore capabilities that client requested and server advertised. As a consequence of these rules, server MUST NOT advertise capabilities it does not understand. -The 'report-status', 'delete-refs', and 'quiet' capabilities are sent and -recognized by the receive-pack (push to server) process. +The 'report-status', 'delete-refs', 'quiet', and 'push-cert' capabilities +are sent and recognized by the receive-pack (push to server) process. The 'ofs-delta' and 'side-band-64k' capabilities are sent and recognized by both upload-pack and receive-pack protocols. The 'agent' capability @@ -250,3 +250,12 @@ allow-tip-sha1-in-want If the upload-pack server advertises this capability, fetch-pack may send "want" lines with SHA-1s that exist at the server but are not advertised by upload-pack. + +push-cert=<nonce> +----------------- + +The receive-pack server that advertises this capability is willing +to accept a signed push certificate, and asks the <nonce> to be +included in the push certificate. A send-pack client MUST NOT +send a push-cert packet unless the receive-pack server advertises +this capability. @@ -5,6 +5,7 @@ #include "archive.h" #include "parse-options.h" #include "unpack-trees.h" +#include "dir.h" static char const * const archive_usage[] = { N_("git archive [options] <tree-ish> [<path>...]"), @@ -98,9 +99,19 @@ static void setup_archive_check(struct git_attr_check *check) check[1].attr = attr_export_subst; } +struct directory { + struct directory *up; + unsigned char sha1[20]; + int baselen, len; + unsigned mode; + int stage; + char path[FLEX_ARRAY]; +}; + struct archiver_context { struct archiver_args *args; write_archive_entry_fn_t write_entry; + struct directory *bottom; }; static int write_archive_entry(const unsigned char *sha1, const char *base, @@ -146,6 +157,65 @@ static int write_archive_entry(const unsigned char *sha1, const char *base, return write_entry(args, sha1, path.buf, path.len, mode); } +static void queue_directory(const unsigned char *sha1, + const char *base, int baselen, const char *filename, + unsigned mode, int stage, struct archiver_context *c) +{ + struct directory *d; + d = xmallocz(sizeof(*d) + baselen + 1 + strlen(filename)); + d->up = c->bottom; + d->baselen = baselen; + d->mode = mode; + d->stage = stage; + c->bottom = d; + d->len = sprintf(d->path, "%.*s%s/", baselen, base, filename); + hashcpy(d->sha1, sha1); +} + +static int write_directory(struct archiver_context *c) +{ + struct directory *d = c->bottom; + int ret; + + if (!d) + return 0; + c->bottom = d->up; + d->path[d->len - 1] = '\0'; /* no trailing slash */ + ret = + write_directory(c) || + write_archive_entry(d->sha1, d->path, d->baselen, + d->path + d->baselen, d->mode, + d->stage, c) != READ_TREE_RECURSIVE; + free(d); + return ret ? -1 : 0; +} + +static int queue_or_write_archive_entry(const unsigned char *sha1, + const char *base, int baselen, const char *filename, + unsigned mode, int stage, void *context) +{ + struct archiver_context *c = context; + + while (c->bottom && + !(baselen >= c->bottom->len && + !strncmp(base, c->bottom->path, c->bottom->len))) { + struct directory *next = c->bottom->up; + free(c->bottom); + c->bottom = next; + } + + if (S_ISDIR(mode)) { + queue_directory(sha1, base, baselen, filename, + mode, stage, c); + return READ_TREE_RECURSIVE; + } + + if (write_directory(c)) + return -1; + return write_archive_entry(sha1, base, baselen, filename, mode, + stage, context); +} + int write_archive_entries(struct archiver_args *args, write_archive_entry_fn_t write_entry) { @@ -167,6 +237,7 @@ int write_archive_entries(struct archiver_args *args, return err; } + memset(&context, 0, sizeof(context)); context.args = args; context.write_entry = write_entry; @@ -187,9 +258,17 @@ int write_archive_entries(struct archiver_args *args, } err = read_tree_recursive(args->tree, "", 0, 0, &args->pathspec, - write_archive_entry, &context); + args->pathspec.has_wildcard ? + queue_or_write_archive_entry : + write_archive_entry, + &context); if (err == READ_TREE_RECURSIVE) err = 0; + while (context.bottom) { + struct directory *next = context.bottom->up; + free(context.bottom); + context.bottom = next; + } return err; } @@ -211,7 +290,16 @@ static int reject_entry(const unsigned char *sha1, const char *base, int baselen, const char *filename, unsigned mode, int stage, void *context) { - return -1; + int ret = -1; + if (S_ISDIR(mode)) { + struct strbuf sb = STRBUF_INIT; + strbuf_addstr(&sb, base); + strbuf_addstr(&sb, filename); + if (!match_pathspec(context, sb.buf, sb.len, 0, NULL, 1)) + ret = READ_TREE_RECURSIVE; + strbuf_release(&sb); + } + return ret; } static int path_exists(struct tree *tree, const char *path) @@ -221,7 +309,9 @@ static int path_exists(struct tree *tree, const char *path) int ret; parse_pathspec(&pathspec, 0, 0, "", paths); - ret = read_tree_recursive(tree, "", 0, 0, &pathspec, reject_entry, NULL); + pathspec.recursive = 1; + ret = read_tree_recursive(tree, "", 0, 0, &pathspec, + reject_entry, &pathspec); free_pathspec(&pathspec); return ret != 0; } @@ -237,6 +327,7 @@ static void parse_pathspec_arg(const char **pathspec, parse_pathspec(&ar_args->pathspec, 0, PATHSPEC_PREFER_FULL, "", pathspec); + ar_args->pathspec.recursive = 1; if (pathspec) { while (*pathspec) { if (**pathspec && !path_exists(ar_args->tree, *pathspec)) diff --git a/builtin/push.c b/builtin/push.c index f50e3d5e77..ae56f73a66 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -506,6 +506,7 @@ int cmd_push(int argc, const char **argv, const char *prefix) OPT_BIT(0, "no-verify", &flags, N_("bypass pre-push hook"), TRANSPORT_PUSH_NO_HOOK), OPT_BIT(0, "follow-tags", &flags, N_("push missing but relevant tags"), TRANSPORT_PUSH_FOLLOW_TAGS), + OPT_BIT(0, "signed", &flags, N_("GPG sign the push"), TRANSPORT_PUSH_CERT), OPT_END() }; diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 10fa25d7b5..f2f6c67359 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -16,6 +16,8 @@ #include "connected.h" #include "argv-array.h" #include "version.h" +#include "tag.h" +#include "gpg-interface.h" #include "sigchain.h" static const char receive_pack_usage[] = "git receive-pack <git-dir>"; @@ -43,11 +45,27 @@ static int prefer_ofs_delta = 1; static int auto_update_server_info; static int auto_gc = 1; static int fix_thin = 1; +static int stateless_rpc; +static const char *service_dir; static const char *head_name; static void *head_name_to_free; static int sent_capabilities; static int shallow_update; static const char *alt_shallow_file; +static struct strbuf push_cert = STRBUF_INIT; +static unsigned char push_cert_sha1[20]; +static struct signature_check sigcheck; +static const char *push_cert_nonce; +static const char *cert_nonce_seed; + +static const char *NONCE_UNSOLICITED = "UNSOLICITED"; +static const char *NONCE_BAD = "BAD"; +static const char *NONCE_MISSING = "MISSING"; +static const char *NONCE_OK = "OK"; +static const char *NONCE_SLOP = "SLOP"; +static const char *nonce_status; +static long nonce_stamp_slop; +static unsigned long nonce_stamp_slop_limit; static enum deny_action parse_deny_action(const char *var, const char *value) { @@ -131,6 +149,14 @@ static int receive_pack_config(const char *var, const char *value, void *cb) return 0; } + if (strcmp(var, "receive.certnonceseed") == 0) + return git_config_string(&cert_nonce_seed, var, value); + + if (strcmp(var, "receive.certnonceslop") == 0) { + nonce_stamp_slop_limit = git_config_ulong(var, value); + return 0; + } + return git_default_config(var, value, cb); } @@ -139,15 +165,23 @@ static void show_ref(const char *path, const unsigned char *sha1) if (ref_is_hidden(path)) return; - if (sent_capabilities) + if (sent_capabilities) { packet_write(1, "%s %s\n", sha1_to_hex(sha1), path); - else - packet_write(1, "%s %s%c%s%s agent=%s\n", - sha1_to_hex(sha1), path, 0, - " report-status delete-refs side-band-64k quiet", - prefer_ofs_delta ? " ofs-delta" : "", - git_user_agent_sanitized()); - sent_capabilities = 1; + } else { + struct strbuf cap = STRBUF_INIT; + + strbuf_addstr(&cap, + "report-status delete-refs side-band-64k quiet"); + if (prefer_ofs_delta) + strbuf_addstr(&cap, " ofs-delta"); + if (push_cert_nonce) + strbuf_addf(&cap, " push-cert=%s", push_cert_nonce); + strbuf_addf(&cap, " agent=%s", git_user_agent_sanitized()); + packet_write(1, "%s %s%c%s\n", + sha1_to_hex(sha1), path, 0, cap.buf); + strbuf_release(&cap); + sent_capabilities = 1; + } } static int show_ref_cb(const char *path, const unsigned char *sha1, int flag, void *unused) @@ -254,6 +288,222 @@ static int copy_to_sideband(int in, int out, void *arg) return 0; } +#define HMAC_BLOCK_SIZE 64 + +static void hmac_sha1(unsigned char *out, + const char *key_in, size_t key_len, + const char *text, size_t text_len) +{ + unsigned char key[HMAC_BLOCK_SIZE]; + unsigned char k_ipad[HMAC_BLOCK_SIZE]; + unsigned char k_opad[HMAC_BLOCK_SIZE]; + int i; + git_SHA_CTX ctx; + + /* RFC 2104 2. (1) */ + memset(key, '\0', HMAC_BLOCK_SIZE); + if (HMAC_BLOCK_SIZE < key_len) { + git_SHA1_Init(&ctx); + git_SHA1_Update(&ctx, key_in, key_len); + git_SHA1_Final(key, &ctx); + } else { + memcpy(key, key_in, key_len); + } + + /* RFC 2104 2. (2) & (5) */ + for (i = 0; i < sizeof(key); i++) { + k_ipad[i] = key[i] ^ 0x36; + k_opad[i] = key[i] ^ 0x5c; + } + + /* RFC 2104 2. (3) & (4) */ + git_SHA1_Init(&ctx); + git_SHA1_Update(&ctx, k_ipad, sizeof(k_ipad)); + git_SHA1_Update(&ctx, text, text_len); + git_SHA1_Final(out, &ctx); + + /* RFC 2104 2. (6) & (7) */ + git_SHA1_Init(&ctx); + git_SHA1_Update(&ctx, k_opad, sizeof(k_opad)); + git_SHA1_Update(&ctx, out, 20); + git_SHA1_Final(out, &ctx); +} + +static char *prepare_push_cert_nonce(const char *path, unsigned long stamp) +{ + struct strbuf buf = STRBUF_INIT; + unsigned char sha1[20]; + + strbuf_addf(&buf, "%s:%lu", path, stamp); + hmac_sha1(sha1, buf.buf, buf.len, cert_nonce_seed, strlen(cert_nonce_seed));; + strbuf_release(&buf); + + /* RFC 2104 5. HMAC-SHA1-80 */ + strbuf_addf(&buf, "%lu-%.*s", stamp, 20, sha1_to_hex(sha1)); + return strbuf_detach(&buf, NULL); +} + +/* + * NEEDSWORK: reuse find_commit_header() from jk/commit-author-parsing + * after dropping "_commit" from its name and possibly moving it out + * of commit.c + */ +static char *find_header(const char *msg, size_t len, const char *key) +{ + int key_len = strlen(key); + const char *line = msg; + + while (line && line < msg + len) { + const char *eol = strchrnul(line, '\n'); + + if ((msg + len <= eol) || line == eol) + return NULL; + if (line + key_len < eol && + !memcmp(line, key, key_len) && line[key_len] == ' ') { + int offset = key_len + 1; + return xmemdupz(line + offset, (eol - line) - offset); + } + line = *eol ? eol + 1 : NULL; + } + return NULL; +} + +static const char *check_nonce(const char *buf, size_t len) +{ + char *nonce = find_header(buf, len, "nonce"); + unsigned long stamp, ostamp; + char *bohmac, *expect = NULL; + const char *retval = NONCE_BAD; + + if (!nonce) { + retval = NONCE_MISSING; + goto leave; + } else if (!push_cert_nonce) { + retval = NONCE_UNSOLICITED; + goto leave; + } else if (!strcmp(push_cert_nonce, nonce)) { + retval = NONCE_OK; + goto leave; + } + + if (!stateless_rpc) { + /* returned nonce MUST match what we gave out earlier */ + retval = NONCE_BAD; + goto leave; + } + + /* + * In stateless mode, we may be receiving a nonce issued by + * another instance of the server that serving the same + * repository, and the timestamps may not match, but the + * nonce-seed and dir should match, so we can recompute and + * report the time slop. + * + * In addition, when a nonce issued by another instance has + * timestamp within receive.certnonceslop seconds, we pretend + * as if we issued that nonce when reporting to the hook. + */ + + /* nonce is concat(<seconds-since-epoch>, "-", <hmac>) */ + if (*nonce <= '0' || '9' < *nonce) { + retval = NONCE_BAD; + goto leave; + } + stamp = strtoul(nonce, &bohmac, 10); + if (bohmac == nonce || bohmac[0] != '-') { + retval = NONCE_BAD; + goto leave; + } + + expect = prepare_push_cert_nonce(service_dir, stamp); + if (strcmp(expect, nonce)) { + /* Not what we would have signed earlier */ + retval = NONCE_BAD; + goto leave; + } + + /* + * By how many seconds is this nonce stale? Negative value + * would mean it was issued by another server with its clock + * skewed in the future. + */ + ostamp = strtoul(push_cert_nonce, NULL, 10); + nonce_stamp_slop = (long)ostamp - (long)stamp; + + if (nonce_stamp_slop_limit && + abs(nonce_stamp_slop) <= nonce_stamp_slop_limit) { + /* + * Pretend as if the received nonce (which passes the + * HMAC check, so it is not a forged by third-party) + * is what we issued. + */ + free((void *)push_cert_nonce); + push_cert_nonce = xstrdup(nonce); + retval = NONCE_OK; + } else { + retval = NONCE_SLOP; + } + +leave: + free(nonce); + free(expect); + return retval; +} + +static void prepare_push_cert_sha1(struct child_process *proc) +{ + static int already_done; + struct argv_array env = ARGV_ARRAY_INIT; + + if (!push_cert.len) + return; + + if (!already_done) { + struct strbuf gpg_output = STRBUF_INIT; + struct strbuf gpg_status = STRBUF_INIT; + int bogs /* beginning_of_gpg_sig */; + + already_done = 1; + if (write_sha1_file(push_cert.buf, push_cert.len, "blob", push_cert_sha1)) + hashclr(push_cert_sha1); + + memset(&sigcheck, '\0', sizeof(sigcheck)); + sigcheck.result = 'N'; + + bogs = parse_signature(push_cert.buf, push_cert.len); + if (verify_signed_buffer(push_cert.buf, bogs, + push_cert.buf + bogs, push_cert.len - bogs, + &gpg_output, &gpg_status) < 0) { + ; /* error running gpg */ + } else { + sigcheck.payload = push_cert.buf; + sigcheck.gpg_output = gpg_output.buf; + sigcheck.gpg_status = gpg_status.buf; + parse_gpg_output(&sigcheck); + } + + strbuf_release(&gpg_output); + strbuf_release(&gpg_status); + nonce_status = check_nonce(push_cert.buf, bogs); + } + if (!is_null_sha1(push_cert_sha1)) { + argv_array_pushf(&env, "GIT_PUSH_CERT=%s", sha1_to_hex(push_cert_sha1)); + argv_array_pushf(&env, "GIT_PUSH_CERT_SIGNER=%s", + sigcheck.signer ? sigcheck.signer : ""); + argv_array_pushf(&env, "GIT_PUSH_CERT_KEY=%s", + sigcheck.key ? sigcheck.key : ""); + argv_array_pushf(&env, "GIT_PUSH_CERT_STATUS=%c", sigcheck.result); + if (push_cert_nonce) { + argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE=%s", push_cert_nonce); + argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE_STATUS=%s", nonce_status); + if (nonce_status == NONCE_SLOP) + argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE_SLOP=%ld", + nonce_stamp_slop); + } + proc->env = env.argv; + } +} + typedef int (*feed_fn)(void *, const char **, size_t *); static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_state) { @@ -272,6 +522,8 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_sta proc.in = -1; proc.stdout_to_stderr = 1; + prepare_push_cert_sha1(&proc); + if (use_sideband) { memset(&muxer, 0, sizeof(muxer)); muxer.proc = copy_to_sideband; @@ -842,40 +1094,79 @@ static void execute_commands(struct command *commands, "the reported refs above"); } +static struct command **queue_command(struct command **tail, + const char *line, + int linelen) +{ + unsigned char old_sha1[20], new_sha1[20]; + struct command *cmd; + const char *refname; + int reflen; + + if (linelen < 83 || + line[40] != ' ' || + line[81] != ' ' || + get_sha1_hex(line, old_sha1) || + get_sha1_hex(line + 41, new_sha1)) + die("protocol error: expected old/new/ref, got '%s'", line); + + refname = line + 82; + reflen = linelen - 82; + cmd = xcalloc(1, sizeof(struct command) + reflen + 1); + hashcpy(cmd->old_sha1, old_sha1); + hashcpy(cmd->new_sha1, new_sha1); + memcpy(cmd->ref_name, refname, reflen); + cmd->ref_name[reflen] = '\0'; + *tail = cmd; + return &cmd->next; +} + +static void queue_commands_from_cert(struct command **tail, + struct strbuf *push_cert) +{ + const char *boc, *eoc; + + if (*tail) + die("protocol error: got both push certificate and unsigned commands"); + + boc = strstr(push_cert->buf, "\n\n"); + if (!boc) + die("malformed push certificate %.*s", 100, push_cert->buf); + else + boc += 2; + eoc = push_cert->buf + parse_signature(push_cert->buf, push_cert->len); + + while (boc < eoc) { + const char *eol = memchr(boc, '\n', eoc - boc); + tail = queue_command(tail, boc, eol ? eol - boc : eoc - eol); + boc = eol ? eol + 1 : eoc; + } +} + static struct command *read_head_info(struct sha1_array *shallow) { struct command *commands = NULL; struct command **p = &commands; for (;;) { char *line; - unsigned char old_sha1[20], new_sha1[20]; - struct command *cmd; - char *refname; - int len, reflen; + int len, linelen; line = packet_read_line(0, &len); if (!line) break; if (len == 48 && starts_with(line, "shallow ")) { - if (get_sha1_hex(line + 8, old_sha1)) - die("protocol error: expected shallow sha, got '%s'", line + 8); - sha1_array_append(shallow, old_sha1); + unsigned char sha1[20]; + if (get_sha1_hex(line + 8, sha1)) + die("protocol error: expected shallow sha, got '%s'", + line + 8); + sha1_array_append(shallow, sha1); continue; } - if (len < 83 || - line[40] != ' ' || - line[81] != ' ' || - get_sha1_hex(line, old_sha1) || - get_sha1_hex(line + 41, new_sha1)) - die("protocol error: expected old/new/ref, got '%s'", - line); - - refname = line + 82; - reflen = strlen(refname); - if (reflen + 82 < len) { - const char *feature_list = refname + reflen + 1; + linelen = strlen(line); + if (linelen < len) { + const char *feature_list = line + linelen + 1; if (parse_feature_request(feature_list, "report-status")) report_status = 1; if (parse_feature_request(feature_list, "side-band-64k")) @@ -883,13 +1174,34 @@ static struct command *read_head_info(struct sha1_array *shallow) if (parse_feature_request(feature_list, "quiet")) quiet = 1; } - cmd = xcalloc(1, sizeof(struct command) + len - 80); - hashcpy(cmd->old_sha1, old_sha1); - hashcpy(cmd->new_sha1, new_sha1); - memcpy(cmd->ref_name, line + 82, len - 81); - *p = cmd; - p = &cmd->next; + + if (!strcmp(line, "push-cert")) { + int true_flush = 0; + char certbuf[1024]; + + for (;;) { + len = packet_read(0, NULL, NULL, + certbuf, sizeof(certbuf), 0); + if (!len) { + true_flush = 1; + break; + } + if (!strcmp(certbuf, "push-cert-end\n")) + break; /* end of cert */ + strbuf_addstr(&push_cert, certbuf); + } + + if (true_flush) + break; + continue; + } + + p = queue_command(p, line, linelen); } + + if (push_cert.len) + queue_commands_from_cert(p, &push_cert); + return commands; } @@ -1130,9 +1442,7 @@ static int delete_only(struct command *commands) int cmd_receive_pack(int argc, const char **argv, const char *prefix) { int advertise_refs = 0; - int stateless_rpc = 0; int i; - const char *dir = NULL; struct command *commands; struct sha1_array shallow = SHA1_ARRAY_INIT; struct sha1_array ref = SHA1_ARRAY_INIT; @@ -1165,19 +1475,21 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix) usage(receive_pack_usage); } - if (dir) + if (service_dir) usage(receive_pack_usage); - dir = arg; + service_dir = arg; } - if (!dir) + if (!service_dir) usage(receive_pack_usage); setup_path(); - if (!enter_repo(dir, 0)) - die("'%s' does not appear to be a git repository", dir); + if (!enter_repo(service_dir, 0)) + die("'%s' does not appear to be a git repository", service_dir); git_config(receive_pack_config, NULL); + if (cert_nonce_seed) + push_cert_nonce = prepare_push_cert_nonce(service_dir, time(NULL)); if (0 <= transfer_unpack_limit) unpack_limit = transfer_unpack_limit; @@ -1222,5 +1534,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix) packet_flush(1); sha1_array_clear(&shallow); sha1_array_clear(&ref); + free((void *)push_cert_nonce); return 0; } diff --git a/builtin/send-pack.c b/builtin/send-pack.c index 4b1bc0fef7..b564a77845 100644 --- a/builtin/send-pack.c +++ b/builtin/send-pack.c @@ -154,6 +154,10 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix) args.verbose = 1; continue; } + if (!strcmp(arg, "--signed")) { + args.push_cert = 1; + continue; + } if (!strcmp(arg, "--progress")) { progress = 1; continue; diff --git a/bulk-checkin.c b/bulk-checkin.c index 98e651c284..0c4b8a7cad 100644 --- a/bulk-checkin.c +++ b/bulk-checkin.c @@ -1,6 +1,7 @@ /* * Copyright (c) 2011, Google Inc. */ +#include "cache.h" #include "bulk-checkin.h" #include "csum-file.h" #include "pack.h" diff --git a/bulk-checkin.h b/bulk-checkin.h index 4f599f8841..fbd40fc98c 100644 --- a/bulk-checkin.h +++ b/bulk-checkin.h @@ -4,8 +4,6 @@ #ifndef BULK_CHECKIN_H #define BULK_CHECKIN_H -#include "cache.h" - extern int index_bulk_checkin(unsigned char sha1[], int fd, size_t size, enum object_type type, const char *path, unsigned flags); @@ -1306,6 +1306,7 @@ extern int git_config_rename_section_in_file(const char *, const char *, const c extern const char *git_etc_gitconfig(void); extern int check_repository_format_version(const char *var, const char *value, void *cb); extern int git_env_bool(const char *, int); +extern unsigned long git_env_ulong(const char *, unsigned long); extern int git_config_system(void); extern int config_error_nonbool(const char *); #if defined(__GNUC__) @@ -1214,42 +1214,6 @@ free_return: free(buf); } -static struct { - char result; - const char *check; -} sigcheck_gpg_status[] = { - { 'G', "\n[GNUPG:] GOODSIG " }, - { 'B', "\n[GNUPG:] BADSIG " }, - { 'U', "\n[GNUPG:] TRUST_NEVER" }, - { 'U', "\n[GNUPG:] TRUST_UNDEFINED" }, -}; - -static void parse_gpg_output(struct signature_check *sigc) -{ - const char *buf = sigc->gpg_status; - int i; - - /* Iterate over all search strings */ - for (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) { - const char *found, *next; - - if (!skip_prefix(buf, sigcheck_gpg_status[i].check + 1, &found)) { - found = strstr(buf, sigcheck_gpg_status[i].check); - if (!found) - continue; - found += strlen(sigcheck_gpg_status[i].check); - } - sigc->result = sigcheck_gpg_status[i].result; - /* The trust messages are not followed by key/signer information */ - if (sigc->result != 'U') { - sigc->key = xmemdupz(found, 16); - found += 17; - next = strchrnul(found, '\n'); - sigc->signer = xmemdupz(found, next - found); - } - } -} - void check_commit_signature(const struct commit *commit, struct signature_check *sigc) { struct strbuf payload = STRBUF_INIT; @@ -1140,12 +1140,28 @@ const char *git_etc_gitconfig(void) return system_wide; } +/* + * Parse environment variable 'k' as a boolean (in various + * possible spellings); if missing, use the default value 'def'. + */ int git_env_bool(const char *k, int def) { const char *v = getenv(k); return v ? git_config_bool(k, v) : def; } +/* + * Parse environment variable 'k' as ulong with possibly a unit + * suffix; if missing, use the default value 'val'. + */ +unsigned long git_env_ulong(const char *k, unsigned long val) +{ + const char *v = getenv(k); + if (v && !git_parse_ulong(v, &val)) + die("failed to parse %s", k); + return val; +} + int git_config_system(void) { return !git_env_bool("GIT_CONFIG_NOSYSTEM", 0); diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 5ea5b82d2b..2ed230a861 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1467,6 +1467,7 @@ _git_log () --abbrev-commit --abbrev= --relative-date --date= --pretty= --format= --oneline + --show-signature --cherry-pick --graph --decorate --decorate= @@ -2344,6 +2345,7 @@ _git_show () ;; --*) __gitcomp "--pretty= --format= --abbrev-commit --oneline + --show-signature $__git_diff_common_options " return @@ -312,11 +312,12 @@ static int crlf_to_worktree(const char *path, const char *src, size_t len, struct filter_params { const char *src; unsigned long size; + int fd; const char *cmd; const char *path; }; -static int filter_buffer(int in, int out, void *data) +static int filter_buffer_or_fd(int in, int out, void *data) { /* * Spawn cmd and feed the buffer contents through its stdin. @@ -354,7 +355,12 @@ static int filter_buffer(int in, int out, void *data) sigchain_push(SIGPIPE, SIG_IGN); - write_err = (write_in_full(child_process.in, params->src, params->size) < 0); + if (params->src) { + write_err = (write_in_full(child_process.in, params->src, params->size) < 0); + } else { + write_err = copy_fd(params->fd, child_process.in); + } + if (close(child_process.in)) write_err = 1; if (write_err) @@ -370,7 +376,7 @@ static int filter_buffer(int in, int out, void *data) return (write_err || status); } -static int apply_filter(const char *path, const char *src, size_t len, +static int apply_filter(const char *path, const char *src, size_t len, int fd, struct strbuf *dst, const char *cmd) { /* @@ -391,11 +397,12 @@ static int apply_filter(const char *path, const char *src, size_t len, return 1; memset(&async, 0, sizeof(async)); - async.proc = filter_buffer; + async.proc = filter_buffer_or_fd; async.data = ¶ms; async.out = -1; params.src = src; params.size = len; + params.fd = fd; params.cmd = cmd; params.path = path; @@ -746,6 +753,25 @@ static void convert_attrs(struct conv_attrs *ca, const char *path) } } +int would_convert_to_git_filter_fd(const char *path) +{ + struct conv_attrs ca; + + convert_attrs(&ca, path); + if (!ca.drv) + return 0; + + /* + * Apply a filter to an fd only if the filter is required to succeed. + * We must die if the filter fails, because the original data before + * filtering is not available. + */ + if (!ca.drv->required) + return 0; + + return apply_filter(path, NULL, 0, -1, NULL, ca.drv->clean); +} + int convert_to_git(const char *path, const char *src, size_t len, struct strbuf *dst, enum safe_crlf checksafe) { @@ -760,7 +786,7 @@ int convert_to_git(const char *path, const char *src, size_t len, required = ca.drv->required; } - ret |= apply_filter(path, src, len, dst, filter); + ret |= apply_filter(path, src, len, -1, dst, filter); if (!ret && required) die("%s: clean filter '%s' failed", path, ca.drv->name); @@ -777,6 +803,23 @@ int convert_to_git(const char *path, const char *src, size_t len, return ret | ident_to_git(path, src, len, dst, ca.ident); } +void convert_to_git_filter_fd(const char *path, int fd, struct strbuf *dst, + enum safe_crlf checksafe) +{ + struct conv_attrs ca; + convert_attrs(&ca, path); + + assert(ca.drv); + assert(ca.drv->clean); + + if (!apply_filter(path, NULL, 0, fd, dst, ca.drv->clean)) + die("%s: clean filter '%s' failed", path, ca.drv->name); + + ca.crlf_action = input_crlf_action(ca.crlf_action, ca.eol_attr); + crlf_to_git(path, dst->buf, dst->len, dst, ca.crlf_action, checksafe); + ident_to_git(path, dst->buf, dst->len, dst, ca.ident); +} + static int convert_to_working_tree_internal(const char *path, const char *src, size_t len, struct strbuf *dst, int normalizing) @@ -810,7 +853,7 @@ static int convert_to_working_tree_internal(const char *path, const char *src, } } - ret_filter = apply_filter(path, src, len, dst, filter); + ret_filter = apply_filter(path, src, len, -1, dst, filter); if (!ret_filter && required) die("%s: smudge filter %s failed", path, ca.drv->name); @@ -40,11 +40,15 @@ extern int convert_to_working_tree(const char *path, const char *src, size_t len, struct strbuf *dst); extern int renormalize_buffer(const char *path, const char *src, size_t len, struct strbuf *dst); -static inline int would_convert_to_git(const char *path, const char *src, - size_t len, enum safe_crlf checksafe) +static inline int would_convert_to_git(const char *path) { - return convert_to_git(path, src, len, NULL, checksafe); + return convert_to_git(path, NULL, 0, NULL, 0); } +/* Precondition: would_convert_to_git_filter_fd(path) == true */ +extern void convert_to_git_filter_fd(const char *path, int fd, + struct strbuf *dst, + enum safe_crlf checksafe); +extern int would_convert_to_git_filter_fd(const char *path); /***************************************************************** * @@ -4,34 +4,17 @@ int copy_fd(int ifd, int ofd) { while (1) { char buffer[8192]; - char *buf = buffer; ssize_t len = xread(ifd, buffer, sizeof(buffer)); if (!len) break; if (len < 0) { - int read_error = errno; - close(ifd); return error("copy-fd: read returned %s", - strerror(read_error)); - } - while (len) { - int written = xwrite(ofd, buf, len); - if (written > 0) { - buf += written; - len -= written; - } - else if (!written) { - close(ifd); - return error("copy-fd: write returned 0"); - } else { - int write_error = errno; - close(ifd); - return error("copy-fd: write returned %s", - strerror(write_error)); - } + strerror(errno)); } + if (write_in_full(ofd, buffer, len) < 0) + return error("copy-fd: write returned %s", + strerror(errno)); } - close(ifd); return 0; } @@ -60,6 +43,7 @@ int copy_file(const char *dst, const char *src, int mode) return fdo; } status = copy_fd(fdi, fdo); + close(fdi); if (close(fdo) != 0) return error("%s: close error: %s", dst, strerror(errno)); @@ -553,20 +553,21 @@ static void parse_host_arg(char *extra_args, int buflen) static char addrbuf[HOST_NAME_MAX + 1]; hent = gethostbyname(hostname); + if (hent) { + ap = hent->h_addr_list; + memset(&sa, 0, sizeof sa); + sa.sin_family = hent->h_addrtype; + sa.sin_port = htons(0); + memcpy(&sa.sin_addr, *ap, hent->h_length); + + inet_ntop(hent->h_addrtype, &sa.sin_addr, + addrbuf, sizeof(addrbuf)); - ap = hent->h_addr_list; - memset(&sa, 0, sizeof sa); - sa.sin_family = hent->h_addrtype; - sa.sin_port = htons(0); - memcpy(&sa.sin_addr, *ap, hent->h_length); - - inet_ntop(hent->h_addrtype, &sa.sin_addr, - addrbuf, sizeof(addrbuf)); - - free(canon_hostname); - canon_hostname = xstrdup(hent->h_name); - free(ip_address); - ip_address = xstrdup(addrbuf); + free(canon_hostname); + canon_hostname = xstrdup(hent->h_name); + free(ip_address); + ip_address = xstrdup(addrbuf); + } #endif } } @@ -814,7 +815,6 @@ static const char *ip2str(int family, struct sockaddr *sin, socklen_t len) static int setup_named_sock(char *listen_addr, int listen_port, struct socketlist *socklist) { int socknum = 0; - int maxfd = -1; char pbuf[NI_MAXSERV]; struct addrinfo hints, *ai0, *ai; int gai; @@ -882,9 +882,6 @@ static int setup_named_sock(char *listen_addr, int listen_port, struct socketlis ALLOC_GROW(socklist->list, socklist->nr + 1, socklist->alloc); socklist->list[socklist->nr++] = sockfd; socknum++; - - if (maxfd < sockfd) - maxfd = sockfd; } freeaddrinfo(ai0); @@ -923,7 +920,7 @@ static int setup_named_sock(char *listen_addr, int listen_port, struct socketlis } if ( bind(sockfd, (struct sockaddr *)&sin, sizeof sin) < 0 ) { - logerror("Could not listen to %s: %s", + logerror("Could not bind to %s: %s", ip2str(AF_INET, (struct sockaddr *)&sin, sizeof(sin)), strerror(errno)); close(sockfd); diff --git a/gpg-interface.c b/gpg-interface.c index 1ef73fb7df..68b0c814f7 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -7,6 +7,9 @@ static char *configured_signing_key; static const char *gpg_program = "gpg"; +#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----" +#define PGP_MESSAGE "-----BEGIN PGP MESSAGE-----" + void signature_check_clear(struct signature_check *sigc) { free(sigc->payload); @@ -21,6 +24,60 @@ void signature_check_clear(struct signature_check *sigc) sigc->key = NULL; } +static struct { + char result; + const char *check; +} sigcheck_gpg_status[] = { + { 'G', "\n[GNUPG:] GOODSIG " }, + { 'B', "\n[GNUPG:] BADSIG " }, + { 'U', "\n[GNUPG:] TRUST_NEVER" }, + { 'U', "\n[GNUPG:] TRUST_UNDEFINED" }, +}; + +void parse_gpg_output(struct signature_check *sigc) +{ + const char *buf = sigc->gpg_status; + int i; + + /* Iterate over all search strings */ + for (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) { + const char *found, *next; + + if (!skip_prefix(buf, sigcheck_gpg_status[i].check + 1, &found)) { + found = strstr(buf, sigcheck_gpg_status[i].check); + if (!found) + continue; + found += strlen(sigcheck_gpg_status[i].check); + } + sigc->result = sigcheck_gpg_status[i].result; + /* The trust messages are not followed by key/signer information */ + if (sigc->result != 'U') { + sigc->key = xmemdupz(found, 16); + found += 17; + next = strchrnul(found, '\n'); + sigc->signer = xmemdupz(found, next - found); + } + } +} + +/* + * Look at GPG signed content (e.g. a signed tag object), whose + * payload is followed by a detached signature on it. Return the + * offset where the embedded detached signature begins, or the end of + * the data when there is no such signature. + */ +size_t parse_signature(const char *buf, unsigned long size) +{ + char *eol; + size_t len = 0; + while (len < size && !starts_with(buf + len, PGP_SIGNATURE) && + !starts_with(buf + len, PGP_MESSAGE)) { + eol = memchr(buf + len, '\n', size - len); + len += eol ? eol - (buf + len) + 1 : size - len; + } + return len; +} + void set_signing_key(const char *key) { free(configured_signing_key); diff --git a/gpg-interface.h b/gpg-interface.h index 37c23daff0..87a4f2e3fa 100644 --- a/gpg-interface.h +++ b/gpg-interface.h @@ -5,16 +5,23 @@ struct signature_check { char *payload; char *gpg_output; char *gpg_status; - char result; /* 0 (not checked), - * N (checked but no further result), - * U (untrusted good), - * G (good) - * B (bad) */ + + /* + * possible "result": + * 0 (not checked) + * N (checked but no further result) + * U (untrusted good) + * G (good) + * B (bad) + */ + char result; char *signer; char *key; }; extern void signature_check_clear(struct signature_check *sigc); +extern size_t parse_signature(const char *buf, unsigned long size); +extern void parse_gpg_output(struct signature_check *); extern int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key); 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); extern int git_gpg_config(const char *, const char *, void *); @@ -1,3 +1,4 @@ +#include "git-compat-util.h" #include "http.h" #include "pack.h" #include "sideband.h" diff --git a/lockfile.c b/lockfile.c index d27e61cafc..7bfec4b773 100644 --- a/lockfile.c +++ b/lockfile.c @@ -207,9 +207,12 @@ int hold_lock_file_for_append(struct lock_file *lk, const char *path, int flags) if (flags & LOCK_DIE_ON_ERROR) exit(128); + close(orig_fd); rollback_lock_file(lk); errno = save_errno; return -1; + } else { + close(orig_fd); } return fd; } diff --git a/merge-recursive.c b/merge-recursive.c index 4b0884b7bb..fdb7d0f10b 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -3,8 +3,8 @@ * Fredrik Kuivinen. * The thieves were Alex Riesen and Johannes Schindelin, in June/July 2006 */ -#include "advice.h" #include "cache.h" +#include "advice.h" #include "lockfile.h" #include "cache-tree.h" #include "commit.h" diff --git a/remote-curl.c b/remote-curl.c index cd626d15e5..dd63bc27ab 100644 --- a/remote-curl.c +++ b/remote-curl.c @@ -25,7 +25,8 @@ struct options { update_shallow : 1, followtags : 1, dry_run : 1, - thin : 1; + thin : 1, + push_cert : 1; }; static struct options options; static struct string_list cas_options = STRING_LIST_INIT_DUP; @@ -106,6 +107,14 @@ static int set_option(const char *name, const char *value) else return -1; return 0; + } else if (!strcmp(name, "pushcert")) { + if (!strcmp(value, "true")) + options.push_cert = 1; + else if (!strcmp(value, "false")) + options.push_cert = 0; + else + return -1; + return 0; } else { return 1 /* unsupported */; } @@ -872,6 +881,8 @@ static int push_git(struct discovery *heads, int nr_spec, char **specs) argv_array_push(&args, "--thin"); if (options.dry_run) argv_array_push(&args, "--dry-run"); + if (options.push_cert) + argv_array_push(&args, "--signed"); if (options.verbosity == 0) argv_array_push(&args, "--quiet"); else if (options.verbosity > 1) diff --git a/send-pack.c b/send-pack.c index 8b4cbf049c..949cb61aa0 100644 --- a/send-pack.c +++ b/send-pack.c @@ -11,6 +11,7 @@ #include "transport.h" #include "version.h" #include "sha1-array.h" +#include "gpg-interface.h" static int feed_object(const unsigned char *sha1, int fd, int negative) { @@ -189,6 +190,94 @@ static void advertise_shallow_grafts_buf(struct strbuf *sb) for_each_commit_graft(advertise_shallow_grafts_cb, sb); } +static int ref_update_to_be_sent(const struct ref *ref, const struct send_pack_args *args) +{ + if (!ref->peer_ref && !args->send_mirror) + return 0; + + /* Check for statuses set by set_ref_status_for_push() */ + switch (ref->status) { + case REF_STATUS_REJECT_NONFASTFORWARD: + case REF_STATUS_REJECT_ALREADY_EXISTS: + case REF_STATUS_REJECT_FETCH_FIRST: + case REF_STATUS_REJECT_NEEDS_FORCE: + case REF_STATUS_REJECT_STALE: + case REF_STATUS_REJECT_NODELETE: + case REF_STATUS_UPTODATE: + return 0; + default: + return 1; + } +} + +/* + * the beginning of the next line, or the end of buffer. + * + * NEEDSWORK: perhaps move this to git-compat-util.h or somewhere and + * convert many similar uses found by "git grep -A4 memchr". + */ +static const char *next_line(const char *line, size_t len) +{ + const char *nl = memchr(line, '\n', len); + if (!nl) + return line + len; /* incomplete line */ + return nl + 1; +} + +static int generate_push_cert(struct strbuf *req_buf, + const struct ref *remote_refs, + struct send_pack_args *args, + const char *cap_string, + const char *push_cert_nonce) +{ + const struct ref *ref; + char *signing_key = xstrdup(get_signing_key()); + const char *cp, *np; + struct strbuf cert = STRBUF_INIT; + int update_seen = 0; + + strbuf_addf(&cert, "certificate version 0.1\n"); + strbuf_addf(&cert, "pusher %s ", signing_key); + datestamp(&cert); + strbuf_addch(&cert, '\n'); + if (args->url && *args->url) { + char *anon_url = transport_anonymize_url(args->url); + strbuf_addf(&cert, "pushee %s\n", anon_url); + free(anon_url); + } + if (push_cert_nonce[0]) + strbuf_addf(&cert, "nonce %s\n", push_cert_nonce); + strbuf_addstr(&cert, "\n"); + + for (ref = remote_refs; ref; ref = ref->next) { + if (!ref_update_to_be_sent(ref, args)) + continue; + update_seen = 1; + strbuf_addf(&cert, "%s %s %s\n", + sha1_to_hex(ref->old_sha1), + sha1_to_hex(ref->new_sha1), + ref->name); + } + if (!update_seen) + goto free_return; + + if (sign_buffer(&cert, &cert, signing_key)) + die(_("failed to sign the push certificate")); + + packet_buf_write(req_buf, "push-cert%c%s", 0, cap_string); + for (cp = cert.buf; cp < cert.buf + cert.len; cp = np) { + np = next_line(cp, cert.buf + cert.len - cp); + packet_buf_write(req_buf, + "%.*s", (int)(np - cp), cp); + } + packet_buf_write(req_buf, "push-cert-end\n"); + +free_return: + free(signing_key); + strbuf_release(&cert); + return update_seen; +} + int send_pack(struct send_pack_args *args, int fd[], struct child_process *conn, struct ref *remote_refs, @@ -197,8 +286,9 @@ int send_pack(struct send_pack_args *args, int in = fd[0]; int out = fd[1]; struct strbuf req_buf = STRBUF_INIT; + struct strbuf cap_buf = STRBUF_INIT; struct ref *ref; - int new_refs; + int need_pack_data = 0; int allow_deleting_refs = 0; int status_report = 0; int use_sideband = 0; @@ -207,6 +297,7 @@ int send_pack(struct send_pack_args *args, unsigned cmds_sent = 0; int ret; struct async demux; + const char *push_cert_nonce = NULL; /* Does the other end support the reporting? */ if (server_supports("report-status")) @@ -223,6 +314,14 @@ int send_pack(struct send_pack_args *args, agent_supported = 1; if (server_supports("no-thin")) args->use_thin_pack = 0; + if (args->push_cert) { + int len; + + push_cert_nonce = server_feature_value("push-cert", &len); + if (!push_cert_nonce) + die(_("the receiving end does not support --signed push")); + push_cert_nonce = xmemdupz(push_cert_nonce, len); + } if (!remote_refs) { fprintf(stderr, "No refs in common and none specified; doing nothing.\n" @@ -230,64 +329,71 @@ int send_pack(struct send_pack_args *args, return 0; } + if (status_report) + strbuf_addstr(&cap_buf, " report-status"); + if (use_sideband) + strbuf_addstr(&cap_buf, " side-band-64k"); + if (quiet_supported && (args->quiet || !args->progress)) + strbuf_addstr(&cap_buf, " quiet"); + if (agent_supported) + strbuf_addf(&cap_buf, " agent=%s", git_user_agent_sanitized()); + + /* + * NEEDSWORK: why does delete-refs have to be so specific to + * send-pack machinery that set_ref_status_for_push() cannot + * set this bit for us??? + */ + for (ref = remote_refs; ref; ref = ref->next) + if (ref->deletion && !allow_deleting_refs) + ref->status = REF_STATUS_REJECT_NODELETE; + if (!args->dry_run) advertise_shallow_grafts_buf(&req_buf); + if (!args->dry_run && args->push_cert) + cmds_sent = generate_push_cert(&req_buf, remote_refs, args, + cap_buf.buf, push_cert_nonce); + /* - * Finally, tell the other end! + * Clear the status for each ref and see if we need to send + * the pack data. */ - new_refs = 0; for (ref = remote_refs; ref; ref = ref->next) { - if (!ref->peer_ref && !args->send_mirror) + if (!ref_update_to_be_sent(ref, args)) continue; - /* Check for statuses set by set_ref_status_for_push() */ - switch (ref->status) { - case REF_STATUS_REJECT_NONFASTFORWARD: - case REF_STATUS_REJECT_ALREADY_EXISTS: - case REF_STATUS_REJECT_FETCH_FIRST: - case REF_STATUS_REJECT_NEEDS_FORCE: - case REF_STATUS_REJECT_STALE: - case REF_STATUS_UPTODATE: - continue; - default: - ; /* do nothing */ - } + if (!ref->deletion) + need_pack_data = 1; - if (ref->deletion && !allow_deleting_refs) { - ref->status = REF_STATUS_REJECT_NODELETE; + if (args->dry_run || !status_report) + ref->status = REF_STATUS_OK; + else + ref->status = REF_STATUS_EXPECTING_REPORT; + } + + /* + * Finally, tell the other end! + */ + for (ref = remote_refs; ref; ref = ref->next) { + char *old_hex, *new_hex; + + if (args->dry_run || args->push_cert) continue; - } - if (!ref->deletion) - new_refs++; + if (!ref_update_to_be_sent(ref, args)) + continue; - if (args->dry_run) { - ref->status = REF_STATUS_OK; + old_hex = sha1_to_hex(ref->old_sha1); + new_hex = sha1_to_hex(ref->new_sha1); + if (!cmds_sent) { + packet_buf_write(&req_buf, + "%s %s %s%c%s", + old_hex, new_hex, ref->name, 0, + cap_buf.buf); + cmds_sent = 1; } else { - char *old_hex = sha1_to_hex(ref->old_sha1); - char *new_hex = sha1_to_hex(ref->new_sha1); - int quiet = quiet_supported && (args->quiet || !args->progress); - - if (!cmds_sent && (status_report || use_sideband || - quiet || agent_supported)) { - packet_buf_write(&req_buf, - "%s %s %s%c%s%s%s%s%s", - old_hex, new_hex, ref->name, 0, - status_report ? " report-status" : "", - use_sideband ? " side-band-64k" : "", - quiet ? " quiet" : "", - agent_supported ? " agent=" : "", - agent_supported ? git_user_agent_sanitized() : "" - ); - } - else - packet_buf_write(&req_buf, "%s %s %s", - old_hex, new_hex, ref->name); - ref->status = status_report ? - REF_STATUS_EXPECTING_REPORT : - REF_STATUS_OK; - cmds_sent++; + packet_buf_write(&req_buf, "%s %s %s", + old_hex, new_hex, ref->name); } } @@ -301,6 +407,7 @@ int send_pack(struct send_pack_args *args, packet_flush(out); } strbuf_release(&req_buf); + strbuf_release(&cap_buf); if (use_sideband && cmds_sent) { memset(&demux, 0, sizeof(demux)); @@ -312,7 +419,7 @@ int send_pack(struct send_pack_args *args, in = demux.out; } - if (new_refs && cmds_sent) { + if (need_pack_data && cmds_sent) { if (pack_objects(out, remote_refs, extra_have, args) < 0) { for (ref = remote_refs; ref; ref = ref->next) ref->status = REF_STATUS_NONE; diff --git a/send-pack.h b/send-pack.h index 8e843924cf..5635457746 100644 --- a/send-pack.h +++ b/send-pack.h @@ -2,6 +2,7 @@ #define SEND_PACK_H struct send_pack_args { + const char *url; unsigned verbose:1, quiet:1, porcelain:1, @@ -11,6 +12,7 @@ struct send_pack_args { use_thin_pack:1, use_ofs_delta:1, dry_run:1, + push_cert:1, stateless_rpc:1; }; diff --git a/sha1_file.c b/sha1_file.c index 46ff273e27..83f77f01b6 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -664,10 +664,26 @@ void release_pack_memory(size_t need) ; /* nothing */ } +static void mmap_limit_check(size_t length) +{ + static size_t limit = 0; + if (!limit) { + limit = git_env_ulong("GIT_MMAP_LIMIT", 0); + if (!limit) + limit = SIZE_MAX; + } + if (length > limit) + die("attempting to mmap %"PRIuMAX" over limit %"PRIuMAX, + (uintmax_t)length, (uintmax_t)limit); +} + void *xmmap(void *start, size_t length, int prot, int flags, int fd, off_t offset) { - void *ret = mmap(start, length, prot, flags, fd, offset); + void *ret; + + mmap_limit_check(length); + ret = mmap(start, length, prot, flags, fd, offset); if (ret == MAP_FAILED) { if (!length) return NULL; @@ -3077,6 +3093,29 @@ static int index_mem(unsigned char *sha1, void *buf, size_t size, return ret; } +static int index_stream_convert_blob(unsigned char *sha1, int fd, + const char *path, unsigned flags) +{ + int ret; + const int write_object = flags & HASH_WRITE_OBJECT; + struct strbuf sbuf = STRBUF_INIT; + + assert(path); + assert(would_convert_to_git_filter_fd(path)); + + convert_to_git_filter_fd(path, fd, &sbuf, + write_object ? safe_crlf : SAFE_CRLF_FALSE); + + if (write_object) + ret = write_sha1_file(sbuf.buf, sbuf.len, typename(OBJ_BLOB), + sha1); + else + ret = hash_sha1_file(sbuf.buf, sbuf.len, typename(OBJ_BLOB), + sha1); + strbuf_release(&sbuf); + return ret; +} + static int index_pipe(unsigned char *sha1, int fd, enum object_type type, const char *path, unsigned flags) { @@ -3142,15 +3181,22 @@ int index_fd(unsigned char *sha1, int fd, struct stat *st, enum object_type type, const char *path, unsigned flags) { int ret; - size_t size = xsize_t(st->st_size); - if (!S_ISREG(st->st_mode)) + /* + * Call xsize_t() only when needed to avoid potentially unnecessary + * die() for large files. + */ + if (type == OBJ_BLOB && path && would_convert_to_git_filter_fd(path)) + ret = index_stream_convert_blob(sha1, fd, path, flags); + else if (!S_ISREG(st->st_mode)) ret = index_pipe(sha1, fd, type, path, flags); - else if (size <= big_file_threshold || type != OBJ_BLOB || - (path && would_convert_to_git(path, NULL, 0, 0))) - ret = index_core(sha1, fd, size, type, path, flags); + else if (st->st_size <= big_file_threshold || type != OBJ_BLOB || + (path && would_convert_to_git(path))) + ret = index_core(sha1, fd, xsize_t(st->st_size), type, path, + flags); else - ret = index_stream(sha1, fd, size, type, path, flags); + ret = index_stream(sha1, fd, xsize_t(st->st_size), type, path, + flags); close(fd); return ret; } diff --git a/sigchain.c b/sigchain.c index 1118b99e57..faa375d5d8 100644 --- a/sigchain.c +++ b/sigchain.c @@ -1,5 +1,5 @@ -#include "sigchain.h" #include "cache.h" +#include "sigchain.h" #define SIGCHAIN_MAX_SIGNALS 32 diff --git a/t/lib-credential.sh b/t/lib-credential.sh index 9e7d7962b0..d8e41f7ddd 100755 --- a/t/lib-credential.sh +++ b/t/lib-credential.sh @@ -278,12 +278,10 @@ helper_test_timeout() { ' } -cat >askpass <<\EOF -#!/bin/sh +write_script askpass <<\EOF echo >&2 askpass: $* what=$(echo $1 | cut -d" " -f1 | tr A-Z a-z | tr -cd a-z) echo "askpass-$what" EOF -chmod +x askpass GIT_ASKPASS="$PWD/askpass" export GIT_ASKPASS diff --git a/t/lib-httpd/apache.conf b/t/lib-httpd/apache.conf index b384d79935..7713dd2609 100644 --- a/t/lib-httpd/apache.conf +++ b/t/lib-httpd/apache.conf @@ -68,6 +68,7 @@ LockFile accept.lock PassEnv GIT_VALGRIND PassEnv GIT_VALGRIND_OPTIONS +PassEnv GNUPGHOME Alias /dumb/ www/ Alias /auth/dumb/ www/auth/dumb/ diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh index f890c54d13..ca7d2a630a 100755 --- a/t/t0021-conversion.sh +++ b/t/t0021-conversion.sh @@ -153,17 +153,23 @@ test_expect_success 'filter shell-escaped filenames' ' : ' -test_expect_success 'required filter success' ' - git config filter.required.smudge cat && - git config filter.required.clean cat && +test_expect_success 'required filter should filter data' ' + git config filter.required.smudge ./rot13.sh && + git config filter.required.clean ./rot13.sh && git config filter.required.required true && echo "*.r filter=required" >.gitattributes && - echo test >test.r && + cat test.o >test.r && git add test.r && + rm -f test.r && - git checkout -- test.r + git checkout -- test.r && + cmp test.o test.r && + + ./rot13.sh <test.o >expected && + git cat-file blob :test.r >actual && + cmp expected actual ' test_expect_success 'required filter smudge failure' ' @@ -190,6 +196,14 @@ test_expect_success 'required filter clean failure' ' test_must_fail git add test.fc ' +test_expect_success 'filtering large input to small output should use little memory' ' + git config filter.devnull.clean "cat >/dev/null" && + git config filter.devnull.required true && + for i in $(test_seq 1 30); do printf "%1048576d" 1; done >30MB && + echo "30MB filter=devnull" >.gitattributes && + GIT_MMAP_LIMIT=1m GIT_ALLOC_LIMIT=1m git add 30MB +' + test_expect_success EXPENSIVE 'filter large file' ' git config filter.largefile.smudge cat && git config filter.largefile.clean cat && diff --git a/t/t0090-cache-tree.sh b/t/t0090-cache-tree.sh index f9648a8642..158cf4f03b 100755 --- a/t/t0090-cache-tree.sh +++ b/t/t0090-cache-tree.sh @@ -22,7 +22,7 @@ generate_expected_cache_tree_rec () { # ls-files might have foo/bar, foo/bar/baz, and foo/bar/quux # We want to count only foo because it's the only direct child subtrees=$(git ls-files|grep /|cut -d / -f 1|uniq) && - subtree_count=$(echo "$subtrees"|awk '$1 {++c} END {print c}') && + subtree_count=$(echo "$subtrees"|awk -v c=0 '$1 {++c} END {print c}') && entries=$(git ls-files|wc -l) && printf "SHA $dir (%d entries, %d subtrees)\n" "$entries" "$subtree_count" && for subtree in $subtrees diff --git a/t/t1050-large.sh b/t/t1050-large.sh index 05a1e1d270..f5a9119290 100755 --- a/t/t1050-large.sh +++ b/t/t1050-large.sh @@ -13,7 +13,7 @@ test_expect_success setup ' echo X | dd of=large2 bs=1k seek=2000 && echo X | dd of=large3 bs=1k seek=2000 && echo Y | dd of=huge bs=1k seek=2500 && - GIT_ALLOC_LIMIT=1500 && + GIT_ALLOC_LIMIT=1500k && export GIT_ALLOC_LIMIT ' diff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh index 7b8babd89b..d01bbdc968 100755 --- a/t/t5000-tar-tree.sh +++ b/t/t5000-tar-tree.sh @@ -305,4 +305,18 @@ test_expect_success GZIP 'remote tar.gz can be disabled' ' >remote.tar.gz ' +test_expect_success 'archive and :(glob)' ' + git archive -v HEAD -- ":(glob)**/sh" >/dev/null 2>actual && + cat >expect <<EOF && +a/ +a/bin/ +a/bin/sh +EOF + test_cmp expect actual +' + +test_expect_success 'catch non-matching pathspec' ' + test_must_fail git archive -v HEAD -- "*.abc" >/dev/null +' + test_done diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh new file mode 100755 index 0000000000..2786346f9a --- /dev/null +++ b/t/t5534-push-signed.sh @@ -0,0 +1,127 @@ +#!/bin/sh + +test_description='signed push' + +. ./test-lib.sh +. "$TEST_DIRECTORY"/lib-gpg.sh + +prepare_dst () { + rm -fr dst && + test_create_repo dst && + + git push dst master:noop master:ff master:noff +} + +test_expect_success setup ' + # master, ff and noff branches pointing at the same commit + test_tick && + git commit --allow-empty -m initial && + + git checkout -b noop && + git checkout -b ff && + git checkout -b noff && + + # noop stays the same, ff advances, noff rewrites + test_tick && + git commit --allow-empty --amend -m rewritten && + git checkout ff && + + test_tick && + git commit --allow-empty -m second +' + +test_expect_success 'unsigned push does not send push certificate' ' + prepare_dst && + mkdir -p dst/.git/hooks && + write_script dst/.git/hooks/post-receive <<-\EOF && + # discard the update list + cat >/dev/null + # record the push certificate + if test -n "${GIT_PUSH_CERT-}" + then + git cat-file blob $GIT_PUSH_CERT >../push-cert + fi + EOF + + git push dst noop ff +noff && + ! test -f dst/push-cert +' + +test_expect_success 'talking with a receiver without push certificate support' ' + prepare_dst && + mkdir -p dst/.git/hooks && + write_script dst/.git/hooks/post-receive <<-\EOF && + # discard the update list + cat >/dev/null + # record the push certificate + if test -n "${GIT_PUSH_CERT-}" + then + git cat-file blob $GIT_PUSH_CERT >../push-cert + fi + EOF + + git push dst noop ff +noff && + ! test -f dst/push-cert +' + +test_expect_success 'push --signed fails with a receiver without push certificate support' ' + prepare_dst && + mkdir -p dst/.git/hooks && + test_must_fail git push --signed dst noop ff +noff 2>err && + test_i18ngrep "the receiving end does not support" err +' + +test_expect_success GPG 'no certificate for a signed push with no update' ' + prepare_dst && + mkdir -p dst/.git/hooks && + write_script dst/.git/hooks/post-receive <<-\EOF && + if test -n "${GIT_PUSH_CERT-}" + then + git cat-file blob $GIT_PUSH_CERT >../push-cert + fi + EOF + git push dst noop && + ! test -f dst/push-cert +' + +test_expect_success GPG 'signed push sends push certificate' ' + prepare_dst && + mkdir -p dst/.git/hooks && + git -C dst config receive.certnonceseed sekrit && + write_script dst/.git/hooks/post-receive <<-\EOF && + # discard the update list + cat >/dev/null + # record the push certificate + if test -n "${GIT_PUSH_CERT-}" + then + git cat-file blob $GIT_PUSH_CERT >../push-cert + fi && + + cat >../push-cert-status <<E_O_F + SIGNER=${GIT_PUSH_CERT_SIGNER-nobody} + KEY=${GIT_PUSH_CERT_KEY-nokey} + STATUS=${GIT_PUSH_CERT_STATUS-nostatus} + NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus} + NONCE=${GIT_PUSH_CERT_NONCE-nononce} + E_O_F + + EOF + + git push --signed dst noop ff +noff && + + ( + cat <<-\EOF && + SIGNER=C O Mitter <committer@example.com> + KEY=13B6F51ECDDE430D + STATUS=G + NONCE_STATUS=OK + EOF + sed -n -e "s/^nonce /NONCE=/p" -e "/^$/q" dst/push-cert + ) >expect && + + grep "$(git rev-parse noop ff) refs/heads/ff" dst/push-cert && + grep "$(git rev-parse noop noff) refs/heads/noff" dst/push-cert && + test_cmp expect dst/push-cert-status +' + +test_done diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh index db1998873c..d2c681ebfd 100755 --- a/t/t5541-http-push-smart.sh +++ b/t/t5541-http-push-smart.sh @@ -12,6 +12,7 @@ if test -n "$NO_CURL"; then fi ROOT_PATH="$PWD" +. "$TEST_DIRECTORY"/lib-gpg.sh . "$TEST_DIRECTORY"/lib-httpd.sh . "$TEST_DIRECTORY"/lib-terminal.sh start_httpd @@ -338,5 +339,45 @@ test_expect_success CMDLINE_LIMIT 'push 2000 tags over http' ' run_with_limited_cmdline git push --mirror ' +test_expect_success GPG 'push with post-receive to inspect certificate' ' + ( + cd "$HTTPD_DOCUMENT_ROOT_PATH"/test_repo.git && + mkdir -p hooks && + write_script hooks/post-receive <<-\EOF && + # discard the update list + cat >/dev/null + # record the push certificate + if test -n "${GIT_PUSH_CERT-}" + then + git cat-file blob $GIT_PUSH_CERT >../push-cert + fi && + cat >../push-cert-status <<E_O_F + SIGNER=${GIT_PUSH_CERT_SIGNER-nobody} + KEY=${GIT_PUSH_CERT_KEY-nokey} + STATUS=${GIT_PUSH_CERT_STATUS-nostatus} + NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus} + NONCE=${GIT_PUSH_CERT_NONCE-nononce} + E_O_F + EOF + + git config receive.certnonceseed sekrit && + git config receive.certnonceslop 30 + ) && + cd "$ROOT_PATH/test_repo_clone" && + test_commit cert-test && + git push --signed "$HTTPD_URL/smart/test_repo.git" && + ( + cd "$HTTPD_DOCUMENT_ROOT_PATH" && + cat <<-\EOF && + SIGNER=C O Mitter <committer@example.com> + KEY=13B6F51ECDDE430D + STATUS=G + NONCE_STATUS=OK + EOF + sed -n -e "s/^nonce /NONCE=/p" -e "/^$/q" push-cert + ) >expect && + test_cmp expect "$HTTPD_DOCUMENT_ROOT_PATH/push-cert-status" +' + stop_httpd test_done diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh index 0366653088..796e9f79ea 100755 --- a/t/t7004-tag.sh +++ b/t/t7004-tag.sh @@ -1460,7 +1460,7 @@ test_expect_success 'invalid sort parameter in configuratoin' ' ' run_with_limited_stack () { - (ulimit -s 64 && "$@") + (ulimit -s 128 && "$@") } test_lazy_prereq ULIMIT 'run_with_limited_stack true' @@ -1469,7 +1469,7 @@ test_lazy_prereq ULIMIT 'run_with_limited_stack true' test_expect_success ULIMIT '--contains works in a deep repo' ' >expect && i=1 && - while test $i -lt 4000 + while test $i -lt 8000 do echo "commit refs/heads/master committer A U Thor <author@example.com> $((1000000000 + $i * 100)) +0200 diff --git a/t/test-lib.sh b/t/test-lib.sh index 82095e34ee..0f4a67bfc6 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -813,7 +813,8 @@ rm -fr "$TRASH_DIRECTORY" || { } HOME="$TRASH_DIRECTORY" -export HOME +GNUPGHOME="$HOME/gnupg-home-not-used" +export HOME GNUPGHOME if test -z "$TEST_NO_CREATE_REPO" then @@ -4,9 +4,6 @@ #include "tree.h" #include "blob.h" -#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----" -#define PGP_MESSAGE "-----BEGIN PGP MESSAGE-----" - const char *tag_type = "tag"; struct object *deref_tag(struct object *o, const char *warn, int warnlen) @@ -143,20 +140,3 @@ int parse_tag(struct tag *item) free(data); return ret; } - -/* - * Look at a signed tag object, and return the offset where - * the embedded detached signature begins, or the end of the - * data when there is no such signature. - */ -size_t parse_signature(const char *buf, unsigned long size) -{ - char *eol; - size_t len = 0; - while (len < size && !starts_with(buf + len, PGP_SIGNATURE) && - !starts_with(buf + len, PGP_MESSAGE)) { - eol = memchr(buf + len, '\n', size - len); - len += eol ? eol - (buf + len) + 1 : size - len; - } - return len; -} @@ -17,6 +17,5 @@ extern int parse_tag_buffer(struct tag *item, const void *data, unsigned long si extern int parse_tag(struct tag *item); extern struct object *deref_tag(struct object *, const char *, int); extern struct object *deref_tag_noverify(struct object *); -extern size_t parse_signature(const char *buf, unsigned long size); #endif /* TAG_H */ diff --git a/test-regex.c b/test-regex.c index b5bfd54139..0dc598ecdc 100644 --- a/test-regex.c +++ b/test-regex.c @@ -1,4 +1,4 @@ -#include <git-compat-util.h> +#include "git-compat-util.h" int main(int argc, char **argv) { diff --git a/test-sigchain.c b/test-sigchain.c index 42db234e87..e499fce60f 100644 --- a/test-sigchain.c +++ b/test-sigchain.c @@ -1,5 +1,5 @@ -#include "sigchain.h" #include "cache.h" +#include "sigchain.h" #define X(f) \ static void f(int sig) { \ diff --git a/transport-helper.c b/transport-helper.c index 080a7a6ae2..2b24d51a24 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -260,7 +260,8 @@ static const char *unsupported_options[] = { static const char *boolean_options[] = { TRANS_OPT_THIN, TRANS_OPT_KEEP, - TRANS_OPT_FOLLOWTAGS + TRANS_OPT_FOLLOWTAGS, + TRANS_OPT_PUSH_CERT }; static int set_helper_option(struct transport *transport, @@ -836,6 +837,9 @@ static int push_refs_with_push(struct transport *transport, if (flags & TRANSPORT_PUSH_DRY_RUN) { if (set_helper_option(transport, "dry-run", "true") != 0) die("helper %s does not support dry-run", data->name); + } else if (flags & TRANSPORT_PUSH_CERT) { + if (set_helper_option(transport, TRANS_OPT_PUSH_CERT, "true") != 0) + die("helper %s does not support --signed", data->name); } strbuf_addch(&buf, '\n'); @@ -860,6 +864,9 @@ static int push_refs_with_export(struct transport *transport, if (flags & TRANSPORT_PUSH_DRY_RUN) { if (set_helper_option(transport, "dry-run", "true") != 0) die("helper %s does not support dry-run", data->name); + } else if (flags & TRANSPORT_PUSH_CERT) { + if (set_helper_option(transport, TRANS_OPT_PUSH_CERT, "true") != 0) + die("helper %s does not support dry-run", data->name); } if (flags & TRANSPORT_PUSH_FORCE) { diff --git a/transport.c b/transport.c index 7388bb87da..055d2a27d9 100644 --- a/transport.c +++ b/transport.c @@ -477,6 +477,9 @@ static int set_git_option(struct git_transport_options *opts, die("transport: invalid depth option '%s'", value); } return 0; + } else if (!strcmp(name, TRANS_OPT_PUSH_CERT)) { + opts->push_cert = !!value; + return 0; } return 1; } @@ -820,6 +823,8 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re args.progress = transport->progress; args.dry_run = !!(flags & TRANSPORT_PUSH_DRY_RUN); args.porcelain = !!(flags & TRANSPORT_PUSH_PORCELAIN); + args.push_cert = !!(flags & TRANSPORT_PUSH_CERT); + args.url = transport->url; ret = send_pack(&args, data->fd, data->conn, remote_refs, &data->extra_have); diff --git a/transport.h b/transport.h index 02ea248db1..3e0091eaab 100644 --- a/transport.h +++ b/transport.h @@ -12,6 +12,7 @@ struct git_transport_options { unsigned check_self_contained_and_connected : 1; unsigned self_contained_and_connected : 1; unsigned update_shallow : 1; + unsigned push_cert : 1; int depth; const char *uploadpack; const char *receivepack; @@ -123,6 +124,7 @@ struct transport { #define TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND 256 #define TRANSPORT_PUSH_NO_HOOK 512 #define TRANSPORT_PUSH_FOLLOW_TAGS 1024 +#define TRANSPORT_PUSH_CERT 2048 #define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3) #define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x) @@ -156,6 +158,9 @@ struct transport *transport_get(struct remote *, const char *); /* Accept refs that may update .git/shallow without --depth */ #define TRANS_OPT_UPDATE_SHALLOW "updateshallow" +/* Send push certificates */ +#define TRANS_OPT_PUSH_CERT "pushcert" + /** * Returns 0 if the option was used, non-zero otherwise. Prints a * message to stderr if the option is not used. @@ -1,3 +1,4 @@ +#include "git-compat-util.h" #include "varint.h" uintmax_t decode_varint(const unsigned char **bufp) @@ -1,8 +1,6 @@ #ifndef VARINT_H #define VARINT_H -#include "git-compat-util.h" - extern int encode_varint(uintmax_t, unsigned char *); extern uintmax_t decode_varint(const unsigned char **); @@ -11,19 +11,20 @@ static void (*try_to_free_routine)(size_t size) = do_nothing; static int memory_limit_check(size_t size, int gentle) { - static int limit = -1; - if (limit == -1) { - const char *env = getenv("GIT_ALLOC_LIMIT"); - limit = env ? atoi(env) * 1024 : 0; + static size_t limit = 0; + if (!limit) { + limit = git_env_ulong("GIT_ALLOC_LIMIT", 0); + if (!limit) + limit = SIZE_MAX; } - if (limit && size > limit) { + if (size > limit) { if (gentle) { - error("attempting to allocate %"PRIuMAX" over limit %d", - (intmax_t)size, limit); + error("attempting to allocate %"PRIuMAX" over limit %"PRIuMAX, + (uintmax_t)size, (uintmax_t)limit); return -1; } else - die("attempting to allocate %"PRIuMAX" over limit %d", - (intmax_t)size, limit); + die("attempting to allocate %"PRIuMAX" over limit %"PRIuMAX, + (uintmax_t)size, (uintmax_t)limit); } return 0; } |