/* * Read the value in a given SCRAM exchange message for given attribute. */ static char * read_attr_value(char **input, char attr) { char *begin = *input; char *end; if (*begin != attr) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Expected attribute \"%c\" but found \"%s\".", attr, sanitize_char(*begin)))); begin++; if (*begin != '=') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Expected character \"=\" for attribute \"%c\".", attr))); begin++; end = begin; while (*end && *end != ',') end++; if (*end) { *end = '\0'; *input = end + 1; } else *input = end; return begin; }
/* * Read the next attribute and value in a SCRAM exchange message. * * Returns NULL if there is attribute. */ static char * read_any_attr(char **input, char *attr_p) { char *begin = *input; char *end; char attr = *begin; /*------ * attr-val = ALPHA "=" value * ;; Generic syntax of any attribute sent * ;; by server or client *------ */ if (!((attr >= 'A' && attr <= 'Z') || (attr >= 'a' && attr <= 'z'))) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Attribute expected, but found invalid character \"%s\".", sanitize_char(attr)))); if (attr_p) *attr_p = attr; begin++; if (*begin != '=') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Expected character \"=\" for attribute \"%c\".", attr))); begin++; end = begin; while (*end && *end != ',') end++; if (*end) { *end = '\0'; *input = end + 1; } else *input = end; return begin; }
/* * Read and parse the first message from client in the context of a SCRAM * authentication exchange message. * * At this stage, any errors will be reported directly with ereport(ERROR). */ static void read_client_first_message(scram_state *state, char *input) { input = pstrdup(input); /*------ * The syntax for the client-first-message is: (RFC 5802) * * saslname = 1*(value-safe-char / "=2C" / "=3D") * ;; Conforms to <value>. * * authzid = "a=" saslname * ;; Protocol specific. * * cb-name = 1*(ALPHA / DIGIT / "." / "-") * ;; See RFC 5056, Section 7. * ;; E.g., "tls-server-end-point" or * ;; "tls-unique". * * gs2-cbind-flag = ("p=" cb-name) / "n" / "y" * ;; "n" -> client doesn't support channel binding. * ;; "y" -> client does support channel binding * ;; but thinks the server does not. * ;; "p" -> client requires channel binding. * ;; The selected channel binding follows "p=". * * gs2-header = gs2-cbind-flag "," [ authzid ] "," * ;; GS2 header for SCRAM * ;; (the actual GS2 header includes an optional * ;; flag to indicate that the GSS mechanism is not * ;; "standard", but since SCRAM is "standard", we * ;; don't include that flag). * * username = "******" saslname * ;; Usernames are prepared using SASLprep. * * reserved-mext = "m=" 1*(value-char) * ;; Reserved for signaling mandatory extensions. * ;; The exact syntax will be defined in * ;; the future. * * nonce = "r=" c-nonce [s-nonce] * ;; Second part provided by server. * * c-nonce = printable * * client-first-message-bare = * [reserved-mext ","] * username "," nonce ["," extensions] * * client-first-message = * gs2-header client-first-message-bare * * For example: * n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL * * The "n,," in the beginning means that the client doesn't support * channel binding, and no authzid is given. "n=user" is the username. * However, in PostgreSQL the username is sent in the startup packet, and * the username in the SCRAM exchange is ignored. libpq always sends it * as an empty string. The last part, "r=fyko+d2lbbFgONRv9qkxdawL" is * the client nonce. *------ */ /* read gs2-cbind-flag */ state->cbind_flag = *input; switch (*input) { case 'n': /* Client does not support channel binding */ input++; break; case 'y': /* Client supports channel binding, but we're not doing it today */ input++; break; case 'p': /* * Client requires channel binding. We don't support it. * * RFC 5802 specifies a particular error code, * e=server-does-support-channel-binding, for this. But it can * only be sent in the server-final message, and we don't want to * go through the motions of the authentication, knowing it will * fail, just to send that error message. */ ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("client requires SCRAM channel binding, but it is not supported"))); default: ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Unexpected channel-binding flag \"%s\".", sanitize_char(*input)))); } if (*input != ',') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Comma expected, but found character \"%s\".", sanitize_char(*input)))); input++; /* * Forbid optional authzid (authorization identity). We don't support it. */ if (*input == 'a') ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("client uses authorization identity, but it is not supported"))); if (*input != ',') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Unexpected attribute \"%s\" in client-first-message.", sanitize_char(*input)))); input++; state->client_first_message_bare = pstrdup(input); /* * Any mandatory extensions would go here. We don't support any. * * RFC 5802 specifies error code "e=extensions-not-supported" for this, * but it can only be sent in the server-final message. We prefer to fail * immediately (which the RFC also allows). */ if (*input == 'm') ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("client requires an unsupported SCRAM extension"))); /* * Read username. Note: this is ignored. We use the username from the * startup message instead, still it is kept around if provided as it * proves to be useful for debugging purposes. */ state->client_username = read_attr_value(&input, 'n'); /* read nonce and check that it is made of only printable characters */ state->client_nonce = read_attr_value(&input, 'r'); if (!is_scram_printable(state->client_nonce)) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("non-printable characters in SCRAM nonce"))); /* * There can be any number of optional extensions after this. We don't * support any extensions, so ignore them. */ while (*input != '\0') read_any_attr(&input, NULL); /* success! */ }
/* * Read and parse the first message from client in the context of a SCRAM * authentication exchange message. * * At this stage, any errors will be reported directly with ereport(ERROR). */ static void read_client_first_message(scram_state *state, char *input) { char *channel_binding_type; input = pstrdup(input); /*------ * The syntax for the client-first-message is: (RFC 5802) * * saslname = 1*(value-safe-char / "=2C" / "=3D") * ;; Conforms to <value>. * * authzid = "a=" saslname * ;; Protocol specific. * * cb-name = 1*(ALPHA / DIGIT / "." / "-") * ;; See RFC 5056, Section 7. * ;; E.g., "tls-server-end-point" or * ;; "tls-unique". * * gs2-cbind-flag = ("p=" cb-name) / "n" / "y" * ;; "n" -> client doesn't support channel binding. * ;; "y" -> client does support channel binding * ;; but thinks the server does not. * ;; "p" -> client requires channel binding. * ;; The selected channel binding follows "p=". * * gs2-header = gs2-cbind-flag "," [ authzid ] "," * ;; GS2 header for SCRAM * ;; (the actual GS2 header includes an optional * ;; flag to indicate that the GSS mechanism is not * ;; "standard", but since SCRAM is "standard", we * ;; don't include that flag). * * username = "******" saslname * ;; Usernames are prepared using SASLprep. * * reserved-mext = "m=" 1*(value-char) * ;; Reserved for signaling mandatory extensions. * ;; The exact syntax will be defined in * ;; the future. * * nonce = "r=" c-nonce [s-nonce] * ;; Second part provided by server. * * c-nonce = printable * * client-first-message-bare = * [reserved-mext ","] * username "," nonce ["," extensions] * * client-first-message = * gs2-header client-first-message-bare * * For example: * n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL * * The "n,," in the beginning means that the client doesn't support * channel binding, and no authzid is given. "n=user" is the username. * However, in PostgreSQL the username is sent in the startup packet, and * the username in the SCRAM exchange is ignored. libpq always sends it * as an empty string. The last part, "r=fyko+d2lbbFgONRv9qkxdawL" is * the client nonce. *------ */ /* * Read gs2-cbind-flag. (For details see also RFC 5802 Section 6 "Channel * Binding".) */ state->cbind_flag = *input; switch (*input) { case 'n': /* * The client does not support channel binding or has simply * decided to not use it. In that case just let it go. */ if (state->channel_binding_in_use) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("The client selected SCRAM-SHA-256-PLUS, but the SCRAM message does not include channel binding data."))); input++; if (*input != ',') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Comma expected, but found character \"%s\".", sanitize_char(*input)))); input++; break; case 'y': /* * The client supports channel binding and thinks that the server * does not. In this case, the server must fail authentication if * it supports channel binding. */ if (state->channel_binding_in_use) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("The client selected SCRAM-SHA-256-PLUS, but the SCRAM message does not include channel binding data."))); #ifdef HAVE_BE_TLS_GET_CERTIFICATE_HASH if (state->port->ssl_in_use) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("SCRAM channel binding negotiation error"), errdetail("The client supports SCRAM channel binding but thinks the server does not. " "However, this server does support channel binding."))); #endif input++; if (*input != ',') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Comma expected, but found character \"%s\".", sanitize_char(*input)))); input++; break; case 'p': /* * The client requires channel binding. Channel binding type * follows, e.g., "p=tls-server-end-point". */ if (!state->channel_binding_in_use) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("The client selected SCRAM-SHA-256 without channel binding, but the SCRAM message includes channel binding data."))); channel_binding_type = read_attr_value(&input, 'p'); /* * The only channel binding type we support is * tls-server-end-point. */ if (strcmp(channel_binding_type, "tls-server-end-point") != 0) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), (errmsg("unsupported SCRAM channel-binding type \"%s\"", sanitize_str(channel_binding_type))))); break; default: ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Unexpected channel-binding flag \"%s\".", sanitize_char(*input)))); } /* * Forbid optional authzid (authorization identity). We don't support it. */ if (*input == 'a') ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("client uses authorization identity, but it is not supported"))); if (*input != ',') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Unexpected attribute \"%s\" in client-first-message.", sanitize_char(*input)))); input++; state->client_first_message_bare = pstrdup(input); /* * Any mandatory extensions would go here. We don't support any. * * RFC 5802 specifies error code "e=extensions-not-supported" for this, * but it can only be sent in the server-final message. We prefer to fail * immediately (which the RFC also allows). */ if (*input == 'm') ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("client requires an unsupported SCRAM extension"))); /* * Read username. Note: this is ignored. We use the username from the * startup message instead, still it is kept around if provided as it * proves to be useful for debugging purposes. */ state->client_username = read_attr_value(&input, 'n'); /* read nonce and check that it is made of only printable characters */ state->client_nonce = read_attr_value(&input, 'r'); if (!is_scram_printable(state->client_nonce)) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("non-printable characters in SCRAM nonce"))); /* * There can be any number of optional extensions after this. We don't * support any extensions, so ignore them. */ while (*input != '\0') read_any_attr(&input, NULL); /* success! */ }