credential: gate new fields on capability

We support the new credential and authtype fields, but we lack a way to indicate to a credential helper that we'd like them to be used. Without some sort of indication, the credential helper doesn't know if it should try to provide us a username and password, or a pre-encoded credential. For example, the helper might prefer a more restricted Bearer token if pre-encoded credentials are possible, but might have to fall back to more general username and password if not. Let's provide a simple way to indicate whether Git (or, for that matter, the helper) is capable of understanding the authtype and credential fields. We send this capability when we generate a request, and the other side may reply to indicate to us that it does, too. For now, don't enable sending capabilities for the HTTP code. In a future commit, we'll introduce appropriate handling for that code, which requires more in-depth work. The logic for determining whether a capability is supported may seem complex, but it is not. At each stage, we emit the capability to the following stage if all preceding stages have declared it. Thus, if the caller to git credential fill didn't declare it, then we won't send it to the helper, and if fill's caller did send but the helper doesn't understand it, then we won't send it on in the response. If we're an internal user, then we know about all capabilities and will request them. For "git credential approve" and "git credential reject", we set the helper capability before calling the helper, since we assume that the input we're getting from the external program comes from a previous call to "git credential fill", and thus we'll invoke send a capability to the helper if and only if we got one from the standard input, which is the correct behavior. Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

brian m. carlson committed Apr 17, 2024 at 00:02 UTC ca9ccbf67450ffcda235970f0693794cee912562
9 files changed +215 -24
builtin/credential-cache--daemon.c
+1 -1
@@ -115,7 +115,7 @@ static int read_request(FILE *fh, struct credential *c,
115 return error("client sent bogus timeout line: %s", item.buf);
116 *timeout = atoi(p);
117
118 - if (credential_read(c, fh) < 0)
118 + if (credential_read(c, fh, CREDENTIAL_OP_HELPER) < 0)
119 return -1;
120 return 0;
121 }
builtin/credential-store.c
+1 -1
@@ -205,7 +205,7 @@ int cmd_credential_store(int argc, const char **argv, const char *prefix)
205 if (!fns.nr)
206 die("unable to set up default path; use --file");
207
208 - if (credential_read(&c, stdin) < 0)
208 + if (credential_read(&c, stdin, CREDENTIAL_OP_HELPER) < 0)
209 die("unable to read credential");
210
211 if (!strcmp(op, "get"))
builtin/credential.c
+5 -3
@@ -17,15 +17,17 @@ int cmd_credential(int argc, const char **argv, const char *prefix UNUSED)
17 usage(usage_msg);
18 op = argv[1];
19
20 - if (credential_read(&c, stdin) < 0)
20 + if (credential_read(&c, stdin, CREDENTIAL_OP_INITIAL) < 0)
21 die("unable to read credential from stdin");
22
23 if (!strcmp(op, "fill")) {
24 - credential_fill(&c);
25 - credential_write(&c, stdout);
24 + credential_fill(&c, 0);
25 + credential_write(&c, stdout, CREDENTIAL_OP_RESPONSE);
26 } else if (!strcmp(op, "approve")) {
27 + credential_set_all_capabilities(&c, CREDENTIAL_OP_HELPER);
28 credential_approve(&c);
29 } else if (!strcmp(op, "reject")) {
30 + credential_set_all_capabilities(&c, CREDENTIAL_OP_HELPER);
31 credential_reject(&c);
32 } else {
33 usage(usage_msg);
credential.c
+57 -7
@@ -34,6 +34,29 @@ void credential_clear(struct credential *c)
34 credential_init(c);
35 }
36
37 +static void credential_set_capability(struct credential_capability *capa,
38 + enum credential_op_type op_type)
39 +{
40 + switch (op_type) {
41 + case CREDENTIAL_OP_INITIAL:
42 + capa->request_initial = 1;
43 + break;
44 + case CREDENTIAL_OP_HELPER:
45 + capa->request_helper = 1;
46 + break;
47 + case CREDENTIAL_OP_RESPONSE:
48 + capa->response = 1;
49 + break;
50 + }
51 +}
52 +
53 +
54 +void credential_set_all_capabilities(struct credential *c,
55 + enum credential_op_type op_type)
56 +{
57 + credential_set_capability(&c->capa_authtype, op_type);
58 +}
59 +
60 int credential_match(const struct credential *want,
61 const struct credential *have, int match_password)
62 {
@@ -210,7 +233,26 @@ static void credential_getpass(struct credential *c)
233 PROMPT_ASKPASS);
234 }
235
213 -int credential_read(struct credential *c, FILE *fp)
236 +static int credential_has_capability(const struct credential_capability *capa,
237 + enum credential_op_type op_type)
238 +{
239 + /*
240 + * We're checking here if each previous step indicated that we had the
241 + * capability. If it did, then we want to pass it along; conversely, if
242 + * it did not, we don't want to report that to our caller.
243 + */
244 + switch (op_type) {
245 + case CREDENTIAL_OP_HELPER:
246 + return capa->request_initial;
247 + case CREDENTIAL_OP_RESPONSE:
248 + return capa->request_initial && capa->request_helper;
249 + default:
250 + return 0;
251 + }
252 +}
253 +
254 +int credential_read(struct credential *c, FILE *fp,
255 + enum credential_op_type op_type)
256 {
257 struct strbuf line = STRBUF_INIT;
258
@@ -249,6 +291,8 @@ int credential_read(struct credential *c, FILE *fp)
291 c->path = xstrdup(value);
292 } else if (!strcmp(key, "wwwauth[]")) {
293 strvec_push(&c->wwwauth_headers, value);
294 + } else if (!strcmp(key, "capability[]") && !strcmp(value, "authtype")) {
295 + credential_set_capability(&c->capa_authtype, op_type);
296 } else if (!strcmp(key, "password_expiry_utc")) {
297 errno = 0;
298 c->password_expiry_utc = parse_timestamp(value, NULL, 10);
@@ -288,14 +332,19 @@ static void credential_write_item(FILE *fp, const char *key, const char *value,
332 fprintf(fp, "%s=%s\n", key, value);
333 }
334
291 -void credential_write(const struct credential *c, FILE *fp)
335 +void credential_write(const struct credential *c, FILE *fp,
336 + enum credential_op_type op_type)
337 {
338 + if (credential_has_capability(&c->capa_authtype, op_type)) {
339 + credential_write_item(fp, "capability[]", "authtype", 0);
340 + credential_write_item(fp, "authtype", c->authtype, 0);
341 + credential_write_item(fp, "credential", c->credential, 0);
342 + }
343 credential_write_item(fp, "protocol", c->protocol, 1);
344 credential_write_item(fp, "host", c->host, 1);
345 credential_write_item(fp, "path", c->path, 0);
346 credential_write_item(fp, "username", c->username, 0);
347 credential_write_item(fp, "password", c->password, 0);
298 - credential_write_item(fp, "credential", c->credential, 0);
348 credential_write_item(fp, "oauth_refresh_token", c->oauth_refresh_token, 0);
349 if (c->password_expiry_utc != TIME_MAX) {
350 char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
@@ -304,7 +353,6 @@ void credential_write(const struct credential *c, FILE *fp)
353 }
354 for (size_t i = 0; i < c->wwwauth_headers.nr; i++)
355 credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
307 - credential_write_item(fp, "authtype", c->authtype, 0);
356 }
357
358 static int run_credential_helper(struct credential *c,
@@ -327,14 +375,14 @@ static int run_credential_helper(struct credential *c,
375
376 fp = xfdopen(helper.in, "w");
377 sigchain_push(SIGPIPE, SIG_IGN);
330 - credential_write(c, fp);
378 + credential_write(c, fp, want_output ? CREDENTIAL_OP_HELPER : CREDENTIAL_OP_RESPONSE);
379 fclose(fp);
380 sigchain_pop(SIGPIPE);
381
382 if (want_output) {
383 int r;
384 fp = xfdopen(helper.out, "r");
337 - r = credential_read(c, fp);
385 + r = credential_read(c, fp, CREDENTIAL_OP_HELPER);
386 fclose(fp);
387 if (r < 0) {
388 finish_command(&helper);
@@ -367,7 +415,7 @@ static int credential_do(struct credential *c, const char *helper,
415 return r;
416 }
417
370 -void credential_fill(struct credential *c)
418 +void credential_fill(struct credential *c, int all_capabilities)
419 {
420 int i;
421
@@ -375,6 +423,8 @@ void credential_fill(struct credential *c)
423 return;
424
425 credential_apply_config(c);
426 + if (all_capabilities)
427 + credential_set_all_capabilities(c, CREDENTIAL_OP_INITIAL);
428
429 for (i = 0; i < c->helpers.nr; i++) {
430 credential_do(c, c->helpers.items[i].string, "get");
credential.h
+37 -3
@@ -93,6 +93,27 @@
93 * -----------------------------------------------------------------------
94 */
95
96 +/*
97 + * These values define the kind of operation we're performing and the
98 + * capabilities at each stage. The first is either an external request (via git
99 + * credential fill) or an internal request (e.g., via the HTTP) code. The
100 + * second is the call to the credential helper, and the third is the response
101 + * we're providing.
102 + *
103 + * At each stage, we will emit the capability only if the previous stage
104 + * supported it.
105 + */
106 +enum credential_op_type {
107 + CREDENTIAL_OP_INITIAL = 1,
108 + CREDENTIAL_OP_HELPER = 2,
109 + CREDENTIAL_OP_RESPONSE = 3,
110 +};
111 +
112 +struct credential_capability {
113 + unsigned request_initial:1,
114 + request_helper:1,
115 + response:1;
116 +};
117
118 /**
119 * This struct represents a single username/password combination
@@ -136,6 +157,8 @@ struct credential {
157 use_http_path:1,
158 username_from_proto:1;
159
160 + struct credential_capability capa_authtype;
161 +
162 char *username;
163 char *password;
164 char *credential;
@@ -174,8 +197,11 @@ void credential_clear(struct credential *);
197 * returns, the username and password fields of the credential are
198 * guaranteed to be non-NULL. If an error occurs, the function will
199 * die().
200 + *
201 + * If all_capabilities is set, this is an internal user that is prepared
202 + * to deal with all known capabilities, and we should advertise that fact.
203 */
178 -void credential_fill(struct credential *);
204 +void credential_fill(struct credential *, int all_capabilities);
205
206 /**
207 * Inform the credential subsystem that the provided credentials
@@ -198,8 +224,16 @@ void credential_approve(struct credential *);
224 */
225 void credential_reject(struct credential *);
226
201 -int credential_read(struct credential *, FILE *);
202 -void credential_write(const struct credential *, FILE *);
227 +/**
228 + * Enable all of the supported credential flags in this credential.
229 + */
230 +void credential_set_all_capabilities(struct credential *c,
231 + enum credential_op_type op_type);
232 +
233 +int credential_read(struct credential *, FILE *,
234 + enum credential_op_type);
235 +void credential_write(const struct credential *, FILE *,
236 + enum credential_op_type);
237
238 /*
239 * Parse a url into a credential struct, replacing any existing contents.
http.c
+5 -5
@@ -569,7 +569,7 @@ static void init_curl_http_auth(CURL *result)
569 return;
570 }
571
572 - credential_fill(&http_auth);
572 + credential_fill(&http_auth, 0);
573
574 curl_easy_setopt(result, CURLOPT_USERNAME, http_auth.username);
575 curl_easy_setopt(result, CURLOPT_PASSWORD, http_auth.password);
@@ -596,7 +596,7 @@ static void init_curl_proxy_auth(CURL *result)
596 {
597 if (proxy_auth.username) {
598 if (!proxy_auth.password)
599 - credential_fill(&proxy_auth);
599 + credential_fill(&proxy_auth, 0);
600 set_proxyauth_name_password(result);
601 }
602
@@ -630,7 +630,7 @@ static int has_cert_password(void)
630 cert_auth.host = xstrdup("");
631 cert_auth.username = xstrdup("");
632 cert_auth.path = xstrdup(ssl_cert);
633 - credential_fill(&cert_auth);
633 + credential_fill(&cert_auth, 0);
634 }
635 return 1;
636 }
@@ -645,7 +645,7 @@ static int has_proxy_cert_password(void)
645 proxy_cert_auth.host = xstrdup("");
646 proxy_cert_auth.username = xstrdup("");
647 proxy_cert_auth.path = xstrdup(http_proxy_ssl_cert);
648 - credential_fill(&proxy_cert_auth);
648 + credential_fill(&proxy_cert_auth, 0);
649 }
650 return 1;
651 }
@@ -2190,7 +2190,7 @@ static int http_request_reauth(const char *url,
2190 BUG("Unknown http_request target");
2191 }
2192
2193 - credential_fill(&http_auth);
2193 + credential_fill(&http_auth, 0);
2194
2195 return http_request(url, result, target, options);
2196 }
imap-send.c
+1 -1
@@ -944,7 +944,7 @@ static void server_fill_credential(struct imap_server_conf *srvc, struct credent
944 cred->username = xstrdup_or_null(srvc->user);
945 cred->password = xstrdup_or_null(srvc->pass);
946
947 - credential_fill(cred);
947 + credential_fill(cred, 1);
948
949 if (!srvc->user)
950 srvc->user = xstrdup(cred->username);
remote-curl.c
+2 -2
@@ -926,7 +926,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece
926 do {
927 err = probe_rpc(rpc, &results);
928 if (err == HTTP_REAUTH)
929 - credential_fill(&http_auth);
929 + credential_fill(&http_auth, 0);
930 } while (err == HTTP_REAUTH);
931 if (err != HTTP_OK)
932 return -1;
@@ -1044,7 +1044,7 @@ retry:
1044 rpc->any_written = 0;
1045 err = run_slot(slot, NULL);
1046 if (err == HTTP_REAUTH && !large_request) {
1047 - credential_fill(&http_auth);
1047 + credential_fill(&http_auth, 0);
1048 curl_slist_free_all(headers);
1049 goto retry;
1050 }
t/t0300-credentials.sh
+106 -1
@@ -12,7 +12,13 @@ test_expect_success 'setup helper scripts' '
12 IFS==
13 while read key value; do
14 echo >&2 "$whoami: $key=$value"
15 - eval "$key=$value"
15 + if test -z "${key%%*\[\]}"
16 + then
17 + key=${key%%\[\]}
18 + eval "$key=\"\$$key $value\""
19 + else
20 + eval "$key=$value"
21 + fi
22 done
23 IFS=$OIFS
24 EOF
@@ -35,6 +41,16 @@ test_expect_success 'setup helper scripts' '
41 test -z "$pass" || echo password=$pass
42 EOF
43
44 + write_script git-credential-verbatim-cred <<-\EOF &&
45 + authtype=$1; shift
46 + credential=$1; shift
47 + . ./dump
48 + echo capability[]=authtype
49 + test -z "${capability##*authtype*}" || exit 0
50 + test -z "$authtype" || echo authtype=$authtype
51 + test -z "$credential" || echo credential=$credential
52 + EOF
53 +
54 write_script git-credential-verbatim-with-expiry <<-\EOF &&
55 user=$1; shift
56 pass=$1; shift
@@ -64,6 +80,26 @@ test_expect_success 'credential_fill invokes helper' '
80 EOF
81 '
82
83 +test_expect_success 'credential_fill invokes helper with credential' '
84 + check fill "verbatim-cred Bearer token" <<-\EOF
85 + capability[]=authtype
86 + protocol=http
87 + host=example.com
88 + --
89 + capability[]=authtype
90 + authtype=Bearer
91 + credential=token
92 + protocol=http
93 + host=example.com
94 + --
95 + verbatim-cred: get
96 + verbatim-cred: capability[]=authtype
97 + verbatim-cred: protocol=http
98 + verbatim-cred: host=example.com
99 + EOF
100 +'
101 +
102 +
103 test_expect_success 'credential_fill invokes multiple helpers' '
104 check fill useless "verbatim foo bar" <<-\EOF
105 protocol=http
@@ -83,6 +119,42 @@ test_expect_success 'credential_fill invokes multiple helpers' '
119 EOF
120 '
121
122 +test_expect_success 'credential_fill response does not get capabilities when helpers are incapable' '
123 + check fill useless "verbatim foo bar" <<-\EOF
124 + capability[]=authtype
125 + protocol=http
126 + host=example.com
127 + --
128 + protocol=http
129 + host=example.com
130 + username=foo
131 + password=bar
132 + --
133 + useless: get
134 + useless: capability[]=authtype
135 + useless: protocol=http
136 + useless: host=example.com
137 + verbatim: get
138 + verbatim: capability[]=authtype
139 + verbatim: protocol=http
140 + verbatim: host=example.com
141 + EOF
142 +'
143 +
144 +test_expect_success 'credential_fill response does not get capabilities when caller is incapable' '
145 + check fill "verbatim-cred Bearer token" <<-\EOF
146 + protocol=http
147 + host=example.com
148 + --
149 + protocol=http
150 + host=example.com
151 + --
152 + verbatim-cred: get
153 + verbatim-cred: protocol=http
154 + verbatim-cred: host=example.com
155 + EOF
156 +'
157 +
158 test_expect_success 'credential_fill stops when we get a full response' '
159 check fill "verbatim one two" "verbatim three four" <<-\EOF
160 protocol=http
@@ -99,6 +171,25 @@ test_expect_success 'credential_fill stops when we get a full response' '
171 EOF
172 '
173
174 +test_expect_success 'credential_fill thinks a credential is a full response' '
175 + check fill "verbatim-cred Bearer token" "verbatim three four" <<-\EOF
176 + capability[]=authtype
177 + protocol=http
178 + host=example.com
179 + --
180 + capability[]=authtype
181 + authtype=Bearer
182 + credential=token
183 + protocol=http
184 + host=example.com
185 + --
186 + verbatim-cred: get
187 + verbatim-cred: capability[]=authtype
188 + verbatim-cred: protocol=http
189 + verbatim-cred: host=example.com
190 + EOF
191 +'
192 +
193 test_expect_success 'credential_fill continues through partial response' '
194 check fill "verbatim one \"\"" "verbatim two three" <<-\EOF
195 protocol=http
@@ -175,6 +266,20 @@ test_expect_success 'credential_fill passes along metadata' '
266 EOF
267 '
268
269 +test_expect_success 'credential_fill produces no credential without capability' '
270 + check fill "verbatim-cred Bearer token" <<-\EOF
271 + protocol=http
272 + host=example.com
273 + --
274 + protocol=http
275 + host=example.com
276 + --
277 + verbatim-cred: get
278 + verbatim-cred: protocol=http
279 + verbatim-cred: host=example.com
280 + EOF
281 +'
282 +
283 test_expect_success 'credential_approve calls all helpers' '
284 check approve useless "verbatim one two" <<-\EOF
285 protocol=http