From 5af5cc68aa8658c42bb2c4c46f2f01dabbd5acff Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Wed, 17 Apr 2024 00:02:31 +0000 Subject: docs: indicate new credential protocol fields Now that we have new fields (authtype and credential), let's document them for users and credential helper implementers. Indicate specifically what common values of authtype are and what values are allowed. Note that, while common, digest and NTLM authentication are insecure because they require unsalted, uniterated password hashes to be stored. Tell users that they can continue to use a username and password even if the new capability is supported. Signed-off-by: brian m. carlson Signed-off-by: Junio C Hamano --- Documentation/git-credential.txt | 49 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) (limited to 'Documentation') diff --git a/Documentation/git-credential.txt b/Documentation/git-credential.txt index 918a0aa42b..230ac4c2c3 100644 --- a/Documentation/git-credential.txt +++ b/Documentation/git-credential.txt @@ -178,6 +178,39 @@ empty string. Components which are missing from the URL (e.g., there is no username in the example above) will be left unset. +`authtype`:: + This indicates that the authentication scheme in question should be used. + Common values for HTTP and HTTPS include `basic`, `bearer`, and `digest`, + although the latter is insecure and should not be used. If `credential` + is used, this may be set to an arbitrary string suitable for the protocol in + question (usually HTTP). ++ +This value should not be sent unless the appropriate capability (see below) is +provided on input. + +`credential`:: + The pre-encoded credential, suitable for the protocol in question (usually + HTTP). If this key is sent, `authtype` is mandatory, and `username` and + `password` are not used. For HTTP, Git concatenates the `authtype` value and + this value with a single space to determine the `Authorization` header. ++ +This value should not be sent unless the appropriate capability (see below) is +provided on input. + +`ephemeral`:: + This boolean value indicates, if true, that the value in the `credential` + field should not be saved by the credential helper because its usefulness is + limited in time. For example, an HTTP Digest `credential` value is computed + using a nonce and reusing it will not result in successful authentication. + This may also be used for situations with short duration (e.g., 24-hour) + credentials. The default value is false. ++ +The credential helper will still be invoked with `store` or `erase` so that it +can determine whether the operation was successful. ++ +This value should not be sent unless the appropriate capability (see below) is +provided on input. + `wwwauth[]`:: When an HTTP response is received by Git that includes one or more @@ -189,7 +222,21 @@ attribute 'wwwauth[]', where the order of the attributes is the same as they appear in the HTTP response. This attribute is 'one-way' from Git to pass additional information to credential helpers. -Unrecognised attributes are silently discarded. +`capability[]`:: + This signals that the caller supports the capability in question. + This can be used to provide better, more specific data as part of the + protocol. ++ +The only capability currently supported is `authtype`, which indicates that the +`authtype`, `credential`, and `ephemeral` values are understood. It is not +obligatory to use these values in such a case, but they should not be provided +without this capability. ++ +Callers of `git credential` and credential helpers should emit the +capabilities they support unconditionally, and Git will gracefully +handle passing them on. + +Unrecognised attributes and capabilities are silently discarded. GIT --- -- cgit v1.3-5-g9baa From 8470c94be33d639c943e051a802c0e28eabf4a96 Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Wed, 17 Apr 2024 00:02:33 +0000 Subject: credential: add an argument to keep state Until now, our credential code has mostly deal with usernames and passwords and we've let libcurl deal with the variant of authentication to be used. However, now that we have the credential value, the credential helper can take control of the authentication, so the value provided might be something that's generated, such as a Digest hash value. In such a case, it would be helpful for a credential helper that gets an erase or store command to be able to keep track of an identifier for the original secret that went into the computation. Furthermore, some types of authentication, such as NTLM and Kerberos, actually need two round trips to authenticate, which will require that the credential helper keep some state. In order to allow for these use cases and others, allow storing state in a field called "state[]". This value is passed back to the credential helper that created it, which avoids confusion caused by parsing values from different helpers. Signed-off-by: brian m. carlson Signed-off-by: Junio C Hamano --- Documentation/git-credential.txt | 28 +++++++++++++++++++--------- credential.c | 20 +++++++++++++++++--- credential.h | 7 +++++++ t/t0300-credentials.sh | 28 ++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 12 deletions(-) (limited to 'Documentation') diff --git a/Documentation/git-credential.txt b/Documentation/git-credential.txt index 230ac4c2c3..f63a8e0458 100644 --- a/Documentation/git-credential.txt +++ b/Documentation/git-credential.txt @@ -211,6 +211,15 @@ can determine whether the operation was successful. This value should not be sent unless the appropriate capability (see below) is provided on input. +`state[]`:: + This value provides an opaque state that will be passed back to this helper + if it is called again. Each different credential helper may specify this + once. The value should include a prefix unique to the credential helper and + should ignore values that don't match its prefix. ++ +This value should not be sent unless the appropriate capability (see below) is +provided on input. + `wwwauth[]`:: When an HTTP response is received by Git that includes one or more @@ -223,18 +232,19 @@ they appear in the HTTP response. This attribute is 'one-way' from Git to pass additional information to credential helpers. `capability[]`:: - This signals that the caller supports the capability in question. - This can be used to provide better, more specific data as part of the + This signals that Git, or the helper, as appropriate, supports the capability + in question. This can be used to provide better, more specific data as part + of the protocol. A `capability[]` directive must precede any value depending + on it and these directives _should_ be the first item announced in the protocol. + -The only capability currently supported is `authtype`, which indicates that the -`authtype`, `credential`, and `ephemeral` values are understood. It is not -obligatory to use these values in such a case, but they should not be provided -without this capability. +There are two currently supported capabilities. The first is `authtype`, which +indicates that the `authtype`, `credential`, and `ephemeral` values are +understood. The second is `state`, which indicates that the `state[]` and +`continue` values are understood. + -Callers of `git credential` and credential helpers should emit the -capabilities they support unconditionally, and Git will gracefully -handle passing them on. +It is not obligatory to use the additional features just because the capability +is supported, but they should not be provided without the capability. Unrecognised attributes and capabilities are silently discarded. diff --git a/credential.c b/credential.c index 3531d74346..48826fb5a2 100644 --- a/credential.c +++ b/credential.c @@ -30,6 +30,7 @@ void credential_clear(struct credential *c) free(c->authtype); string_list_clear(&c->helpers, 0); strvec_clear(&c->wwwauth_headers); + strvec_clear(&c->state_headers); credential_init(c); } @@ -293,8 +294,13 @@ int credential_read(struct credential *c, FILE *fp, c->ephemeral = !!git_config_bool("ephemeral", value); } else if (!strcmp(key, "wwwauth[]")) { strvec_push(&c->wwwauth_headers, value); - } else if (!strcmp(key, "capability[]") && !strcmp(value, "authtype")) { - credential_set_capability(&c->capa_authtype, op_type); + } else if (!strcmp(key, "state[]")) { + strvec_push(&c->state_headers, value); + } else if (!strcmp(key, "capability[]")) { + if (!strcmp(value, "authtype")) + credential_set_capability(&c->capa_authtype, op_type); + else if (!strcmp(value, "state")) + credential_set_capability(&c->capa_state, op_type); } else if (!strcmp(key, "password_expiry_utc")) { errno = 0; c->password_expiry_utc = parse_timestamp(value, NULL, 10); @@ -337,8 +343,12 @@ static void credential_write_item(FILE *fp, const char *key, const char *value, void credential_write(const struct credential *c, FILE *fp, enum credential_op_type op_type) { - if (credential_has_capability(&c->capa_authtype, op_type)) { + if (credential_has_capability(&c->capa_authtype, op_type)) credential_write_item(fp, "capability[]", "authtype", 0); + if (credential_has_capability(&c->capa_state, op_type)) + credential_write_item(fp, "capability[]", "state", 0); + + if (credential_has_capability(&c->capa_authtype, op_type)) { credential_write_item(fp, "authtype", c->authtype, 0); credential_write_item(fp, "credential", c->credential, 0); if (c->ephemeral) @@ -357,6 +367,10 @@ void credential_write(const struct credential *c, FILE *fp, } for (size_t i = 0; i < c->wwwauth_headers.nr; i++) credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i], 0); + if (credential_has_capability(&c->capa_state, op_type)) { + for (size_t i = 0; i < c->state_headers.nr; i++) + credential_write_item(fp, "state[]", c->state_headers.v[i], 0); + } } static int run_credential_helper(struct credential *c, diff --git a/credential.h b/credential.h index da2a4802b7..c307300d12 100644 --- a/credential.h +++ b/credential.h @@ -144,6 +144,11 @@ struct credential { */ struct strvec wwwauth_headers; + /** + * A `strvec` of state headers from credential helpers. + */ + struct strvec state_headers; + /** * Internal use only. Keeps track of if we previously matched against a * WWW-Authenticate header line in order to re-fold future continuation @@ -159,6 +164,7 @@ struct credential { username_from_proto:1; struct credential_capability capa_authtype; + struct credential_capability capa_state; char *username; char *password; @@ -180,6 +186,7 @@ struct credential { .helpers = STRING_LIST_INIT_DUP, \ .password_expiry_utc = TIME_MAX, \ .wwwauth_headers = STRVEC_INIT, \ + .state_headers = STRVEC_INIT, \ } /* Initialize a credential structure, setting all fields to empty. */ diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh index eceb6bbfbe..432f029d48 100755 --- a/t/t0300-credentials.sh +++ b/t/t0300-credentials.sh @@ -46,9 +46,12 @@ test_expect_success 'setup helper scripts' ' credential=$1; shift . ./dump echo capability[]=authtype + echo capability[]=state test -z "${capability##*authtype*}" || exit 0 test -z "$authtype" || echo authtype=$authtype test -z "$credential" || echo credential=$credential + test -z "${capability##*state*}" || exit 0 + echo state[]=verbatim-cred:foo EOF write_script git-credential-verbatim-ephemeral <<-\EOF && @@ -129,6 +132,28 @@ test_expect_success 'credential_fill invokes helper with ephemeral credential' ' verbatim-ephemeral: host=example.com EOF ' +test_expect_success 'credential_fill invokes helper with credential and state' ' + check fill "verbatim-cred Bearer token" <<-\EOF + capability[]=authtype + capability[]=state + protocol=http + host=example.com + -- + capability[]=authtype + capability[]=state + authtype=Bearer + credential=token + protocol=http + host=example.com + state[]=verbatim-cred:foo + -- + verbatim-cred: get + verbatim-cred: capability[]=authtype + verbatim-cred: capability[]=state + verbatim-cred: protocol=http + verbatim-cred: host=example.com + EOF +' test_expect_success 'credential_fill invokes multiple helpers' ' check fill useless "verbatim foo bar" <<-\EOF @@ -152,6 +177,7 @@ test_expect_success 'credential_fill invokes multiple helpers' ' test_expect_success 'credential_fill response does not get capabilities when helpers are incapable' ' check fill useless "verbatim foo bar" <<-\EOF capability[]=authtype + capability[]=state protocol=http host=example.com -- @@ -162,10 +188,12 @@ test_expect_success 'credential_fill response does not get capabilities when hel -- useless: get useless: capability[]=authtype + useless: capability[]=state useless: protocol=http useless: host=example.com verbatim: get verbatim: capability[]=authtype + verbatim: capability[]=state verbatim: protocol=http verbatim: host=example.com EOF -- cgit v1.3-5-g9baa From bd590bde5846889dea72d301d7cd8abd70e5b0e1 Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Wed, 17 Apr 2024 00:02:35 +0000 Subject: docs: set a limit on credential line length We recently introduced a way for credential helpers to add arbitrary state as part of the protocol. Set some limits on line length to avoid helpers passing extremely large amounts of data. While Git doesn't have a fixed parsing length, there are other tools which support this protocol and it's kind to allow them to use a reasonable fixed-size buffer for parsing. In addition, we would like to be moderate in our memory usage and imposing reasonable limits is helpful for that purpose. In the event a credential helper is incapable of storing its serialized state in 64 KiB, it can feel free to serialize it on disk and store a reference instead. Signed-off-by: brian m. carlson Signed-off-by: Junio C Hamano --- Documentation/git-credential.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'Documentation') diff --git a/Documentation/git-credential.txt b/Documentation/git-credential.txt index f63a8e0458..4bbf2db9ca 100644 --- a/Documentation/git-credential.txt +++ b/Documentation/git-credential.txt @@ -111,7 +111,9 @@ attribute per line. Each attribute is specified by a key-value pair, separated by an `=` (equals) sign, followed by a newline. The key may contain any bytes except `=`, newline, or NUL. The value may -contain any bytes except newline or NUL. +contain any bytes except newline or NUL. A line, including the trailing +newline, may not exceed 65535 bytes in order to allow implementations to +parse efficiently. Attributes with keys that end with C-style array brackets `[]` can have multiple values. Each instance of a multi-valued attribute forms an -- cgit v1.3-5-g9baa From ac4c7cbfaa1871e8865d5fa5b8142a70da37cce4 Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Wed, 17 Apr 2024 00:02:37 +0000 Subject: credential: add support for multistage credential rounds Over HTTP, NTLM and Kerberos require two rounds of authentication on the client side. It's possible that there are custom authentication schemes that also implement this same approach. Since these are tricky schemes to implement and the HTTP library in use may not always handle them gracefully on all systems, it would be helpful to allow the credential helper to implement them instead for increased portability and robustness. To allow this to happen, add a boolean flag, continue, that indicates that instead of failing when we get a 401, we should retry another round of authentication. However, this necessitates some changes in our current credential code so that we can make this work. Keep the state[] headers between iterations, but only use them to send to the helper and only consider the new ones we read from the credential helper to be valid on subsequent iterations. That avoids us passing stale data when we finally approve or reject the credential. Similarly, clear the multistage and wwwauth[] values appropriately so that we don't pass stale data or think we're trying a multiround response when we're not. Remove the credential values so that we can actually fill a second time with new responses. Limit the number of iterations of reauthentication we do to 3. This means that if there's a problem, we'll terminate with an error message instead of retrying indefinitely and not informing the user (and possibly conducting a DoS on the server). In our tests, handle creating multiple response output files from our helper so we can verify that each of the messages sent is correct. Signed-off-by: brian m. carlson Signed-off-by: Junio C Hamano --- Documentation/git-credential.txt | 13 ++++++ builtin/credential.c | 1 + credential.c | 32 +++++++++++++-- credential.h | 27 +++++++++++- http.c | 57 +++++++++++++------------ t/t5563-simple-http-auth.sh | 89 +++++++++++++++++++++++++++++++++++++--- 6 files changed, 184 insertions(+), 35 deletions(-) (limited to 'Documentation') diff --git a/Documentation/git-credential.txt b/Documentation/git-credential.txt index 4bbf2db9ca..3d3accc273 100644 --- a/Documentation/git-credential.txt +++ b/Documentation/git-credential.txt @@ -222,6 +222,19 @@ provided on input. This value should not be sent unless the appropriate capability (see below) is provided on input. +`continue`:: + This is a boolean value, which, if enabled, indicates that this + authentication is a non-final part of a multistage authentication step. This + is common in protocols such as NTLM and Kerberos, where two rounds of client + authentication are required, and setting this flag allows the credential + helper to implement the multistage authentication step. This flag should + only be sent if a further stage is required; that is, if another round of + authentication is expected. ++ +This value should not be sent unless the appropriate capability (see below) is +provided on input. This attribute is 'one-way' from a credential helper to +pass information to Git (or other programs invoking `git credential`). + `wwwauth[]`:: When an HTTP response is received by Git that includes one or more diff --git a/builtin/credential.c b/builtin/credential.c index 643bf0b5e4..3568e57025 100644 --- a/builtin/credential.c +++ b/builtin/credential.c @@ -22,6 +22,7 @@ int cmd_credential(int argc, const char **argv, const char *prefix UNUSED) if (!strcmp(op, "fill")) { credential_fill(&c, 0); + credential_next_state(&c); credential_write(&c, stdout, CREDENTIAL_OP_RESPONSE); } else if (!strcmp(op, "approve")) { credential_set_all_capabilities(&c, CREDENTIAL_OP_HELPER); diff --git a/credential.c b/credential.c index c93de92f65..98b040cf11 100644 --- a/credential.c +++ b/credential.c @@ -31,10 +31,23 @@ void credential_clear(struct credential *c) string_list_clear(&c->helpers, 0); strvec_clear(&c->wwwauth_headers); strvec_clear(&c->state_headers); + strvec_clear(&c->state_headers_to_send); credential_init(c); } +void credential_next_state(struct credential *c) +{ + strvec_clear(&c->state_headers_to_send); + SWAP(c->state_headers, c->state_headers_to_send); +} + +void credential_clear_secrets(struct credential *c) +{ + FREE_AND_NULL(c->password); + FREE_AND_NULL(c->credential); +} + static void credential_set_capability(struct credential_capability *capa, enum credential_op_type op_type) { @@ -302,6 +315,8 @@ int credential_read(struct credential *c, FILE *fp, credential_set_capability(&c->capa_authtype, op_type); else if (!strcmp(value, "state")) credential_set_capability(&c->capa_state, op_type); + } else if (!strcmp(key, "continue")) { + c->multistage = !!git_config_bool("continue", value); } else if (!strcmp(key, "password_expiry_utc")) { errno = 0; c->password_expiry_utc = parse_timestamp(value, NULL, 10); @@ -369,8 +384,10 @@ void credential_write(const struct credential *c, FILE *fp, for (size_t i = 0; i < c->wwwauth_headers.nr; i++) credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i], 0); if (credential_has_capability(&c->capa_state, op_type)) { - for (size_t i = 0; i < c->state_headers.nr; i++) - credential_write_item(fp, "state[]", c->state_headers.v[i], 0); + if (c->multistage) + credential_write_item(fp, "continue", "1", 0); + for (size_t i = 0; i < c->state_headers_to_send.nr; i++) + credential_write_item(fp, "state[]", c->state_headers_to_send.v[i], 0); } } @@ -441,6 +458,9 @@ void credential_fill(struct credential *c, int all_capabilities) if ((c->username && c->password) || c->credential) return; + credential_next_state(c); + c->multistage = 0; + credential_apply_config(c); if (all_capabilities) credential_set_all_capabilities(c, CREDENTIAL_OP_INITIAL); @@ -453,8 +473,10 @@ void credential_fill(struct credential *c, int all_capabilities) /* Reset expiry to maintain consistency */ c->password_expiry_utc = TIME_MAX; } - if ((c->username && c->password) || c->credential) + if ((c->username && c->password) || c->credential) { + strvec_clear(&c->wwwauth_headers); return; + } if (c->quit) die("credential helper '%s' told us to quit", c->helpers.items[i].string); @@ -474,6 +496,8 @@ void credential_approve(struct credential *c) if (((!c->username || !c->password) && !c->credential) || c->password_expiry_utc < time(NULL)) return; + credential_next_state(c); + credential_apply_config(c); for (i = 0; i < c->helpers.nr; i++) @@ -485,6 +509,8 @@ void credential_reject(struct credential *c) { int i; + credential_next_state(c); + credential_apply_config(c); for (i = 0; i < c->helpers.nr; i++) diff --git a/credential.h b/credential.h index c307300d12..19163fc6a0 100644 --- a/credential.h +++ b/credential.h @@ -145,10 +145,15 @@ struct credential { struct strvec wwwauth_headers; /** - * A `strvec` of state headers from credential helpers. + * A `strvec` of state headers received from credential helpers. */ struct strvec state_headers; + /** + * A `strvec` of state headers to send to credential helpers. + */ + struct strvec state_headers_to_send; + /** * Internal use only. Keeps track of if we previously matched against a * WWW-Authenticate header line in order to re-fold future continuation @@ -159,6 +164,7 @@ struct credential { unsigned approved:1, ephemeral:1, configured:1, + multistage: 1, quit:1, use_http_path:1, username_from_proto:1; @@ -187,6 +193,7 @@ struct credential { .password_expiry_utc = TIME_MAX, \ .wwwauth_headers = STRVEC_INIT, \ .state_headers = STRVEC_INIT, \ + .state_headers_to_send = STRVEC_INIT, \ } /* Initialize a credential structure, setting all fields to empty. */ @@ -238,6 +245,24 @@ void credential_reject(struct credential *); void credential_set_all_capabilities(struct credential *c, enum credential_op_type op_type); +/** + * Clear the secrets in this credential, but leave other data intact. + * + * This is useful for resetting credentials in preparation for a subsequent + * stage of filling. + */ +void credential_clear_secrets(struct credential *c); + +/** + * Prepares the credential for the next iteration of the helper protocol by + * updating the state headers to send with the ones read by the last iteration + * of the protocol. + * + * Except for internal callers, this should be called exactly once between + * reading credentials with `credential_fill` and writing them. + */ +void credential_next_state(struct credential *c); + int credential_read(struct credential *, FILE *, enum credential_op_type); void credential_write(const struct credential *, FILE *, diff --git a/http.c b/http.c index 906eb098c8..9a514404d0 100644 --- a/http.c +++ b/http.c @@ -1780,6 +1780,10 @@ static int handle_curl_result(struct slot_results *results) else if (results->http_code == 401) { if ((http_auth.username && http_auth.password) ||\ (http_auth.authtype && http_auth.credential)) { + if (http_auth.multistage) { + credential_clear_secrets(&http_auth); + return HTTP_REAUTH; + } credential_reject(&http_auth); return HTTP_NOAUTH; } else { @@ -2177,6 +2181,7 @@ static int http_request_reauth(const char *url, void *result, int target, struct http_get_options *options) { + int i = 3; int ret = http_request(url, result, target, options); if (ret != HTTP_OK && ret != HTTP_REAUTH) @@ -2190,35 +2195,35 @@ static int http_request_reauth(const char *url, } } - if (ret != HTTP_REAUTH) - return ret; - - /* - * The previous request may have put cruft into our output stream; we - * should clear it out before making our next request. - */ - switch (target) { - case HTTP_REQUEST_STRBUF: - strbuf_reset(result); - break; - case HTTP_REQUEST_FILE: - if (fflush(result)) { - error_errno("unable to flush a file"); - return HTTP_START_FAILED; - } - rewind(result); - if (ftruncate(fileno(result), 0) < 0) { - error_errno("unable to truncate a file"); - return HTTP_START_FAILED; + while (ret == HTTP_REAUTH && --i) { + /* + * The previous request may have put cruft into our output stream; we + * should clear it out before making our next request. + */ + switch (target) { + case HTTP_REQUEST_STRBUF: + strbuf_reset(result); + break; + case HTTP_REQUEST_FILE: + if (fflush(result)) { + error_errno("unable to flush a file"); + return HTTP_START_FAILED; + } + rewind(result); + if (ftruncate(fileno(result), 0) < 0) { + error_errno("unable to truncate a file"); + return HTTP_START_FAILED; + } + break; + default: + BUG("Unknown http_request target"); } - break; - default: - BUG("Unknown http_request target"); - } - credential_fill(&http_auth, 1); + credential_fill(&http_auth, 1); - return http_request(url, result, target, options); + ret = http_request(url, result, target, options); + } + return ret; } int http_get_strbuf(const char *url, diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh index 515185ae00..5d5caa3f58 100755 --- a/t/t5563-simple-http-auth.sh +++ b/t/t5563-simple-http-auth.sh @@ -21,9 +21,17 @@ test_expect_success 'setup_credential_helper' ' CREDENTIAL_HELPER="$TRASH_DIRECTORY/bin/git-credential-test-helper" && write_script "$CREDENTIAL_HELPER" <<-\EOF cmd=$1 - teefile=$cmd-query.cred + teefile=$cmd-query-temp.cred catfile=$cmd-reply.cred sed -n -e "/^$/q" -e "p" >>$teefile + state=$(sed -ne "s/^state\[\]=helper://p" "$teefile") + if test -z "$state" + then + mv "$teefile" "$cmd-query.cred" + else + mv "$teefile" "$cmd-query-$state.cred" + catfile="$cmd-reply-$state.cred" + fi if test "$cmd" = "get" then cat $catfile @@ -32,13 +40,15 @@ test_expect_success 'setup_credential_helper' ' ' set_credential_reply () { - cat >"$TRASH_DIRECTORY/$1-reply.cred" + local suffix="$(test -n "$2" && echo "-$2")" + cat >"$TRASH_DIRECTORY/$1-reply$suffix.cred" } expect_credential_query () { - cat >"$TRASH_DIRECTORY/$1-expect.cred" && - test_cmp "$TRASH_DIRECTORY/$1-expect.cred" \ - "$TRASH_DIRECTORY/$1-query.cred" + local suffix="$(test -n "$2" && echo "-$2")" + cat >"$TRASH_DIRECTORY/$1-expect$suffix.cred" && + test_cmp "$TRASH_DIRECTORY/$1-expect$suffix.cred" \ + "$TRASH_DIRECTORY/$1-query$suffix.cred" } per_test_cleanup () { @@ -479,4 +489,73 @@ test_expect_success 'access using bearer auth with invalid credentials' ' EOF ' +test_expect_success 'access using three-legged auth' ' + test_when_finished "per_test_cleanup" && + + set_credential_reply get <<-EOF && + capability[]=authtype + capability[]=state + authtype=Multistage + credential=YS1naXQtdG9rZW4= + state[]=helper:foobar + continue=1 + EOF + + set_credential_reply get foobar <<-EOF && + capability[]=authtype + capability[]=state + authtype=Multistage + credential=YW5vdGhlci10b2tlbg== + state[]=helper:bazquux + EOF + + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF && + id=1 creds=Multistage YS1naXQtdG9rZW4= + id=2 creds=Multistage YW5vdGhlci10b2tlbg== + EOF + + CHALLENGE="$HTTPD_ROOT_PATH/custom-auth.challenge" && + + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF && + id=1 status=401 response=WWW-Authenticate: Multistage challenge="456" + id=1 status=401 response=WWW-Authenticate: Bearer authorize_uri="id.example.com" p=1 q=0 + id=2 status=200 + id=default response=WWW-Authenticate: Multistage challenge="123" + id=default response=WWW-Authenticate: Bearer authorize_uri="id.example.com" p=1 q=0 + EOF + + test_config_global credential.helper test-helper && + git ls-remote "$HTTPD_URL/custom_auth/repo.git" && + + expect_credential_query get <<-EOF && + capability[]=authtype + capability[]=state + protocol=http + host=$HTTPD_DEST + wwwauth[]=Multistage challenge="123" + wwwauth[]=Bearer authorize_uri="id.example.com" p=1 q=0 + EOF + + expect_credential_query get foobar <<-EOF && + capability[]=authtype + capability[]=state + authtype=Multistage + protocol=http + host=$HTTPD_DEST + wwwauth[]=Multistage challenge="456" + wwwauth[]=Bearer authorize_uri="id.example.com" p=1 q=0 + state[]=helper:foobar + EOF + + expect_credential_query store bazquux <<-EOF + capability[]=authtype + capability[]=state + authtype=Multistage + credential=YW5vdGhlci10b2tlbg== + protocol=http + host=$HTTPD_DEST + state[]=helper:bazquux + EOF +' + test_done -- cgit v1.3-5-g9baa From ffff4ac0658a2cad162c08feb1552ba02fed9099 Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Wed, 17 Apr 2024 00:02:40 +0000 Subject: credential: add method for querying capabilities Right now, there's no specific way to determine whether a credential helper or git credential itself supports a given set of capabilities. It would be helpful to have such a way, so let's let credential helpers and git credential take an argument, "capability", which has it list the capabilities and a version number on standard output. Specifically choose a format that is slightly different from regular credential output and assume that no capabilities are supported if a non-zero exit status occurs or the data deviates from the format. It is common for users to write small shell scripts as the argument to credential.helper, which will almost never be designed to emit capabilities. We want callers to gracefully handle this case by assuming that they are not capable of extended support because that is almost certainly the case, and specifying the error behavior up front does this and preserves backwards compatibility in a graceful way. Signed-off-by: brian m. carlson Signed-off-by: Junio C Hamano --- Documentation/git-credential.txt | 28 +++++++++++++++++++++++++++- builtin/credential-cache.c | 10 ++++++++++ builtin/credential.c | 6 ++++++ credential.c | 11 +++++++++++ credential.h | 6 ++++++ 5 files changed, 60 insertions(+), 1 deletion(-) (limited to 'Documentation') diff --git a/Documentation/git-credential.txt b/Documentation/git-credential.txt index 3d3accc273..e41493292f 100644 --- a/Documentation/git-credential.txt +++ b/Documentation/git-credential.txt @@ -8,7 +8,7 @@ git-credential - Retrieve and store user credentials SYNOPSIS -------- ------------------ -'git credential' (fill|approve|reject) +'git credential' (fill|approve|reject|capability) ------------------ DESCRIPTION @@ -41,6 +41,9 @@ If the action is `reject`, git-credential will send the description to any configured credential helpers, which may erase any stored credentials matching the description. +If the action is `capability`, git-credential will announce any capabilities +it supports to standard output. + If the action is `approve` or `reject`, no output should be emitted. TYPICAL USE OF GIT CREDENTIAL @@ -263,6 +266,29 @@ is supported, but they should not be provided without the capability. Unrecognised attributes and capabilities are silently discarded. +[[CAPA-IOFMT]] +CAPABILITY INPUT/OUTPUT FORMAT +------------------------------ + +For `git credential capability`, the format is slightly different. First, a +`version 0` announcement is made to indicate the current version of the +protocol, and then each capability is announced with a line like `capability +authtype`. Credential helpers may also implement this format, again with the +`capability` argument. Additional lines may be added in the future; callers +should ignore lines which they don't understand. + +Because this is a new part of the credential helper protocol, older versions of +Git, as well as some credential helpers, may not support it. If a non-zero +exit status is received, or if the first line doesn't start with the word +`version` and a space, callers should assume that no capabilities are supported. + +The intention of this format is to differentiate it from the credential output +in an unambiguous way. It is possible to use very simple credential helpers +(e.g., inline shell scripts) which always produce identical output. Using a +distinct format allows users to continue to use this syntax without having to +worry about correctly implementing capability advertisements or accidentally +confusing callers querying for capabilities. + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/credential-cache.c b/builtin/credential-cache.c index bba96d4ffd..f5c989e2b2 100644 --- a/builtin/credential-cache.c +++ b/builtin/credential-cache.c @@ -1,4 +1,5 @@ #include "builtin.h" +#include "credential.h" #include "gettext.h" #include "parse-options.h" #include "path.h" @@ -127,6 +128,13 @@ static char *get_socket_path(void) return socket; } +static void announce_capabilities(void) +{ + struct credential c = CREDENTIAL_INIT; + c.capa_authtype.request_initial = 1; + credential_announce_capabilities(&c, stdout); +} + int cmd_credential_cache(int argc, const char **argv, const char *prefix) { char *socket_path = NULL; @@ -160,6 +168,8 @@ int cmd_credential_cache(int argc, const char **argv, const char *prefix) do_cache(socket_path, op, timeout, FLAG_RELAY); else if (!strcmp(op, "store")) do_cache(socket_path, op, timeout, FLAG_RELAY|FLAG_SPAWN); + else if (!strcmp(op, "capability")) + announce_capabilities(); else ; /* ignore unknown operation */ diff --git a/builtin/credential.c b/builtin/credential.c index 3568e57025..5100d441f2 100644 --- a/builtin/credential.c +++ b/builtin/credential.c @@ -17,6 +17,12 @@ int cmd_credential(int argc, const char **argv, const char *prefix UNUSED) usage(usage_msg); op = argv[1]; + if (!strcmp(op, "capability")) { + credential_set_all_capabilities(&c, CREDENTIAL_OP_INITIAL); + credential_announce_capabilities(&c, stdout); + return 0; + } + if (credential_read(&c, stdin, CREDENTIAL_OP_INITIAL) < 0) die("unable to read credential from stdin"); diff --git a/credential.c b/credential.c index ffaf31499e..758528b291 100644 --- a/credential.c +++ b/credential.c @@ -72,6 +72,17 @@ void credential_set_all_capabilities(struct credential *c, credential_set_capability(&c->capa_state, op_type); } +static void announce_one(struct credential_capability *cc, const char *name, FILE *fp) { + if (cc->request_initial) + fprintf(fp, "capability %s\n", name); +} + +void credential_announce_capabilities(struct credential *c, FILE *fp) { + fprintf(fp, "version 0\n"); + announce_one(&c->capa_authtype, "authtype", fp); + announce_one(&c->capa_state, "state", fp); +} + int credential_match(const struct credential *want, const struct credential *have, int match_password) { diff --git a/credential.h b/credential.h index f8df10937c..af8c287ff2 100644 --- a/credential.h +++ b/credential.h @@ -253,6 +253,12 @@ void credential_set_all_capabilities(struct credential *c, */ void credential_clear_secrets(struct credential *c); +/** + * Print a list of supported capabilities and version numbers to standard + * output. + */ +void credential_announce_capabilities(struct credential *c, FILE *fp); + /** * Prepares the credential for the next iteration of the helper protocol by * updating the state headers to send with the ones read by the last iteration -- cgit v1.3-5-g9baa