diff options
author | Edward Thomson <ethomson@edwardthomson.com> | 2014-07-31 18:43:20 -0400 |
---|---|---|
committer | Edward Thomson <ethomson@edwardthomson.com> | 2014-08-15 11:12:42 -0400 |
commit | 315cb38e1e32e77037e82c6cbdc383c151041112 (patch) | |
tree | ef6f113d3df5acfe713a1c0b26b2e3234c7c8870 /src | |
parent | e003f83a5840d6e9966f1830768771bcd205ba52 (diff) | |
download | libgit2-315cb38e1e32e77037e82c6cbdc383c151041112.tar.gz |
Add GSSAPI support for SPNEGO/Kerberos auth over HTTP
Diffstat (limited to 'src')
-rw-r--r-- | src/transports/http.c | 317 |
1 files changed, 287 insertions, 30 deletions
diff --git a/src/transports/http.c b/src/transports/http.c index ffa293ec0..c43f6c548 100644 --- a/src/transports/http.c +++ b/src/transports/http.c @@ -12,6 +12,11 @@ #include "netops.h" #include "smart.h" +#ifdef GIT_GSSAPI +# include <gssapi.h> +# include <krb5.h> +#endif + static const char *upload_pack_service = "upload-pack"; static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack"; static const char *upload_pack_service_url = "/git-upload-pack"; @@ -21,6 +26,17 @@ static const char *receive_pack_service_url = "/git-receive-pack"; static const char *get_verb = "GET"; static const char *post_verb = "POST"; static const char *basic_authtype = "Basic"; +static const char *negotiate_authtype = "Negotiate"; + +#ifdef GIT_GSSAPI +static gss_OID_desc negotiate_oid_spnego = + { 6, (void *) "\x2b\x06\x01\x05\x05\x02" }; +static gss_OID_desc negotiate_oid_krb5 = + { 9, (void *) "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02" }; + +static gss_OID negotiate_oids[] = + { &negotiate_oid_spnego, &negotiate_oid_krb5, NULL }; +#endif #define OWNING_SUBTRANSPORT(s) ((http_subtransport *)(s)->parent.subtransport) @@ -37,6 +53,7 @@ enum last_cb { typedef enum { GIT_HTTP_AUTH_BASIC = 1, + GIT_HTTP_AUTH_NEGOTIATE = 2, } http_authmechanism_t; typedef struct { @@ -61,6 +78,7 @@ typedef struct { git_cred *cred; git_cred *url_cred; http_authmechanism_t auth_mechanism; + char *auth_challenge; bool connected; /* Parser structures */ @@ -76,6 +94,14 @@ typedef struct { enum last_cb last_cb; int parse_error; unsigned parse_finished : 1; + +#ifdef GIT_GSSAPI + unsigned negotiate_configured : 1, + negotiate_complete : 1; + git_buf negotiate_target; + gss_ctx_id_t negotiate_context; + gss_OID negotiate_oid; +#endif } http_subtransport; typedef struct { @@ -88,12 +114,17 @@ typedef struct { size_t *bytes_read; } parser_context; -static int apply_basic_credential(git_buf *buf, git_cred *cred) +static int apply_basic_credential( + git_buf *buf, + http_subtransport *transport, + git_cred *cred) { git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; git_buf raw = GIT_BUF_INIT; int error = -1; + GIT_UNUSED(transport); + git_buf_printf(&raw, "%s:%s", c->username, c->password); if (git_buf_oom(&raw) || @@ -112,6 +143,188 @@ on_error: return error; } +#ifdef GIT_GSSAPI + +static void negotiate_err_set( + OM_uint32 status_major, + OM_uint32 status_minor, + const char *message) +{ + gss_buffer_desc buffer = GSS_C_EMPTY_BUFFER; + OM_uint32 status_display, context = 0; + + if (gss_display_status(&status_display, status_major, GSS_C_GSS_CODE, + GSS_C_NO_OID, &context, &buffer) == GSS_S_COMPLETE) { + giterr_set(GITERR_NET, "%s: %.*s (%d.%d)", + message, (int)buffer.length, (const char *)buffer.value, + status_major, status_minor); + gss_release_buffer(&status_minor, &buffer); + } else { + giterr_set(GITERR_NET, "%s: unknown negotiate error (%d.%d)", + message, status_major, status_minor); + } +} + +static int negotiate_configure(http_subtransport *transport) +{ + OM_uint32 status_major, status_minor; + gss_OID item, *oid; + gss_OID_set mechanism_list; + size_t i; + + /* Query supported mechanisms looking for SPNEGO) */ + if (GSS_ERROR(status_major = + gss_indicate_mechs(&status_minor, &mechanism_list))) { + negotiate_err_set(status_major, status_minor, + "could not query mechanisms"); + return -1; + } + + if (mechanism_list) { + for (oid = negotiate_oids; *oid; oid++) { + for (i = 0; i < mechanism_list->count; i++) { + item = &mechanism_list->elements[i]; + + if (item->length == (*oid)->length && + memcmp(item->elements, (*oid)->elements, item->length) == 0) { + transport->negotiate_oid = *oid; + break; + } + + } + + if (transport->negotiate_oid) + break; + } + } + + gss_release_oid_set(&status_minor, &mechanism_list); + + if (!transport->negotiate_oid) { + giterr_set(GITERR_NET, "Negotiate authentication is not supported"); + return -1; + } + + git_buf_puts(&transport->negotiate_target, "HTTP@"); + git_buf_puts(&transport->negotiate_target, transport->connection_data.host); + + if (git_buf_oom(&transport->negotiate_target)) + return -1; + + transport->negotiate_context = GSS_C_NO_CONTEXT; + transport->negotiate_configured = 1; + return 0; +} + +static int negotiate_next_token( + git_buf *buf, + http_subtransport *transport, + git_cred *cred) +{ + OM_uint32 status_major, status_minor; + gss_buffer_desc target_buffer = GSS_C_EMPTY_BUFFER, + input_token = GSS_C_EMPTY_BUFFER, + output_token = GSS_C_EMPTY_BUFFER; + gss_buffer_t input_token_ptr = GSS_C_NO_BUFFER; + git_buf input_buf = GIT_BUF_INIT; + gss_name_t server = NULL; + gss_OID mech; + size_t challenge_len; + int error = 0; + + GIT_UNUSED(cred); + + target_buffer.value = (void *)transport->negotiate_target.ptr; + target_buffer.length = transport->negotiate_target.size; + + status_major = gss_import_name(&status_minor, &target_buffer, + GSS_C_NT_HOSTBASED_SERVICE, &server); + + if (GSS_ERROR(status_major)) { + negotiate_err_set(status_major, status_minor, + "Could not parse principal"); + error = -1; + goto done; + } + + challenge_len = transport->auth_challenge ? + strlen(transport->auth_challenge) : 0; + assert(challenge_len >= 9); + + if (challenge_len > 9) { + if (git_buf_decode_base64(&input_buf, + transport->auth_challenge + 10, challenge_len - 10) < 0) { + giterr_set(GITERR_NET, "Invalid negotiate challenge from server"); + error = -1; + goto done; + } + + input_token.value = input_buf.ptr; + input_token.length = input_buf.size; + input_token_ptr = &input_token; + } else if (transport->negotiate_context != GSS_C_NO_CONTEXT) { + giterr_set(GITERR_NET, "Could not restart authentication"); + error = -1; + goto done; + } + + mech = &negotiate_oid_spnego; + + if (GSS_ERROR(status_major = gss_init_sec_context( + &status_minor, + GSS_C_NO_CREDENTIAL, + &transport->negotiate_context, + server, + mech, + GSS_C_DELEG_FLAG | GSS_C_MUTUAL_FLAG, + GSS_C_INDEFINITE, + GSS_C_NO_CHANNEL_BINDINGS, + input_token_ptr, + NULL, + &output_token, + NULL, + NULL))) { + negotiate_err_set(status_major, status_minor, "Negotiate failure"); + error = -1; + goto done; + } + + /* This message merely told us auth was complete; we do not respond. */ + if (status_major == GSS_S_COMPLETE) { + transport->negotiate_complete = 1; + goto done; + } + + git_buf_puts(buf, "Authorization: Negotiate "); + git_buf_encode_base64(buf, output_token.value, output_token.length); + git_buf_puts(buf, "\r\n"); + + if (git_buf_oom(buf)) + error = -1; + +done: + gss_release_name(&status_minor, &server); + gss_release_buffer(&status_minor, (gss_buffer_t) &output_token); + git_buf_free(&input_buf); + return error; +} + +static int apply_negotiate_credential( + git_buf *buf, + http_subtransport *transport, + git_cred *cred) +{ + if (!transport->negotiate_configured && negotiate_configure(transport) < 0) + return -1; + + if (transport->negotiate_complete) + return 0; + + return negotiate_next_token(buf, transport, cred); +} + +#endif /* GIT_GSSAPI */ + static int gen_request( git_buf *buf, http_stream *s, @@ -137,17 +350,26 @@ static int gen_request( git_buf_puts(buf, "Accept: */*\r\n"); /* Apply credentials to the request */ +#ifdef GIT_GSSAPI + if (t->cred && t->cred->credtype == GIT_CREDTYPE_DEFAULT && + (t->auth_mechanism & GIT_HTTP_AUTH_NEGOTIATE)) { + if (apply_negotiate_credential(buf, t, t->cred) < 0) + return -1; + } else +#endif + if (t->cred && t->cred->credtype == GIT_CREDTYPE_USERPASS_PLAINTEXT && - t->auth_mechanism == GIT_HTTP_AUTH_BASIC && - apply_basic_credential(buf, t->cred) < 0) - return -1; + t->auth_mechanism == GIT_HTTP_AUTH_BASIC) { + if (apply_basic_credential(buf, t, t->cred) < 0) + return -1; + } /* Use url-parsed basic auth if username and password are both provided */ if (!t->cred && t->connection_data.user && t->connection_data.pass) { if (!t->url_cred && git_cred_userpass_plaintext_new(&t->url_cred, t->connection_data.user, t->connection_data.pass) < 0) return -1; - if (apply_basic_credential(buf, t->url_cred) < 0) return -1; + if (apply_basic_credential(buf, t, t->url_cred) < 0) return -1; } git_buf_puts(buf, "\r\n"); @@ -158,19 +380,32 @@ static int gen_request( return 0; } -static int parse_unauthorized_response( +static int parse_authenticate_response( git_vector *www_authenticate, int *allowed_types, - http_authmechanism_t *auth_mechanism) + http_authmechanism_t *auth_mechanism, + char **auth_challenge) { unsigned i; char *entry; git_vector_foreach(www_authenticate, i, entry) { - if (!strncmp(entry, basic_authtype, 5) && + if (!strncmp(entry, negotiate_authtype, 9) && + (entry[9] == '\0' || entry[9] == ' ')) { + *allowed_types |= GIT_CREDTYPE_DEFAULT; + *auth_mechanism = GIT_HTTP_AUTH_NEGOTIATE; + + *auth_challenge = git__strdup(entry); + GITERR_CHECK_ALLOC(*auth_challenge); + } + + else if (!strncmp(entry, basic_authtype, 5) && (entry[5] == '\0' || entry[5] == ' ')) { *allowed_types |= GIT_CREDTYPE_USERPASS_PLAINTEXT; *auth_mechanism = GIT_HTTP_AUTH_BASIC; + + *auth_challenge = git__strdup(entry); + GITERR_CHECK_ALLOC(*auth_challenge); } } @@ -248,7 +483,7 @@ static int on_headers_complete(http_parser *parser) http_subtransport *t = ctx->t; http_stream *s = ctx->s; git_buf buf = GIT_BUF_INIT; - int error = 0, no_callback = 0; + int error = 0, no_callback = 0, allowed_auth_types = 0; /* Both parse_header_name and parse_header_value are populated * and ready for consumption. */ @@ -256,26 +491,31 @@ static int on_headers_complete(http_parser *parser) if (on_header_ready(t) < 0) return t->parse_error = PARSE_ERROR_GENERIC; - /* Check for an authentication failure. */ + /* Capture authentication headers which may be a 401 (authentication + * is not complete) or a 200 (simply informing us that auth *is* + * complete.) + */ + git__free(t->auth_challenge); + t->auth_challenge = NULL; - if (parser->status_code == 401 && - get_verb == s->verb) { + if (parse_authenticate_response(&t->www_authenticate, + &allowed_auth_types, + &t->auth_mechanism, + &t->auth_challenge) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; + + /* Check for an authentication failure. */ + if (parser->status_code == 401 && get_verb == s->verb) { if (!t->owner->cred_acquire_cb) { no_callback = 1; } else { - int allowed_types = 0; - - if (parse_unauthorized_response(&t->www_authenticate, - &allowed_types, &t->auth_mechanism) < 0) - return t->parse_error = PARSE_ERROR_GENERIC; - - if (allowed_types && - (!t->cred || 0 == (t->cred->credtype & allowed_types))) { + if (allowed_auth_types && + (!t->cred || 0 == (t->cred->credtype & allowed_auth_types))) { error = t->owner->cred_acquire_cb(&t->cred, t->owner->url, t->connection_data.user, - allowed_types, + allowed_auth_types, t->owner->cred_acquire_payload); if (error == GIT_PASSTHROUGH) { @@ -511,10 +751,8 @@ replay: clear_parser_state(t); - if (gen_request(&request, s, 0) < 0) { - giterr_set(GITERR_NET, "Failed to generate request"); + if (gen_request(&request, s, 0) < 0) return -1; - } if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) { git_buf_free(&request); @@ -613,10 +851,8 @@ static int http_stream_write_chunked( clear_parser_state(t); - if (gen_request(&request, s, 0) < 0) { - giterr_set(GITERR_NET, "Failed to generate request"); + if (gen_request(&request, s, 0) < 0) return -1; - } if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) { git_buf_free(&request); @@ -688,10 +924,8 @@ static int http_stream_write_single( clear_parser_state(t); - if (gen_request(&request, s, len) < 0) { - giterr_set(GITERR_NET, "Failed to generate request"); + if (gen_request(&request, s, len) < 0) return -1; - } if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) goto on_error; @@ -855,6 +1089,24 @@ static int http_action( return -1; } +static void clear_negotiate_state(http_subtransport *t) +{ +#ifdef GIT_GSSAPI + OM_uint32 status_minor; + + if (t->negotiate_context != GSS_C_NO_CONTEXT) { + gss_delete_sec_context(&status_minor, &t->negotiate_context, GSS_C_NO_BUFFER); + t->negotiate_context = GSS_C_NO_CONTEXT; + } + + git_buf_free(&t->negotiate_target); + + t->negotiate_configured = 0; + t->negotiate_complete = 0; + t->negotiate_oid = NULL; +#endif +} + static int http_close(git_smart_subtransport *subtransport) { http_subtransport *t = (http_subtransport *) subtransport; @@ -876,6 +1128,11 @@ static int http_close(git_smart_subtransport *subtransport) t->url_cred = NULL; } + git__free(t->auth_challenge); + t->auth_challenge = NULL; + + clear_negotiate_state(t); + gitno_connection_data_free_ptrs(&t->connection_data); return 0; |