1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-07-01 11:32:48 -05:00

HTTP proxy: implement Digest authentication.

In http.c, this drops in reasonably neatly alongside the existing
support for Basic, now that we're waiting for an initial 407 response
from the proxy to tell us which auth mechanism it would prefer to use.

The rest of this patch is mostly contriving to add testcrypt support
for the function in cproxy.c that generates the complicated output
header to go in the HTTP request: you need about a dozen assorted
parameters, the actual response hash has two more hashes in its
preimage, and there's even an option to hash the username as well if
necessary. Much more complicated than CHAP (which is just plain
HMAC-MD5), so it needs testing!

Happily, RFC 7616 comes with some reasonably useful test cases, and
I've managed to transcribe them directly into cryptsuite.py and
demonstrate that my response-generator agrees with them.

End-to-end testing of the whole system was done against Squid 4.13
(specifically, the squid package in Debian bullseye, version 4.13-10).
This commit is contained in:
Simon Tatham
2021-11-20 14:56:32 +00:00
parent 52ee636b09
commit 3c21fa54c5
9 changed files with 511 additions and 39 deletions

View File

@ -41,6 +41,8 @@ static bool read_line(bufchain *input, strbuf *output, bool is_header)
return true;
}
typedef enum HttpAuthType { AUTH_NONE, AUTH_BASIC, AUTH_DIGEST } HttpAuthType;
typedef struct HttpProxyNegotiator {
int crLine;
strbuf *response, *header, *token;
@ -49,7 +51,14 @@ typedef struct HttpProxyNegotiator {
strbuf *username, *password;
int http_status;
bool connection_close;
bool tried_no_auth, try_auth_from_conf;
HttpAuthType next_auth_type;
bool try_auth_from_conf;
strbuf *realm, *nonce, *opaque, *uri;
bool got_opaque;
uint32_t nonce_count;
bool digest_nonce_was_stale;
HttpDigestHash digest_hash;
bool hash_username;
prompts_t *prompts;
int username_prompt_index, password_prompt_index;
size_t content_length;
@ -66,6 +75,17 @@ static ProxyNegotiator *proxy_http_new(const ProxyNegotiatorVT *vt)
s->token = strbuf_new();
s->username = strbuf_new();
s->password = strbuf_new_nm();
s->realm = strbuf_new();
s->nonce = strbuf_new();
s->opaque = strbuf_new();
s->uri = strbuf_new();
s->nonce_count = 0;
/*
* Always start with a CONNECT request containing no auth. If the
* proxy rejects that, it will tell us what kind of auth it would
* prefer.
*/
s->next_auth_type = AUTH_NONE;
return &s->pn;
}
@ -77,6 +97,10 @@ static void proxy_http_free(ProxyNegotiator *pn)
strbuf_free(s->token);
strbuf_free(s->username);
strbuf_free(s->password);
strbuf_free(s->realm);
strbuf_free(s->nonce);
strbuf_free(s->opaque);
strbuf_free(s->uri);
if (s->prompts)
free_prompts(s->prompts);
sfree(s);
@ -110,6 +134,21 @@ static inline bool is_separator(char c)
#define HTTP_SEPARATORS
static bool get_end_of_header(HttpProxyNegotiator *s)
{
size_t pos = s->header_pos;
while (pos < s->header->len && is_whitespace(s->header->s[pos]))
pos++;
if (pos == s->header->len) {
s->header_pos = pos;
return true;
}
return false;
}
static bool get_token(HttpProxyNegotiator *s)
{
size_t pos = s->header_pos;
@ -150,6 +189,39 @@ static bool get_separator(HttpProxyNegotiator *s, char sep)
return true;
}
static bool get_quoted_string(HttpProxyNegotiator *s)
{
size_t pos = s->header_pos;
while (pos < s->header->len && is_whitespace(s->header->s[pos]))
pos++;
if (pos == s->header->len)
return false; /* end of string */
if (s->header->s[pos] != '"')
return false;
pos++;
strbuf_clear(s->token);
while (pos < s->header->len && s->header->s[pos] != '"') {
if (s->header->s[pos] == '\\') {
/* Backslash makes the next char literal, even if it's " or \ */
pos++;
if (pos == s->header->len)
return false; /* unexpected end of string */
}
put_byte(s->token, s->header->s[pos++]);
}
if (pos == s->header->len)
return false; /* no closing quote */
pos++;
s->header_pos = pos;
return true;
}
static void proxy_http_process_queue(ProxyNegotiator *pn)
{
HttpProxyNegotiator *s = container_of(pn, HttpProxyNegotiator, pn);
@ -164,46 +236,60 @@ static void proxy_http_process_queue(ProxyNegotiator *pn)
if (s->username->len || s->password->len)
s->try_auth_from_conf = true;
/*
* Set up the host:port string we're trying to connect to, also
* used as the URI string in HTTP Digest auth.
*/
{
char dest[512];
sk_getaddr(pn->ps->remote_addr, dest, lenof(dest));
put_fmt(s->uri, "%s:%d", dest, pn->ps->remote_port);
}
while (true) {
/*
* Standard prefix for the HTTP CONNECT request.
*/
{
char dest[512];
sk_getaddr(pn->ps->remote_addr, dest, lenof(dest));
put_fmt(pn->output,
"CONNECT %s:%d HTTP/1.1\r\n"
"Host: %s:%d\r\n",
dest, pn->ps->remote_port, dest, pn->ps->remote_port);
}
put_fmt(pn->output,
"CONNECT %s HTTP/1.1\r\n"
"Host: %s\r\n", s->uri->s, s->uri->s);
/*
* Optionally send an HTTP Basic auth header with the username
* and password. We do this only after we've first tried no
* authentication at all (even if we have a password to start
* with).
* Add an auth header, if we're planning to this time round.
*/
if (s->tried_no_auth) {
if (s->username->len || s->password->len) {
put_datalit(pn->output, "Proxy-Authorization: Basic ");
if (s->next_auth_type == AUTH_BASIC) {
put_datalit(pn->output, "Proxy-Authorization: Basic ");
strbuf *base64_input = strbuf_new_nm();
put_datapl(base64_input, ptrlen_from_strbuf(s->username));
put_byte(base64_input, ':');
put_datapl(base64_input, ptrlen_from_strbuf(s->password));
strbuf *base64_input = strbuf_new_nm();
put_datapl(base64_input, ptrlen_from_strbuf(s->username));
put_byte(base64_input, ':');
put_datapl(base64_input, ptrlen_from_strbuf(s->password));
char base64_output[4];
for (size_t i = 0, e = base64_input->len; i < e; i += 3) {
base64_encode_atom(base64_input->u + i,
e-i > 3 ? 3 : e-i, base64_output);
put_data(pn->output, base64_output, 4);
}
strbuf_free(base64_input);
smemclr(base64_output, sizeof(base64_output));
put_datalit(pn->output, "\r\n");
char base64_output[4];
for (size_t i = 0, e = base64_input->len; i < e; i += 3) {
base64_encode_atom(base64_input->u + i,
e-i > 3 ? 3 : e-i, base64_output);
put_data(pn->output, base64_output, 4);
}
} else {
s->tried_no_auth = true;
strbuf_free(base64_input);
smemclr(base64_output, sizeof(base64_output));
put_datalit(pn->output, "\r\n");
} else if (s->next_auth_type == AUTH_DIGEST) {
put_datalit(pn->output, "Proxy-Authorization: Digest ");
http_digest_response(BinarySink_UPCAST(pn->output),
ptrlen_from_strbuf(s->username),
ptrlen_from_strbuf(s->password),
ptrlen_from_strbuf(s->realm),
PTRLEN_LITERAL("CONNECT"),
ptrlen_from_strbuf(s->uri),
PTRLEN_LITERAL("auth"),
ptrlen_from_strbuf(s->nonce),
(s->got_opaque ?
ptrlen_from_strbuf(s->opaque) :
make_ptrlen(NULL, 0)),
++s->nonce_count, s->digest_hash,
s->hash_username);
put_datalit(pn->output, "\r\n");
}
/*
@ -281,7 +367,114 @@ static void proxy_http_process_queue(ProxyNegotiator *pn)
continue;
if (!stricmp(s->token->s, "Basic")) {
/* fine, we know how to do Basic auth */
s->next_auth_type = AUTH_BASIC;
} else if (!stricmp(s->token->s, "Digest")) {
if (!http_digest_available) {
pn->error = dupprintf(
"HTTP proxy requested Digest authentication "
"which we do not support");
crStopV;
}
/* Parse the rest of the Digest header */
s->digest_nonce_was_stale = false;
s->digest_hash = HTTP_DIGEST_MD5;
strbuf_clear(s->realm);
strbuf_clear(s->nonce);
strbuf_clear(s->opaque);
s->got_opaque = false;
s->hash_username = false;
while (true) {
if (!get_token(s))
goto bad_digest;
if (!stricmp(s->token->s, "realm")) {
if (!get_separator(s, '=') ||
!get_quoted_string(s))
goto bad_digest;
put_datapl(s->realm, ptrlen_from_strbuf(s->token));
} else if (!stricmp(s->token->s, "nonce")) {
if (!get_separator(s, '=') ||
!get_quoted_string(s))
goto bad_digest;
/* If we have a fresh nonce, reset the
* nonce count. Otherwise, keep incrementing it. */
if (!ptrlen_eq_ptrlen(
ptrlen_from_strbuf(s->token),
ptrlen_from_strbuf(s->nonce)))
s->nonce_count = 0;
put_datapl(s->nonce, ptrlen_from_strbuf(s->token));
} else if (!stricmp(s->token->s, "opaque")) {
if (!get_separator(s, '=') ||
!get_quoted_string(s))
goto bad_digest;
put_datapl(s->opaque,
ptrlen_from_strbuf(s->token));
s->got_opaque = true;
} else if (!stricmp(s->token->s, "stale")) {
if (!get_separator(s, '=') ||
!get_token(s))
goto bad_digest;
s->digest_nonce_was_stale = !stricmp(
s->token->s, "true");
} else if (!stricmp(s->token->s, "userhash")) {
if (!get_separator(s, '=') ||
!get_token(s))
goto bad_digest;
s->hash_username = !stricmp(s->token->s, "true");
} else if (!stricmp(s->token->s, "algorithm")) {
if (!get_separator(s, '=') ||
!get_token(s))
goto bad_digest;
bool found = false;
for (size_t i = 0; i < N_HTTP_DIGEST_HASHES; i++) {
if (!stricmp(s->token->s, httphashnames[i])) {
s->digest_hash = i;
found = true;
break;
}
}
if (!found) {
pn->error = dupprintf(
"HTTP proxy requested Digest hash "
"algorithm '%s' which we do not support",
s->token->s);
crStopV;
}
} else if (!stricmp(s->token->s, "qop")) {
if (!get_separator(s, '=') ||
!get_quoted_string(s))
goto bad_digest;
if (stricmp(s->token->s, "auth")) {
pn->error = dupprintf(
"HTTP proxy requested Digest quality-of-"
"protection type '%s' which we do not "
"support", s->token->s);
crStopV;
}
} else {
/* Ignore any other auth-param */
if (!get_separator(s, '=') ||
(!get_quoted_string(s) && !get_token(s)))
goto bad_digest;
}
if (get_end_of_header(s))
break;
if (!get_separator(s, ','))
goto bad_digest;
}
s->next_auth_type = AUTH_DIGEST;
continue;
bad_digest:
pn->error = dupprintf("HTTP proxy sent Digest auth "
"request we could not parse");
crStopV;
} else {
pn->error = dupprintf("HTTP proxy asked for unsupported "
"authentication type '%s'",
@ -314,6 +507,13 @@ static void proxy_http_process_queue(ProxyNegotiator *pn)
continue;
}
/* If the server sent us stale="true" in a Digest auth
* header, that means we _don't_ need to request a new
* password yet; just try again with the existing details
* and the fresh nonce it sent us. */
if (s->digest_nonce_was_stale)
continue;
/* Either we never had a password in the first place, or
* the one we already presented was rejected. We can only
* proceed from here if we have a way to ask the user