/* $NetBSD: mech_digestmd5.c,v 1.13 2018/01/30 15:28:39 shm Exp $ */
/* Copyright (c) 2010 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Mateusz Kocielski.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the NetBSD
* Foundation, Inc. and its contributors.
* 4. Neither the name of The NetBSD Foundation nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <sys/cdefs.h>
__RCSID("$NetBSD: mech_digestmd5.c,v 1.13 2018/01/30 15:28:39 shm Exp $");
#include <sys/param.h>
#include <assert.h>
#include <ctype.h>
#include <md5.h>
#include <saslc.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
#include "buffer.h"
#include "crypto.h"
#include "error.h"
#include "list.h"
#include "mech.h"
#include "msg.h"
#include "saslc_private.h"
/* See RFC 2831. */
/*
* TODO:
*
* 1) Add support for Subsequent Authentication (see RFC 2831 section 2.2).
*/
/* properties */
#define SASLC_DIGESTMD5_AUTHCID SASLC_PROP_AUTHCID
#define SASLC_DIGESTMD5_AUTHZID SASLC_PROP_AUTHZID
#define SASLC_DIGESTMD5_CIPHERMASK SASLC_PROP_CIPHERMASK
#define SASLC_DIGESTMD5_HOSTNAME SASLC_PROP_HOSTNAME
#define SASLC_DIGESTMD5_MAXBUF SASLC_PROP_MAXBUF
#define SASLC_DIGESTMD5_PASSWD SASLC_PROP_PASSWD
#define SASLC_DIGESTMD5_QOPMASK SASLC_PROP_QOPMASK
#define SASLC_DIGESTMD5_REALM SASLC_PROP_REALM
#define SASLC_DIGESTMD5_SERVICE SASLC_PROP_SERVICE
#define SASLC_DIGESTMD5_SERVNAME SASLC_PROP_SERVNAME
/*
* XXX: define this if you want to be able to set a fixed cnonce for
* debugging purposes.
*/
#define SASLC_DIGESTMD5_CNONCE "CNONCE"
/*
* XXX: define this if you want to test the saslc_sess_encode() and
* saslc_sess_decode() routines against themselves, i.e., have them
* use the same key.
*/
#define SASLC_DIGESTMD5_SELFTEST "SELFTEST"
#define DEFAULT_QOP_MASK (F_QOP_NONE | F_QOP_INT | F_QOP_CONF)
#define DEFAULT_CIPHER_MASK (F_CIPHER_DES | F_CIPHER_3DES | \
F_CIPHER_RC4 | F_CIPHER_RC4_40 | \
F_CIPHER_RC4_56 | F_CIPHER_AES)
#define DEFAULT_MAXBUF 0x10000
#define MAX_MAXBUF 0xffffff
#define INVALID_MAXBUF(m) ((m) <= sizeof(md5hash_t) && (m) > MAX_MAXBUF)
#define NONCE_LEN 33 /* Minimum recommended length is 64bits (rfc2831).
cyrus-sasl uses 33 bytes. */
typedef enum {
CHALLENGE_IGNORE = -1, /* must be -1 */
CHALLENGE_REALM = 0,
CHALLENGE_NONCE = 1,
CHALLENGE_QOP = 2,
CHALLENGE_STALE = 3,
CHALLENGE_MAXBUF = 4,
CHALLENGE_CHARSET = 5,
CHALLENGE_ALGORITHM = 6,
CHALLENGE_CIPHER = 7
} challenge_t;
typedef enum {
/*
* NB: Values used to index cipher_tbl[] and cipher_ctx_tbl[]
* in cipher_context_create().
*/
CIPHER_DES = 0,
CIPHER_3DES = 1,
CIPHER_RC4 = 2,
CIPHER_RC4_40 = 3,
CIPHER_RC4_56 = 4,
CIPHER_AES = 5
} cipher_t;
#define F_CIPHER_DES (1 << CIPHER_DES)
#define F_CIPHER_3DES (1 << CIPHER_3DES)
#define F_CIPHER_RC4 (1 << CIPHER_RC4)
#define F_CIPHER_RC4_40 (1 << CIPHER_RC4_40)
#define F_CIPHER_RC4_56 (1 << CIPHER_RC4_56)
#define F_CIPHER_AES (1 << CIPHER_AES)
static const named_flag_t cipher_tbl[] = {
/* NB: to be indexed by cipher_t values */
{ "des", F_CIPHER_DES },
{ "3des", F_CIPHER_3DES },
{ "rc4", F_CIPHER_RC4 },
{ "rc4-40", F_CIPHER_RC4_40 },
{ "rc4-56", F_CIPHER_RC4_56 },
{ "aes", F_CIPHER_AES },
{ NULL, 0 }
};
static inline const char *
cipher_name(cipher_t cipher)
{
assert(cipher < __arraycount(cipher_tbl) - 1); /* NULL terminated */
if (cipher < __arraycount(cipher_tbl) - 1)
return cipher_tbl[cipher].name;
return NULL;
}
static inline unsigned int
cipher_list_flags(list_t *list)
{
return saslc__list_flags(list, cipher_tbl);
}
typedef struct { /* data parsed from challenge */
bool utf8;
bool algorithm;
bool stale;
char * nonce;
list_t * realm;
uint32_t cipher_flags;
uint32_t qop_flags;
size_t maxbuf;
} cdata_t;
typedef struct { /* response data */
/* NB: the qop is in saslc__mech_sess_t */
char *authcid;
char *authzid;
char *cnonce;
char *digesturi;
char *passwd;
char *realm;
cipher_t cipher;
int nonce_cnt;
size_t maxbuf;
} rdata_t;
typedef uint8_t md5hash_t[MD5_DIGEST_LENGTH];
typedef struct {
md5hash_t kic; /* client->server integrity key */
md5hash_t kis; /* server->client integrity key */
md5hash_t kcc; /* client->server confidentiality key */
md5hash_t kcs; /* server->client confidentiality key */
} keys_t;
typedef struct cipher_context_t {
size_t blksize; /* block size for cipher */
EVP_CIPHER_CTX *evp_ctx; /* openssl EVP context */
} cipher_context_t;
typedef struct coder_context_t {
uint8_t *key; /* key for coding */
uint32_t seqnum; /* 4 byte sequence number */
void *buf_ctx; /* buffer context */
cipher_context_t *cph_ctx; /* cipher context */
saslc_sess_t *sess; /* session: for error setting */
} coder_context_t;
/* mech state */
typedef struct {
saslc__mech_sess_t mech_sess; /* must be first */
cdata_t cdata; /* data parsed from challenge string */
rdata_t rdata; /* data used for response string */
keys_t keys; /* keys */
coder_context_t dec_ctx; /* decode context */
coder_context_t enc_ctx; /* encode context */
} saslc__mech_digestmd5_sess_t;
/**
* @brief if possible convert a UTF-8 string to a ISO8859-1 string.
* @param utf8 original UTF-8 string.
* @param iso8859 pointer to pointer to the malloced ISO8859-1 string.
* @return -1 if the string cannot be translated.
*
* NOTE: this allocates memory for its output and the caller is
* responsible for freeing it.
*/
static int
utf8_to_8859_1(char *utf8, char **iso8859)
{
unsigned char *s, *d, *end, *src;
size_t cnt;
src = (unsigned char *)utf8;
cnt = 0;
end = src + strlen(utf8);
for (s = src; s < end; ++s) {
if (*s > 0xC3) /* abort if outside 8859-1 range */
return -1;
/*
* Look for valid 2 byte UTF-8 encoding with, 8 bits
* of info. Quit if invalid pair found.
*/
if (*s >= 0xC0 && *s <= 0xC3) { /* 2 bytes, 8 bits */
if (++s == end || *s < 0x80 || *s > 0xBF)
return -1; /* broken utf-8 encoding */
}
cnt++;
}
/* Allocate adequate space. */
d = malloc(cnt + 1);
if (d == NULL)
return -1;
*iso8859 = (char *)d;
/* convert to 8859-1 */
do {
for (s = src; s < end && *s < 0xC0; ++s)
*d++ = *s;
if (s + 1 >= end)
break;
*d++ = ((s[0] & 0x3) << 6) | (s[1] & 0x3f);
src = s + 2;
} while (src < end);
*d = '\0';
return 0;
}
/**
* @brief unquote a string by removing escapes.
* @param str string to unquote.
* @return NULL on failure
*
* NOTE: this allocates memory for its output and the caller is
* responsible for freeing it.
*/
static char *
unq(const char *str)
{
const char *s;
char *unq_str, *d;
int escaped;
unq_str = malloc(strlen(str) + 1);
if (unq_str == NULL)
return NULL;
escaped = 0;
d = unq_str;
for (s = str; *s != '\0'; s++) {
switch (*s) {
case '\\':
if (escaped)
*d++ = *s;
escaped = !escaped;
break;
default:
*d++ = *s;
escaped = 0;
}
}
*d = '\0';
return unq_str;
}
/**
* @brief computing MD5(username:realm:password).
* @param ms mechanism session
* @param buf buffer for hash
* @return 0 on success, -1 on failure
*/
static int
saslc__mech_digestmd5_userhash(saslc__mech_digestmd5_sess_t *ms, uint8_t *buf)
{
char *tmp;
char *unq_username, *unq_realm;
ssize_t len;
if ((unq_username = unq(ms->rdata.authcid)) == NULL)
return -1;
/********************************************************/
/* RFC 2831 section 2.1.2 */
/* ... If the directive is missing, "realm-value" will */
/* set to the empty string when computing A1. */
/********************************************************/
if (ms->rdata.realm == NULL)
unq_realm = strdup("");
else
unq_realm = unq(ms->rdata.realm);
if (unq_realm == NULL) {
free(unq_username);
return -1;
}
len = asprintf(&tmp, "%s:%s:%s",
unq_username, unq_realm, ms->rdata.passwd);
free(unq_realm);
free(unq_username);
if (len == -1)
return -1;
saslc__crypto_md5_hash(tmp, (size_t)len, buf);
memset(tmp, 0, (size_t)len);
free(tmp);
return 0;
}
/**
* @brief setup the appropriate QOP keys as determined by the chosen
* QOP type (see RFC2831 sections 2.3 and 2.4).
* @param ms mechanism session
* @param a1hash MD5(a1)
* @return 0 on success, -1 on failure
*/
static int
setup_qop_keys(saslc__mech_digestmd5_sess_t *ms, md5hash_t a1hash)
{
#define KIC_MAGIC "Digest session key to client-to-server signing key magic constant"
#define KIS_MAGIC "Digest session key to server-to-client signing key magic constant"
#define KCC_MAGIC "Digest H(A1) to client-to-server sealing key magic constant"
#define KCS_MAGIC "Digest H(A1) to server-to-client sealing key magic constant"
#define KIC_MAGIC_LEN (sizeof(KIC_MAGIC) - 1)
#define KIS_MAGIC_LEN (sizeof(KIS_MAGIC) - 1)
#define KCC_MAGIC_LEN (sizeof(KCC_MAGIC) - 1)
#define KCS_MAGIC_LEN (sizeof(KCS_MAGIC) - 1)
#define MAX_MAGIC_LEN KIC_MAGIC_LEN
char buf[MD5_DIGEST_LENGTH + MAX_MAGIC_LEN];
size_t buflen;
size_t n;
switch (ms->mech_sess.qop) {
case QOP_NONE:
/* nothing to do */
break;
case QOP_CONF:
/*************************************************************************/
/* See RFC2831 section 2.4 (Confidentiality Protection) */
/* */
/* The key for confidentiality protecting messages from client to server */
/* is: */
/* */
/* Kcc = MD5({H(A1)[0..n], */
/* "Digest H(A1) to client-to-server sealing key magic constant"}) */
/* */
/* The key for confidentiality protecting messages from server to client */
/* is: */
/* */
/* Kcs = MD5({H(A1)[0..n], */
/* "Digest H(A1) to server-to-client sealing key magic constant"}) */
/* */
/* where MD5 is as specified in [RFC 1321]. For cipher "rc4-40" n is 5; */
/* for "rc4-56" n is 7; for the rest n is 16. */
/*************************************************************************/
switch (ms->rdata.cipher) {
case CIPHER_RC4_40: n = 5; break;
case CIPHER_RC4_56: n = 7; break;
default: n = MD5_DIGEST_LENGTH; break;
}
memcpy(buf, a1hash, n);
memcpy(buf + n, KCC_MAGIC, KCC_MAGIC_LEN);
buflen = n + KCC_MAGIC_LEN;
saslc__crypto_md5_hash(buf, buflen, ms->keys.kcc);
memcpy(buf + n, KCS_MAGIC, KCS_MAGIC_LEN);
buflen = n + KCS_MAGIC_LEN;
saslc__crypto_md5_hash(buf, buflen, ms->keys.kcs);
/*FALLTHROUGH*/
case QOP_INT:
/*************************************************************************/
/* See RFC2831 section 2.3 (Integrity Protection) */
/* The key for integrity protecting messages from client to server is: */
/* */
/* Kic = MD5({H(A1), */
/* "Digest session key to client-to-server signing key magic constant"}) */
/* */
/* The key for integrity protecting messages from server to client is: */
/* */
/* Kis = MD5({H(A1), */
/* "Digest session key to server-to-client signing key magic constant"}) */
/*************************************************************************/
memcpy(buf, a1hash, MD5_DIGEST_LENGTH);
memcpy(buf + MD5_DIGEST_LENGTH, KIC_MAGIC, KIC_MAGIC_LEN);
buflen = MD5_DIGEST_LENGTH + KIC_MAGIC_LEN;
saslc__crypto_md5_hash(buf, buflen, ms->keys.kic);
memcpy(buf + MD5_DIGEST_LENGTH, KIS_MAGIC, KIS_MAGIC_LEN);
buflen = MD5_DIGEST_LENGTH + KIS_MAGIC_LEN;
saslc__crypto_md5_hash(buf, buflen, ms->keys.kis);
break;
}
return 0;
#undef KIC_MAGIC
#undef KIS_MAGIC
#undef KCC_MAGIC
#undef KCS_MAGIC
#undef KIC_MAGIC_LEN
#undef KIS_MAGIC_LEN
#undef KCC_MAGIC_LEN
#undef KCS_MAGIC_LEN
#undef MAX_MAGIC_LEN
}
/**
* @brief computes A1 hash value (see: RFC2831)
* @param ms mechanism session
* @return hash in hex form
*/
static char *
saslc__mech_digestmd5_a1(saslc__mech_digestmd5_sess_t *ms)
{
char *tmp1, *tmp2, *r;
char *unq_authzid;
md5hash_t a1hash, userhash;
int plen;
size_t len;
/*****************************************************************************/
/* If authzid is specified, then A1 is */
/* */
/* A1 = { H({ unq(username-value), ":", unq(realm-value), ":", passwd }), */
/* ":", nonce-value, ":", cnonce-value, ":", unq(authzid-value) } */
/* */
/* If authzid is not specified, then A1 is */
/* */
/* A1 = { H({ unq(username-value), ":", unq(realm-value), ":", passwd }), */
/* ":", nonce-value, ":", cnonce-value } */
/*****************************************************************************/
if (saslc__mech_digestmd5_userhash(ms, userhash) == -1)
return NULL;
if (ms->rdata.authzid == NULL)
plen = asprintf(&tmp1, ":%s:%s",
ms->cdata.nonce, ms->rdata.cnonce);
else {
if ((unq_authzid = unq(ms->rdata.authzid)) == NULL)
return NULL;
plen = asprintf(&tmp1, ":%s:%s:%s",
ms->cdata.nonce, ms->rdata.cnonce, unq_authzid);
free(unq_authzid);
}
if (plen == -1)
return NULL;
len = plen;
tmp2 = malloc(MD5_DIGEST_LENGTH + len);
if (tmp2 == NULL) {
free(tmp1);
return NULL;
}
memcpy(tmp2, userhash, MD5_DIGEST_LENGTH);
memcpy(tmp2 + MD5_DIGEST_LENGTH, tmp1, len);
free(tmp1);
saslc__crypto_md5_hash(tmp2, MD5_DIGEST_LENGTH + len, a1hash);
free(tmp2);
r = saslc__crypto_hash_to_hex(a1hash);
setup_qop_keys(ms, a1hash);
return r;
}
/**
* @brief computes A2 hash value (see: RFC2831)
* @param ms mechanism session
* @param method string indicating method "AUTHENTICATE" or ""
* @return hash converted to ascii
*/
static char *
saslc__mech_digestmd5_a2(saslc__mech_digestmd5_sess_t *ms,
const char *method)
{
char *tmp, *r;
int rval;
/*****************************************************************/
/* If the "qop" directive's value is "auth", then A2 is: */
/* */
/* A2 = { "AUTHENTICATE:", digest-uri-value } */
/* */
/* If the "qop" value is "auth-int" or "auth-conf" then A2 is: */
/* */
/* A2 = { "AUTHENTICATE:", digest-uri-value, */
/* ":00000000000000000000000000000000" } */
/*****************************************************************/
rval = -1;
switch(ms->mech_sess.qop) {
case QOP_NONE:
rval = asprintf(&tmp, "%s:%s", method,
ms->rdata.digesturi);
break;
case QOP_INT:
case QOP_CONF:
rval = asprintf(&tmp,
"%s:%s:00000000000000000000000000000000",
method, ms->rdata.digesturi);
break;
}
if (rval == -1)
return NULL;
r = saslc__crypto_md5_hex(tmp, strlen(tmp));
free(tmp);
return r;
}
/**
* @brief computes result hash.
* @param ms mechanism session
* @param a1 A1 hash value
* @param a2 A2 hash value
* @return hash converted to ascii, NULL on failure.
*/
static char *
saslc__mech_digestmd5_rhash(saslc__mech_digestmd5_sess_t *ms,
const char *a1, const char *a2)
{
char *tmp, *r;
/******************************************************************/
/* response-value = */
/* HEX( KD ( HEX(H(A1)), */
/* { nonce-value, ":" nc-value, ":", */
/* cnonce-value, ":", qop-value, ":", HEX(H(A2)) })) */
/******************************************************************/
if (asprintf(&tmp, "%s:%s:%08x:%s:%s:%s", a1, ms->cdata.nonce,
ms->rdata.nonce_cnt, ms->rdata.cnonce,
saslc__mech_qop_name(ms->mech_sess.qop), a2)
== -1)
return NULL;
r = saslc__crypto_md5_hex(tmp, strlen(tmp));
free(tmp);
return r;
}
/**
* @brief building response string. Basing on
* session and mechanism properties.
* @param ms mechanism session
* @param method string indicating method: "AUTHENTICATE" or ""
* @return response string, NULL on failure.
*/
static char *
saslc__mech_digestmd5_response(saslc__mech_digestmd5_sess_t *ms,
const char *method)
{
char *r, *a1, *a2;
/******************************************************************/
/* charset = "charset" "=" "utf-8" */
/* */
/* This directive, if present, specifies that the client has used */
/* UTF-8 [UTF-8] encoding for the username, realm and */
/* password. If present, the username, realm and password are in */
/* Unicode, prepared using the "SASLPrep" profile [SASLPrep] of */
/* the "stringprep" algorithm [StringPrep] and than encoded as */
/* UTF-8 [UTF-8]. If not present, the username and password must */
/* be encoded in ISO 8859-1 [ISO-8859] (of which US-ASCII */
/* [USASCII] is a subset). The client should send this directive */
/* only if the server has indicated it supports UTF-8 */
/* [UTF-8]. The directive is needed for backwards compatibility */
/* with HTTP Digest, which only supports ISO 8859-1. */
/******************************************************************/
/*
* NOTE: We don't set charset in the response, so this is not
* an issue here. However, see the note in stringprep_realms()
* which is called when processing the challenge.
*/
/******************************************************************/
/* response-value = */
/* HEX( KD ( HEX(H(A1)), */
/* { nonce-value, ":" nc-value, ":", */
/* cnonce-value, ":", qop-value, ":", HEX(H(A2)) })) */
/******************************************************************/
r = NULL;
a1 = saslc__mech_digestmd5_a1(ms);
if (a1 == NULL)
return NULL;
a2 = saslc__mech_digestmd5_a2(ms, method);
if (a2 != NULL) {
r = saslc__mech_digestmd5_rhash(ms, a1, a2);
free(a2);
}
free(a1);
return r;
}
/**
* @brief Choose a string from a user provided host qualified list,
* i.e., a comma delimited list with possible hostname qualifiers on
* the elements.
* @param hqlist a comma delimited list with entries of the form
* "[hostname:]string".
* @param hostname the hostname to use in the selection.
* @param rval pointer to location for returned string. Set to NULL
* if none found, otherwise set to strdup(3) of the string found.
* @return 0 on success, -1 on failure (no memory).
*
* NOTE: hqlist and rval must not be NULL.
* NOTE: this allocates memory for its output and the caller is
* responsible for freeing it.
*/
static int
choose_from_hqlist(const char *hqlist, const char *hostname, char **rval)
{
list_t *l, *list;
size_t len;
char *p;
if (saslc__list_parse(&list, hqlist) == -1)
return -1; /* no memory */
/*
* If the user provided a list and the caller provided a
* hostname, pick the first string from the list that
* corresponds to the hostname.
*/
if (hostname != NULL) {
len = strlen(hostname);
for (l = list; l != NULL; l = l->next) {
p = l->value + len;
if (*p != ':' ||
strncasecmp(l->value, hostname, len) != 0)
continue;
if (*(++p) != '\0' && isalnum((unsigned char)*p)) {
if ((p = strdup(p)) == NULL)
goto nomem;
goto done;
}
}
}
/*
* If one couldn't be found, look for first string in the list
* without a hostname specifier.
*/
p = NULL;
for (l = list; l != NULL; l = l->next) {
if (strchr(l->value, ':') == NULL) {
if ((p = strdup(l->value)) == NULL)
goto nomem;
goto done;
}
}
done:
saslc__list_free(list);
*rval = p;
return 0;
nomem:
saslc__list_free(list);
return -1;
}
/**
* @brief builds digesturi string
* @param serv_type type of service to use, e.g., "smtp"
* @param host fully-qualified canonical DNS name of host
* @param serv_name service name if it is replicated via DNS records; may
* be NULL.
* @return digesturi string, NULL on failure.
*/
static char *
saslc__mech_digestmd5_digesturi(saslc_sess_t *sess, const char *serv_host)
{
const char *serv_list;
char *serv_name;
const char *serv_type;
char *r;
int rv;
serv_type = saslc_sess_getprop(sess, SASLC_DIGESTMD5_SERVICE);
if (serv_type == NULL) {
saslc__error_set(ERR(sess), ERROR_MECH,
"service is required for an authentication");
return NULL;
}
serv_list = saslc_sess_getprop(sess, SASLC_DIGESTMD5_SERVNAME);
if (serv_list == NULL)
serv_name = NULL;
else if (choose_from_hqlist(serv_list, serv_host, &serv_name) == -1)
goto nomem;
saslc__msg_dbg("%s: serv_name='%s'", __func__,
serv_name ? serv_name : "<null>");
/****************************************************************/
/* digest-uri = "digest-uri" "=" <"> digest-uri-value <"> */
/* digest-uri-value = serv-type "/" host [ "/" serv-name ] */
/* */
/* If the service is not replicated, or the serv-name is */
/* identical to the host, then the serv-name component MUST be */
/* omitted. The service is considered to be replicated if the */
/* client's service-location process involves resolution using */
/* standard DNS lookup operations, and if these operations */
/* involve DNS records (such as SRV, or MX) which resolve one */
/* DNS name into a set of other DNS names. */
/****************************************************************/
rv = serv_name == NULL || strcmp(serv_host, serv_name) == 0
? asprintf(&r, "%s/%s", serv_type, serv_host)
: asprintf(&r, "%s/%s/%s", serv_type, serv_host, serv_name);
if (serv_name != NULL)
free(serv_name);
if (rv == -1)
goto nomem;
saslc__msg_dbg("%s: digest-uri='%s'", __func__, r);
return r;
nomem:
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
return NULL;
}
/**
* @brief creates client's nonce. (Basing on crypto.h)
* @param s length of nonce
* @return nonce string, NULL on failure.
*/
static char *
saslc__mech_digestmd5_nonce(size_t s)
{
char *nonce;
char *r;
nonce = saslc__crypto_nonce(s);
if (nonce == NULL)
return NULL;
if (saslc__crypto_encode_base64(nonce, s, &r, NULL) == -1)
return NULL;
free(nonce);
return r;
}
/**
* @brief strip quotes from a string (modifies the string)
* @param str the string
* @return string without quotes.
*/
static char *
strip_quotes(char *str)
{
char *p;
size_t len;
if (*str != '"')
return str;
len = strlen(str);
p = str + len;
if (len < 2 || p[-1] != '"')
return str;
p[-1] = '\0';
return ++str;
}
/**
* @brief convert a list of realms from utf-8 to iso8859-q if necessary.
* @param is_utf8 the characterset of the realms (true if utf8)
* @param realms the realm list
*/
static int
stringprep_realms(bool is_utf8, list_t *realms)
{
list_t *l;
char *utf8, *iso8859;
/******************************************************************/
/* If at least one realm is present and the charset directive is */
/* also specified (which means that realm(s) are encoded as */
/* UTF-8), the client should prepare each instance of realm using */
/* the "SASLPrep" profile [SASLPrep] of the "stringprep" */
/* algorithm [StringPrep]. If preparation of a realm instance */
/* fails or results in an empty string, the client should abort */
/* the authentication exchange. */
/******************************************************************/
if (!is_utf8)
return 0;
for (l = realms; l != NULL; l = l->next) {
utf8 = l->value;
if (utf8_to_8859_1(utf8, &iso8859) == -1)
return -1;
free(utf8);
l->value = iso8859;
}
return 0;
}
/**
* @brief choose a realm from a list of possible realms provided by the server
* @param sess the session context
* @param realms the list of realms
* @return our choice of realm or NULL on failure. It is the user's
* responsibility to free the memory allocated for the return string.
*/
static char *
choose_realm(saslc_sess_t *sess, const char *hostname, list_t *realms)
{
const char *user_realms;
list_t *l;
char *p = NULL;
/*****************************************************************/
/* The realm containing the user's account. This directive is */
/* required if the server provided any realms in the */
/* "digest-challenge", in which case it may appear exactly once */
/* and its value SHOULD be one of those realms. If the directive */
/* is missing, "realm-value" will set to the empty string when */
/* computing A1 (see below for details). */
/*****************************************************************/
user_realms = saslc_sess_getprop(sess, SASLC_DIGESTMD5_REALM);
/*
* If the challenge provided no realms, try to pick one from a
* user specified list, which may be keyed by the hostname.
* If one can't be found, return NULL;
*/
if (realms == NULL) {
/*
* No realm was supplied in challenge. Figure out a
* plausable default.
*/
if (user_realms == NULL) {
saslc__error_set(ERR(sess), ERROR_MECH,
"cannot determine the realm");
return NULL;
}
if (choose_from_hqlist(user_realms, hostname, &p) == -1)
goto nomem;
if (p == NULL)
saslc__error_set(ERR(sess), ERROR_MECH,
"cannot choose a realm");
return p;
}
/************************************************************/
/* Multiple realm directives are allowed, in which case the */
/* user or client must choose one as the realm for which to */
/* supply to username and password. */
/************************************************************/
/*
* If the user hasn't specified any realms, or we can't find
* one from the user provided list, just take the first realm
* from the challenge.
*/
if (user_realms == NULL)
goto use_1st_realm;
if (choose_from_hqlist(user_realms, hostname, &p) == -1)
goto nomem;
if (p == NULL)
goto use_1st_realm;
/*
* If we found a matching user provide realm, make sure it is
* on the list of realms. If it isn't, just take the first
* realm in the challenge.
*/
for (l = realms; l != NULL; l = l->next) {
if (strcasecmp(p, l->value) == 0)
return p;
}
use_1st_realm:
free(p);
if ((p = strdup(realms->value)) == NULL)
goto nomem;
return p;
nomem:
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
return NULL;
}
/**
* @brief destroy a cipher context
* @param ctx cipher context
* @return nothing
*/
static void
cipher_context_destroy(cipher_context_t *ctx)
{
if (ctx != NULL) {
if (ctx->evp_ctx != NULL)
EVP_CIPHER_CTX_free(ctx->evp_ctx);
free(ctx);
}
}
/**
* @brief slide the bits from 7 bytes into the high 7 bits of 8 bites
* @param ikey input key
* @param okey output key
*
* This matches cyrus-sasl 2.1.23
*/
static inline void
slidebits(uint8_t *ikey, uint8_t *okey)
{
okey[0] = ikey[0] << 0;
okey[1] = ikey[0] << 7 | (unsigned)ikey[1] >> 1;
okey[2] = ikey[1] << 6 | (unsigned)ikey[2] >> 2;
okey[3] = ikey[2] << 5 | (unsigned)ikey[3] >> 3;
okey[4] = ikey[3] << 4 | (unsigned)ikey[4] >> 4;
okey[5] = ikey[4] << 3 | (unsigned)ikey[5] >> 5;
okey[6] = ikey[5] << 2 | (unsigned)ikey[6] >> 6;
okey[7] = ikey[6] << 1;
}
/**
* @brief convert our key to a DES key
* @param key our key
* @param keylen our key length
* @param deskey the key in DES format
*
* NOTE: The openssl implementations of "des" and "3des" expect their
* keys to be in the high 7 bits of 8 bytes and 16 bytes,
* respectively. Thus, our key length will be 7 and 14 bytes,
* respectively.
*/
static void
make_deskey(uint8_t *key, size_t keylen, uint8_t *deskey)
{
assert(keylen == 7 || keylen == 14);
slidebits(deskey + 0, key + 0);
if (keylen == 14)
slidebits(deskey + 7, key + 7);
}
/**
* @brief create a cipher context, including EVP cipher initialization.
* @param sess session context
* @param cipher cipher to use
* @param do_enc encode context if set, decode context if 0
* @param key crypt key to use
* @return cipher context, or NULL on error
*/
static cipher_context_t *
cipher_context_create(saslc_sess_t *sess, cipher_t cipher, int do_enc, uint8_t *key)
{
#define AES_IV_MAGIC "aes-128"
#define AES_IV_MAGIC_LEN (sizeof(AES_IV_MAGIC) - 1)
static const struct cipher_ctx_tbl_s {
cipher_t eval; /* for error checking */
const EVP_CIPHER *(*evp_type)(void);/* type of cipher */
size_t keylen; /* key length */
ssize_t blksize; /* block size for cipher */
size_t ivlen; /* initial value length */
} cipher_ctx_tbl[] = {
/* NB: table indexed by cipher_t */
/* eval evp_type keylen blksize ivlen */
{ CIPHER_DES, EVP_des_cbc, 7, 8, 8 },
{ CIPHER_3DES, EVP_des_ede_cbc, 14, 8, 8 },
{ CIPHER_RC4, EVP_rc4, 16, 1, 0 },
{ CIPHER_RC4_40, EVP_rc4, 5, 1, 0 },
{ CIPHER_RC4_56, EVP_rc4, 7, 1, 0 },
{ CIPHER_AES, EVP_aes_128_cbc, 16, 16, 16 }
};
const struct cipher_ctx_tbl_s *ctp;
char buf[sizeof(md5hash_t) + AES_IV_MAGIC_LEN];
uint8_t deskey[16];
md5hash_t aes_iv; /* initial value buffer for aes */
cipher_context_t *ctx; /* cipher context */
uint8_t *ivp;
const char *errmsg;
int rv;
/*************************************************************************/
/* See draft-ietf-sasl-rfc2831bis-02.txt section 2.4 (mentions "aes") */
/* The key for the "rc4" and "aes" ciphers is all 16 bytes of Kcc or Kcs.*/
/* The key for the "rc4-40" cipher is the first 5 bytes of Kcc or Kcs. */
/* The key for the "rc4-56" is the first 7 bytes of Kcc or Kcs. */
/* The key for "des" is the first 7 bytes of Kcc or Kcs. */
/* The key for "3des" is the first 14 bytes of Kcc or Kcs. */
/* */
/* The IV used to send/receive the initial buffer of security encoded */
/* data for "des" and "3des" is the last 8 bytes of Kcc or Kcs. For all */
/* subsequent buffers the last 8 bytes of the ciphertext of the buffer */
/* NNN is used as the IV for the buffer (NNN + 1). */
/* */
/* The IV for the "aes" cipher in CBC mode for messages going from the */
/* client to the server (IVc) consists of 16 bytes calculated as */
/* follows: IVc = MD5({Kcc, "aes-128"}) */
/* */
/* The IV for the "aes" cipher in CBC mode for messages going from the */
/* server to the client (IVs) consists of 16 bytes calculated as */
/* follows: IVs = MD5({Kcs, "aes-128"}) */
/*************************************************************************/
assert(cipher < __arraycount(cipher_ctx_tbl));
if (cipher >= __arraycount(cipher_ctx_tbl)) {
saslc__error_set_errno(ERR(sess), ERROR_BADARG);
return NULL;
}
ctx = malloc(sizeof(*ctx));
if (ctx == NULL) {
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
return NULL;
}
ctp = &cipher_ctx_tbl[cipher];
assert(ctp->eval == cipher);
ctx->blksize = ctp->blksize;
ctx->evp_ctx = EVP_CIPHER_CTX_new();
if (ctx->evp_ctx == NULL) {
errmsg = "EVP_CIPHER_CTX_new failed";
goto err;
}
if (EVP_CipherInit_ex(ctx->evp_ctx, ctp->evp_type(), NULL, NULL, NULL,
do_enc) == 0) {
errmsg = "EVP_CipherInit_ex failed";
goto err;
}
if (EVP_CIPHER_CTX_set_padding(ctx->evp_ctx, 0) == 0) {
errmsg = "EVP_CIPHER_CTX_set_padding failed";
goto err;
}
ivp = NULL;
switch (cipher) { /* prepare key and IV */
case CIPHER_RC4:
case CIPHER_RC4_40:
case CIPHER_RC4_56:
assert(ctp->ivlen == 0); /* no IV */
rv = EVP_CIPHER_CTX_set_key_length(ctx->evp_ctx,
(int)ctp->keylen);
if (rv == 0) {
errmsg = "EVP_CIPHER_CTX_set_key_length failed";
goto err;
}
break;
case CIPHER_DES:
case CIPHER_3DES:
assert(ctp->ivlen == 8);
ivp = key + 8;
make_deskey(key, ctp->keylen, deskey);
key = deskey;
break;
case CIPHER_AES:
assert(ctp->ivlen == 16);
/* IVs = MD5({Kcs, "aes-128"}) */
memcpy(buf, key, sizeof(md5hash_t));
memcpy(buf + sizeof(md5hash_t), AES_IV_MAGIC, AES_IV_MAGIC_LEN);
saslc__crypto_md5_hash(buf, sizeof(buf), aes_iv);
ivp = aes_iv;
break;
}
if (EVP_CipherInit_ex(ctx->evp_ctx, NULL, NULL, key, ivp, do_enc) == 0) {
errmsg = "EVP_CipherInit_ex 2 failed";
goto err;
}
return ctx;
err:
cipher_context_destroy(ctx);
saslc__error_set(ERR(sess), ERROR_MECH, errmsg);
return NULL;
#undef AES_IV_MAGIC_LEN
#undef AES_IV_MAGIC
}
/**
* @brief compute the necessary padding length
* @param ctx the cipher context
* @param inlen the data length to put in the packet
* @return the length of padding needed (zero if none needed)
*/
static size_t
get_padlen(cipher_context_t *ctx, size_t inlen)
{
size_t blksize;
if (ctx == NULL)
return 0;
blksize = ctx->blksize;
if (blksize == 1)
return 0;
return blksize - ((inlen + 10) % blksize);
}
/**
* @brief compute the packet integrity including the version and
* sequence number
* @param key the hmac_md5 hash key
* @param seqnum the sequence number
* @param in the input buffer
* @param inlen the input buffer length
* @return 0 on success, -1 on failure
*/
static int
packet_integrity(md5hash_t key, uint32_t seqnum, void *in, size_t inlen,
md5hash_t mac)
{
be32enc(in, seqnum);
if (saslc__crypto_hmac_md5_hash(key, MD5_DIGEST_LENGTH, in, inlen, mac)
== -1)
return -1;
/* we keep only the first 10 bytes of the hash */
be16enc(mac + 10, 0x0001); /* add 2 byte version number */
be32enc(mac + 12, seqnum); /* add 4 byte sequence number */
return 0;
}
/**
* @brief encode or decode a buffer (in place)
* @param ctx the cipher context
* @param in the input buffer
* @param inlen the buffer length
* @return the length of the result left in the input buffer after
* processing, or -1 on failure.
*/
static ssize_t
cipher_update(cipher_context_t *ctx, void *in, size_t inlen)
{
int outl, rv;
void *out;
out = in; /* XXX: this assumes we can encoded and decode in place */
rv = EVP_CipherUpdate(ctx->evp_ctx, out, &outl, in, (int)inlen);
if (rv == 0)
return -1;
return outl;
}
/**
* @brief incapsulate a message with confidentiality (sign and encrypt)
* @param ctx coder context
* @param in pointer to message to encode
* @param inlen length of message
* @param out encoded output packet (including prefixed 4 byte length field)
* @param outlen decoded output packet length
* @returns 0 on success, -1 on failure
*
* NOTE: this allocates memory for its output and the caller is
* responsible for freeing it.
*
* integrity (auth-int):
* len, HMAC(ki, {SeqNum, msg})[0..9], x0001, SeqNum
*
* confidentiality (auth-conf):
* len, CIPHER(Kc, {msg, pag, HMAC(ki, {SeqNum, msg})[0..9]}), x0001, SeqNum
*/
static ssize_t
encode_buffer(coder_context_t *ctx, const void *in, size_t inlen,
void **out, size_t *outlen)
{
void *buf;
uint8_t *mac, *p;
ssize_t tmplen;
size_t buflen;
size_t padlen;
padlen = get_padlen(ctx->cph_ctx, inlen);
buflen = 4 + inlen + padlen + sizeof(md5hash_t);
buf = malloc(buflen);
if (buf == NULL) {
saslc__error_set_errno(ERR(ctx->sess), ERROR_NOMEM);
return -1;
}
p = buf;
memcpy(p + 4, in, inlen);
mac = p + 4 + inlen + padlen;
if (packet_integrity(ctx->key, ctx->seqnum, buf, 4 + inlen, mac)
== -1) {
saslc__error_set(ERR(ctx->sess), ERROR_MECH, "HMAC failed");
free(buf);
return -1;
}
if (padlen)
memset(p + 4 + inlen, (int)padlen, padlen);
if (ctx->cph_ctx != NULL) {
if ((tmplen = cipher_update(ctx->cph_ctx, p + 4,
inlen + padlen + 10)) == -1) {
saslc__error_set(ERR(ctx->sess), ERROR_MECH,
"cipher error");
free(buf);
return -1;
}
assert((size_t)tmplen == inlen + padlen + 10);
if ((size_t)tmplen != inlen + padlen + 10)
return -1;
}
be32enc(buf, (uint32_t)(buflen - 4));
*out = buf;
*outlen = buflen;
ctx->seqnum++; /* wraps at 2^32 */
return 0;
}
/**
* @brief decode one complete confidentiality encoded packet
* @param ctx coder context
* @param in pointer to packet, including the beginning 4 byte length field.
* @param inlen length of packet
* @param out decoded output
* @param outlen decoded output length
* @returns 0 on success, -1 on failure
*
* NOTE: this modifies the intput buffer!
* NOTE: this allocates memory for its output and the caller is
* responsible for freeing it.
*
* integrity (auth-int):
* len, HMAC(ki, {SeqNum, msg})[0..9], x0001, SeqNum
*
* confidentiality (auth-conf):
* len, CIPHER(Kc, {msg, pag, HMAC(ki, {SeqNum, msg})[0..9]}), x0001, SeqNum
*/
static ssize_t
decode_buffer(coder_context_t *ctx, void *in, size_t inlen,
void **out, size_t *outlen)
{
md5hash_t mac;
void *buf;
uint8_t *p;
size_t blksize, buflen, padlen;
ssize_t tmplen;
uint32_t len;
padlen = get_padlen(ctx->cph_ctx, 1);
if (inlen < 4 + 1 + padlen + MD5_DIGEST_LENGTH) {
saslc__error_set(ERR(ctx->sess), ERROR_MECH,
"zero payload packet");
return -1;
}
len = be32dec(in);
if (len + 4 != inlen) {
saslc__error_set(ERR(ctx->sess), ERROR_MECH,
"bad packet length");
return -1;
}
if (ctx->cph_ctx != NULL) {
p = in;
if ((tmplen = cipher_update(ctx->cph_ctx, p + 4, len - 6)) == -1) {
saslc__error_set(ERR(ctx->sess), ERROR_MECH,
"cipher error");
return -1;
}
assert(tmplen == (ssize_t)len - 6);
if (tmplen != (ssize_t)len - 6)
return -1;
}
blksize = ctx->cph_ctx ? ctx->cph_ctx->blksize : 0;
if (blksize <= 1)
padlen = 0;
else{
p = in;
padlen = p[inlen - sizeof(md5hash_t) - 1];
if (padlen > blksize || padlen == 0) {
saslc__error_set(ERR(ctx->sess), ERROR_MECH,
"invalid padding length after decode");
return -1;
}
}
if (packet_integrity(ctx->key, ctx->seqnum, in,
inlen - padlen - sizeof(mac), mac) == -1) {
saslc__error_set(ERR(ctx->sess), ERROR_MECH, "HMAC failed");
return -1;
}
p = in;
p += 4 + len - MD5_DIGEST_LENGTH;
if (memcmp(p, mac, MD5_DIGEST_LENGTH) != 0) {
uint32_t seqnum;
p = in;
seqnum = be32dec(p + inlen - 4);
saslc__error_set(ERR(ctx->sess), ERROR_MECH,
seqnum != ctx->seqnum ? "invalid MAC (bad seqnum)" :
"invalid MAC");
return -1;
}
buflen = len - padlen - MD5_DIGEST_LENGTH;
buf = malloc(buflen);
if (buf == NULL) {
saslc__error_set_errno(ERR(ctx->sess), ERROR_NOMEM);
return -1;
}
p = in;
p += 4;
memcpy(buf, p, buflen);
*out = buf;
*outlen = buflen;
ctx->seqnum++;
return 0;
}
/**
* @brief add integrity or confidentiality layer
* @param sess session handle
* @param in input buffer
* @param inlen input buffer length
* @param out pointer to output buffer
* @param out pointer to output buffer length
* @return number of bytes consumed on success, 0 if insufficient data
* to process, -1 on failure
*/
static ssize_t
saslc__mech_digestmd5_encode(saslc_sess_t *sess, const void *in, size_t inlen,
void **out, size_t *outlen)
{
saslc__mech_digestmd5_sess_t *ms;
uint8_t *buf;
size_t buflen;
ssize_t rval;
ms = sess->mech_sess;
assert(ms->mech_sess.qop != QOP_NONE);
if (ms->mech_sess.qop == QOP_NONE)
return -1;
rval = saslc__buffer_fetch(ms->enc_ctx.buf_ctx, in, inlen, &buf, &buflen);
if (rval == -1)
return -1;
if (buflen == 0) {
*out = NULL;
*outlen = 0;
return rval;
}
if (encode_buffer(&ms->enc_ctx, buf, buflen, out, outlen) == -1)
return -1;
return rval;
}
/**
* @brief remove integrity or confidentiality layer
* @param sess session handle
* @param in input buffer
* @param inlen input buffer length
* @param out pointer to output buffer
* @param out pointer to output buffer length
* @return number of bytes consumed on success, 0 if insufficient data
* to process, -1 on failure
*
* integrity (auth-int):
* len, HMAC(ki, {SeqNum, msg})[0..9], x0001, SeqNum
*
* confidentiality (auth-conf):
* len, CIPHER(Kc, {msg, pag, HMAC(ki, {SeqNum, msg})[0..9]}), x0001, SeqNum
*/
static ssize_t
saslc__mech_digestmd5_decode(saslc_sess_t *sess, const void *in, size_t inlen,
void **out, size_t *outlen)
{
saslc__mech_digestmd5_sess_t *ms;
uint8_t *buf;
size_t buflen;
ssize_t rval;
ms = sess->mech_sess;
assert(ms->mech_sess.qop != QOP_NONE);
if (ms->mech_sess.qop == QOP_NONE)
return -1;
rval = saslc__buffer32_fetch(ms->dec_ctx.buf_ctx, in, inlen, &buf, &buflen);
if (rval == -1)
return -1;
if (buflen == 0) {
*out = NULL;
*outlen = 0;
return rval;
}
if (decode_buffer(&ms->dec_ctx, buf, buflen, out, outlen) == -1)
return -1;
return rval;
}
/************************************************************************
* XXX: Share with mech_gssapi.c? They are almost identical.
*/
/**
* @brief choose the best qop based on what was provided by the
* challenge and a possible user mask.
* @param sess the session context
* @param qop_flags the qop flags parsed from the challenge string
* @return the selected saslc__mech_sess_qop_t or -1 if no match
*/
static int
choose_qop(saslc_sess_t *sess, uint32_t qop_flags)
{
list_t *list;
const char *user_qop;
if (qop_flags == 0) /* no qop spec in challenge (it's optional) */
return QOP_NONE;
qop_flags &= DEFAULT_QOP_MASK;
user_qop = saslc_sess_getprop(sess, SASLC_DIGESTMD5_QOPMASK);
if (user_qop != NULL) {
if (saslc__list_parse(&list, user_qop) == -1) {
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
return -1;
}
qop_flags &= saslc__mech_qop_list_flags(list);
saslc__list_free(list);
}
/*
* Select the most secure supported qop.
*/
if ((qop_flags & F_QOP_CONF) != 0)
return QOP_CONF;
if ((qop_flags & F_QOP_INT) != 0)
return QOP_INT;
if ((qop_flags & F_QOP_NONE) != 0)
return QOP_NONE;
saslc__error_set(ERR(sess), ERROR_MECH,
"cannot choose an acceptable qop");
return -1;
}
/************************************************************************/
/**
* @brief choose the best cipher based on what was provided by the
* challenge and a possible user mask.
* @param sess the session context
* @param cipher_flags the cipher flags parsed from the challenge
* string
* @return the selected cipher_t
*/
static int
choose_cipher(saslc_sess_t *sess, unsigned int cipher_flags)
{
list_t *list;
unsigned int cipher_mask;
const char *user_cipher;
if (cipher_flags == 0) {
saslc__error_set(ERR(sess), ERROR_MECH,
"no cipher spec in challenge");
return -1;
}
cipher_mask = DEFAULT_CIPHER_MASK;
user_cipher = saslc_sess_getprop(sess, SASLC_DIGESTMD5_CIPHERMASK);
if (user_cipher != NULL) {
if (saslc__list_parse(&list, user_cipher) == -1) {
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
return -1;
}
cipher_mask = cipher_list_flags(list);
saslc__list_free(list);
}
cipher_flags &= cipher_mask;
/*
* Select the most secure cipher supported.
* XXX: Is the order here right?
*/
if ((cipher_flags & F_CIPHER_AES) != 0)
return CIPHER_AES;
if ((cipher_flags & F_CIPHER_3DES) != 0)
return CIPHER_3DES;
if ((cipher_flags & F_CIPHER_DES) != 0)
return CIPHER_DES;
if ((cipher_flags & F_CIPHER_RC4) != 0)
return CIPHER_RC4;
if ((cipher_flags & F_CIPHER_RC4_56) != 0)
return CIPHER_RC4_56;
if ((cipher_flags & F_CIPHER_RC4_40) != 0)
return CIPHER_RC4_40;
saslc__error_set(ERR(sess), ERROR_MECH,
"qop \"auth-conf\" requires a cipher");
return -1;
}
/**
* @brief get the challenge_t value corresponding to a challenge key
* string.
* @param key challenge key string
* @return the challenge_t value including CHALLENGE_IGNORE (-1) if
* the key is not recognized
*/
static challenge_t
get_challenge_t(const char *key)
{
static const struct {
const char *key;
challenge_t value;
} challenge_keys[] = {
{ "realm", CHALLENGE_REALM },
{ "nonce", CHALLENGE_NONCE },
{ "qop", CHALLENGE_QOP },
{ "stale", CHALLENGE_STALE },
{ "maxbuf", CHALLENGE_MAXBUF },
{ "charset", CHALLENGE_CHARSET },
{ "algorithm", CHALLENGE_ALGORITHM },
{ "cipher", CHALLENGE_CIPHER }
};
size_t i;
for (i = 0; i < __arraycount(challenge_keys); i++) {
if (strcasecmp(key, challenge_keys[i].key) == 0)
return challenge_keys[i].value;
}
return CHALLENGE_IGNORE;
}
/**
* @brief parses challenge and store result in mech_sess.
* @param mech_sess session where parsed data will be stored
* @param challenge challenge
* @return 0 on success, -1 on failure.
*/
static int
saslc__mech_digestmd5_parse_challenge(saslc_sess_t *sess, const char *challenge)
{
saslc__mech_digestmd5_sess_t *ms;
list_t *list, *n;
list_t *tmp_list;
cdata_t *cdata;
size_t maxbuf;
uint32_t tmp_flags;
int rv;
/******************************************************************/
/* digest-challenge = */
/* 1#( realm | nonce | qop-options | stale | server_maxbuf | */
/* charset | algorithm | cipher-opts | auth-param ) */
/******************************************************************/
saslc__msg_dbg("challenge: '%s'\n", challenge);
ms = sess->mech_sess;
cdata = &ms->cdata;
rv = -1;
memset(cdata, 0, sizeof(*cdata));
if (saslc__list_parse(&list, challenge) == -1) {
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
return -1;
}
saslc__list_log(list, "parse list:\n");
for (n = list; n != NULL; n = n->next) {
char *key;
char *val;
/* Split string into key and val */
key = n->value;
val = strchr(key, '=');
if (val == NULL)
goto no_mem;
*val = '\0';
val = strip_quotes(val + 1);
saslc__msg_dbg("key='%s' val='%s'\n", key, val);
switch (get_challenge_t(key)) {
case CHALLENGE_REALM:
/**************************************************/
/* realm = "realm" "=" <"> realm-value <"> */
/* realm-value = qdstr-val */
/* */
/* This directive is optional; if not present, */
/* the client SHOULD solicit it from the user or */
/* be able to compute a default; a plausible */
/* default might be the realm supplied by the */
/* user when they logged in to the client system. */
/* Multiple realm directives are allowed, in */
/* which case the user or client must choose one */
/* as the realm for which to supply to username */
/* and password. */
/**************************************************/
if (saslc__list_append(&cdata->realm, val) == -1)
goto no_mem;
break;
case CHALLENGE_NONCE:
/**************************************************/
/* nonce = "nonce" "=" <"> nonce-value <"> */
/* nonce-value = *qdtext */
/* */
/* This directive is required and MUST appear */
/* exactly once; if not present, or if multiple */
/* instances are present, the client should abort */
/* the authentication exchange. */
/**************************************************/
if (cdata->nonce != NULL) {
saslc__error_set(ERR(sess), ERROR_MECH,
"multiple nonce in challenge");
goto out;
}
cdata->nonce = strdup(val);
if (cdata->nonce == NULL)
goto no_mem;
break;
case CHALLENGE_QOP:
/**************************************************/
/* qop-options = "qop" "=" <"> qop-list <"> */
/* qop-list = 1#qop-value */
/* qop-value = "auth" | "auth-int" | */
/* "auth-conf" | token */
/* */
/* This directive is optional; if not present it */
/* defaults to "auth". The client MUST ignore */
/* unrecognized options; if the client recognizes */
/* no option, it should abort the authentication */
/* exchange. */
/**************************************************/
if (saslc__list_parse(&tmp_list, val) == -1)
goto no_mem;
saslc__list_log(tmp_list, "qop list:\n");
tmp_flags = saslc__mech_qop_list_flags(tmp_list);
saslc__list_free(tmp_list);
if (tmp_flags == 0) {
saslc__error_set(ERR(sess), ERROR_MECH,
"qop required in challenge");
goto out;
}
cdata->qop_flags |= tmp_flags;
break;
case CHALLENGE_STALE:
/**************************************************/
/* stale = "stale" "=" "true" */
/* */
/* This directive may appear at most once; if */
/* multiple instances are present, the client */
/* should abort the authentication exchange. */
/**************************************************/
if (cdata->stale) {
saslc__error_set(ERR(sess), ERROR_MECH,
"multiple stale in challenge");
goto out;
}
if (strcasecmp(val, "true") != 0) {
saslc__error_set(ERR(sess), ERROR_MECH,
"stale must be true");
goto out;
}
cdata->stale = true;
break;
case CHALLENGE_MAXBUF:
/**************************************************/
/* maxbuf-value = 1*DIGIT */
/* */
/* The value MUST be bigger than 16 and smaller */
/* or equal to 16777215 (i.e. 2**24-1). If this */
/* directive is missing, the default value is */
/* 65536. This directive may appear at most once; */
/* if multiple instances are present, the client */
/* MUST abort the authentication exchange. */
/**************************************************/
if (cdata->maxbuf != 0) {
saslc__error_set(ERR(sess), ERROR_MECH,
"multiple maxbuf in challenge");
goto out;
}
maxbuf = (size_t)strtoul(val, NULL, 10);
if (INVALID_MAXBUF(maxbuf)) {
saslc__error_set(ERR(sess), ERROR_MECH,
"invalid maxbuf in challenge");
goto out;
}
cdata->maxbuf = maxbuf;
break;
case CHALLENGE_CHARSET:
/**************************************************/
/* charset = "charset" "=" "utf-8" */
/* */
/* This directive may appear at most once; if */
/* multiple instances are present, the client */
/* should abort the authentication exchange. */
/**************************************************/
if (cdata->utf8) {
saslc__error_set(ERR(sess), ERROR_MECH,
"multiple charset in challenge");
goto out;
}
if (strcasecmp(val, "utf-8") != 0) {
saslc__error_set(ERR(sess), ERROR_MECH,
"charset != \"utf-8\" in challenge");
goto out;
}
cdata->utf8 = true;
break;
case CHALLENGE_ALGORITHM:
/**************************************************/
/* algorithm = "algorithm" "=" "md5-sess" */
/* */
/* This directive is required and MUST appear */
/* exactly once; if not present, or if multiple */
/* instances are present, the client should abort */
/* the authentication exchange. */
/**************************************************/
if (cdata->algorithm) {
saslc__error_set(ERR(sess), ERROR_MECH,
"multiple algorithm in challenge");
goto out;
}
if (strcasecmp(val, "md5-sess") != 0) {
saslc__error_set(ERR(sess), ERROR_MECH,
"algorithm != \"md5-sess\" in challenge");
goto out;
}
cdata->algorithm = true;
break;
case CHALLENGE_CIPHER:
/**************************************************/
/* cipher-opts = "cipher" "=" <"> 1#cipher-val <">*/
/* cipher-val = "3des" | "des" | "rc4-40" | */
/* "rc4" |"rc4-56" | "aes" | */
/* token */
/* */
/* This directive must be present exactly once if */
/* "auth-conf" is offered in the "qop-options" */
/* directive, in which case the "3des" cipher is */
/* mandatory-to-implement. The client MUST ignore */
/* unrecognized options; if the client recognizes */
/* no option, it should abort the authentication */
/* exchange. */
/**************************************************/
if (saslc__list_parse(&tmp_list, val) == -1)
goto no_mem;
saslc__list_log(tmp_list, "cipher list:\n");
tmp_flags = cipher_list_flags(tmp_list);
saslc__list_free(tmp_list);
if (tmp_flags == 0) {
saslc__error_set(ERR(sess), ERROR_MECH,
"unknown cipher");
goto out;
}
cdata->cipher_flags |= tmp_flags;
break;
case CHALLENGE_IGNORE:
/**************************************************/
/* auth-param = token "=" ( token | */
/* quoted-string ) */
/* */
/* The client MUST ignore any unrecognized */
/* directives. */
/**************************************************/
break;
}
}
/*
* make sure realms are in iso8859-1
*/
if (stringprep_realms(cdata->utf8, cdata->realm) == -1) {
saslc__error_set(ERR(sess), ERROR_MECH,
"unable to convert realms in challenge from "
"\"utf-8\" to iso8859-1");
goto out;
}
/*
* test for required options
*/
if (cdata->nonce == NULL) {
saslc__error_set(ERR(sess), ERROR_MECH,
"nonce required in challenge");
goto out;
}
if (!cdata->algorithm) {
saslc__error_set(ERR(sess), ERROR_MECH,
"algorithm required in challenge");
goto out;
}
/*
* set the default maxbuf value if it was missing from the
* challenge.
*/
if (cdata->maxbuf == 0)
cdata->maxbuf = DEFAULT_MAXBUF;
saslc__msg_dbg("qop_flags=0x%04x\n", cdata->qop_flags);
saslc__msg_dbg("cipher_flags=0x%04x\n", cdata->cipher_flags);
rv = 0;
out:
saslc__list_free(list);
return rv;
no_mem:
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
goto out;
}
/**
* @brief creates digestmd5 mechanism session.
* Function initializes also default options for the session.
* @param sess sasl session
* @return 0 on success, -1 on failure.
*/
static int
saslc__mech_digestmd5_create(saslc_sess_t *sess)
{
saslc__mech_digestmd5_sess_t *c;
if ((c = calloc(1, sizeof(*c))) == NULL) {
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
return -1;
}
c->rdata.nonce_cnt = 1;
sess->mech_sess = c;
return 0;
}
static void
free_cdata(cdata_t *cdata)
{
free(cdata->nonce);
saslc__list_free(cdata->realm);
}
static void
free_rdata(rdata_t *rdata)
{
free(rdata->authcid);
free(rdata->authzid);
free(rdata->cnonce);
free(rdata->digesturi);
if (rdata->passwd != NULL) {
memset(rdata->passwd, 0, strlen(rdata->passwd));
free(rdata->passwd);
}
free(rdata->realm);
}
/**
* @brief destroys digestmd5 mechanism session.
* Function also is freeing assigned resources to the session.
* @param sess sasl session
* @return Functions always returns 0.
*/
static int
saslc__mech_digestmd5_destroy(saslc_sess_t *sess)
{
saslc__mech_digestmd5_sess_t *ms;
ms = sess->mech_sess;
free_cdata(&ms->cdata);
free_rdata(&ms->rdata);
saslc__buffer32_destroy(ms->dec_ctx.buf_ctx);
saslc__buffer_destroy(ms->enc_ctx.buf_ctx);
cipher_context_destroy(ms->dec_ctx.cph_ctx);
cipher_context_destroy(ms->enc_ctx.cph_ctx);
free(sess->mech_sess);
sess->mech_sess = NULL;
return 0;
}
/**
* @brief collect the response data necessary to build the reply.
* @param sess the session context
* @return 0 on success, -1 on failure
*
* NOTE:
* The input info is from the challenge (previously saved in cdata of
* saslc__mech_digestmd5_sess_t) or from the property dictionaries.
*
* The output info is saved in (mostly) in rdata of the
* saslc__mech_digestmd5_sess_t structure. The qop is special in that
* it is exposed to the saslc__mech_sess_t layer.
*/
static int
saslc__mech_digestmd5_response_data(saslc_sess_t *sess)
{
saslc__mech_digestmd5_sess_t *ms;
cdata_t *cdata;
rdata_t *rdata;
const char *authcid;
const char *authzid;
const char *hostname;
const char *maxbuf;
const char *passwd;
int rv;
ms = sess->mech_sess;
cdata = &ms->cdata;
rdata = &ms->rdata;
if ((rv = choose_qop(sess, cdata->qop_flags)) == -1)
return -1; /* error message already set */
ms->mech_sess.qop = rv;
if (ms->mech_sess.qop == QOP_CONF) {
if ((rv = choose_cipher(sess, cdata->cipher_flags)) == -1)
return -1; /* error message already set */
rdata->cipher = rv;
}
hostname = saslc_sess_getprop(sess, SASLC_DIGESTMD5_HOSTNAME);
if (hostname == NULL) {
saslc__error_set(ERR(sess), ERROR_MECH,
"hostname is required for authentication");
return -1;
}
rdata->realm = choose_realm(sess, hostname, cdata->realm);
if (rdata->realm == NULL)
return -1; /* error message already set */
rdata->digesturi = saslc__mech_digestmd5_digesturi(sess, hostname);
if (rdata->digesturi == NULL)
return -1; /* error message already set */
authcid = saslc_sess_getprop(sess, SASLC_DIGESTMD5_AUTHCID);
if (authcid == NULL) {
saslc__error_set(ERR(sess), ERROR_MECH,
"authcid is required for an authentication");
return -1;
}
rdata->authcid = strdup(authcid);
if (rdata->authcid == NULL)
goto no_mem;
authzid = saslc_sess_getprop(sess, SASLC_DIGESTMD5_AUTHZID);
if (authzid != NULL) {
rdata->authzid = strdup(authzid);
if (rdata->authzid == NULL)
goto no_mem;
}
passwd = saslc_sess_getprop(sess, SASLC_DIGESTMD5_PASSWD);
if (passwd == NULL) {
saslc__error_set(ERR(sess), ERROR_MECH,
"password is required for an authentication");
return -1;
}
rdata->passwd = strdup(passwd);
if (rdata->passwd == NULL)
goto no_mem;
rdata->cnonce = saslc__mech_digestmd5_nonce(NONCE_LEN);
if (rdata->cnonce == NULL) {
saslc__error_set(ERR(sess), ERROR_MECH,
"failed to create cnonce");
return -1;
}
#ifdef SASLC_DIGESTMD5_CNONCE /* XXX: for debugging! */
{
const char *cnonce;
cnonce = saslc_sess_getprop(sess, SASLC_DIGESTMD5_CNONCE);
if (cnonce != NULL) {
rdata->cnonce = strdup(cnonce);
if (rdata->cnonce == NULL)
goto no_mem;
}
}
#endif
if (ms->mech_sess.qop != QOP_NONE) {
maxbuf = saslc_sess_getprop(sess, SASLC_DIGESTMD5_MAXBUF);
if (maxbuf != NULL)
rdata->maxbuf = (size_t)strtoul(maxbuf, NULL, 10);
if (rdata->maxbuf == 0)
rdata->maxbuf = cdata->maxbuf;
if (INVALID_MAXBUF(rdata->maxbuf)) {
saslc__error_set(ERR(sess), ERROR_MECH,
"maxbuf out of range");
return -1;
}
}
return 0;
no_mem:
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
return -1;
}
/**
* @brief compute the maximum payload that can go into an integrity or
* confidentiality packet.
* @param maxbuf the server's maxbuf size.
* @param blksize the ciphers block size. 0 or 1 if there is no blocking.
* @return the payload size
*
* The packet (not including the leading uint32_t packet length field)
* has this structure:
*
* struct {
* uint8_t payload[]; // packet payload
* uint8_t padding[]; // padding to block size
* uint8_t hmac_0_9[10]; // the first 10 bytes of the hmac
* uint8_t version[2]; // version number (1) in BE format
* uint8_t seqnum[4]; // sequence number in BE format
* } __packed
*
* NOTE: if the block size is > 1, then padding is required to make
* the {payload[], padding[], and hmac_0_9[]} a multiple of the block
* size. Furthermore there must be at least one byte of padding! The
* padding bytes are all set to the padding length and one byte of
* padding is necessary to recover the padding length.
*/
static size_t
maxpayload(size_t maxbuf, size_t blksize)
{
size_t l;
if (blksize <= 1) { /* no padding used */
if (maxbuf <= sizeof(md5hash_t))
return 0;
return maxbuf - sizeof(md5hash_t);
}
if (maxbuf < 2 * blksize + 6)
return 0;
l = rounddown(maxbuf - 6, blksize);
if (l <= 10 + 1) /* we need at least one byte of padding */
return 0;
return l - 10 - 1;
}
/**
* @brief initialize the encode and decode coder contexts for the session
* @param sess the current session
* @return 0 on success, -1 on failure.
*/
static int
init_coder_context(saslc_sess_t *sess)
{
saslc__mech_digestmd5_sess_t *ms;
size_t blksize;
#ifdef SASLC_DIGESTMD5_SELFTEST
int selftest; /* XXX: allow for testing against ourselves */
#endif
ms = sess->mech_sess;
#ifdef SASLC_DIGESTMD5_SELFTEST
selftest = saslc_sess_getprop(sess, SASLC_DIGESTMD5_SELFTEST) != NULL;
#endif
blksize = 0;
switch (ms->mech_sess.qop) {
case QOP_NONE:
return 0;
case QOP_INT:
#ifdef SASLC_DIGESTMD5_SELFTEST
ms->dec_ctx.key = selftest ? ms->keys.kic : ms->keys.kis;
#else
ms->dec_ctx.key = ms->keys.kis;
#endif
ms->enc_ctx.key = ms->keys.kic;
ms->dec_ctx.cph_ctx = NULL;
ms->enc_ctx.cph_ctx = NULL;
break;
case QOP_CONF:
#ifdef SASLC_DIGESTMD5_SELFTEST
ms->dec_ctx.key = selftest ? ms->keys.kcc : ms->keys.kcs;
#else
ms->dec_ctx.key = ms->keys.kcs;
#endif
ms->enc_ctx.key = ms->keys.kcc;
ms->dec_ctx.cph_ctx = cipher_context_create(sess,
ms->rdata.cipher, 0, ms->dec_ctx.key);
if (ms->dec_ctx.cph_ctx == NULL)
return -1;
ms->enc_ctx.cph_ctx = cipher_context_create(sess,
ms->rdata.cipher, 1, ms->enc_ctx.key);
if (ms->enc_ctx.cph_ctx == NULL)
return -1;
blksize = ms->enc_ctx.cph_ctx->blksize;
break;
}
ms->dec_ctx.sess = sess;
ms->enc_ctx.sess = sess;
ms->dec_ctx.buf_ctx = saslc__buffer32_create(sess, ms->rdata.maxbuf);
if (ms->cdata.maxbuf < 2 * blksize + 6) {
saslc__error_set(ERR(sess), ERROR_MECH,
"server buffer too small for packet");
return -1;
}
ms->enc_ctx.buf_ctx = saslc__buffer_create(sess,
maxpayload(ms->cdata.maxbuf, blksize));
if (ms->dec_ctx.buf_ctx == NULL || ms->enc_ctx.buf_ctx == NULL) {
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
return -1;
}
return 0;
}
/**
* @brief construct the reply string.
* @param sess session context
* @param response string
* @return reply string or NULL on failure.
*/
static char *
saslc__mech_digestmd5_reply(saslc_sess_t *sess, char *response)
{
saslc__mech_digestmd5_sess_t *ms;
char *out;
char *cipher, *maxbuf, *realm;
ms = sess->mech_sess;
out = NULL;
cipher = __UNCONST("");
maxbuf = __UNCONST("");
realm = __UNCONST("");
switch (ms->mech_sess.qop) {
case QOP_CONF:
if (asprintf(&cipher, "cipher=\"%s\",",
cipher_name(ms->rdata.cipher)) == -1) {
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
goto done;
}
/*FALLTHROUGH*/
case QOP_INT:
if (asprintf(&maxbuf, "maxbuf=%zu,", ms->rdata.maxbuf) == -1) {
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
goto done;
}
break;
case QOP_NONE:
break;
default:
assert(/*CONSTCOND*/0);
return NULL;
}
if (ms->rdata.realm != NULL &&
asprintf(&realm, "realm=\"%s\",", ms->rdata.realm) == -1) {
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
goto done;
}
if (asprintf(&out,
"username=\"%s\","
"%s" /* realm= */
"nonce=\"%s\","
"cnonce=\"%s\","
"nc=%08d,"
"qop=%s,"
"%s" /* cipher= */
"%s" /* maxbuf= */
"digest-uri=\"%s\","
"response=%s",
ms->rdata.authcid,
realm,
ms->cdata.nonce,
ms->rdata.cnonce,
ms->rdata.nonce_cnt,
saslc__mech_qop_name(ms->mech_sess.qop),
cipher,
maxbuf,
ms->rdata.digesturi,
response
) == -1) {
saslc__error_set_errno(ERR(sess), ERROR_NOMEM);
out = NULL;
}
done:
if (realm[0] != '\0')
free(realm);
if (maxbuf[0] != '\0')
free(maxbuf);
if (cipher[0] != '\0')
free(cipher);
return out;
}
/**
* @brief do one step of the sasl authentication
* @param sess sasl session
* @param in input data
* @param inlen input data length
* @param out place to store output data
* @param outlen output data length
* @return MECH_OK - authentication successful,
* MECH_STEP - more steps are needed,
* MECH_ERROR - error
*/
static int
saslc__mech_digestmd5_cont(saslc_sess_t *sess, const void *in, size_t inlen,
void **out, size_t *outlen)
{
saslc__mech_digestmd5_sess_t *ms;
char *response;
const char *p;
ms = sess->mech_sess;
switch(ms->mech_sess.step) {
case 0:
/* in case we are called before getting data from server */
if (inlen == 0) {
*out = NULL;
*outlen = 0;
return MECH_STEP;
}
/* if input data was provided, then doing the first step */
ms->mech_sess.step++;
/*FALLTHROUGH*/
case 1:
if (saslc__mech_digestmd5_parse_challenge(sess, in) == -1)
return MECH_ERROR;
if (saslc__mech_digestmd5_response_data(sess) == -1)
return MECH_ERROR;
if ((response = saslc__mech_digestmd5_response(ms,
"AUTHENTICATE")) == NULL) {
saslc__error_set(ERR(sess), ERROR_MECH,
"unable to construct response");
return MECH_ERROR;
}
*out = saslc__mech_digestmd5_reply(sess, response);
free(response);
if (*out == NULL)
return MECH_ERROR;
*outlen = strlen(*out);
return MECH_STEP;
case 2:
if ((response = saslc__mech_digestmd5_response(ms, ""))
== NULL) {
saslc__error_set(ERR(sess), ERROR_MECH,
"unable to construct rspauth");
return MECH_ERROR;
}
p = in;
if (strncmp(p, "rspauth=", 8) != 0 ||
strcmp(response, p + 8) != 0) {
saslc__msg_dbg("rspauth='%s'\n", response);
saslc__error_set(ERR(sess), ERROR_MECH,
"failed to validate rspauth response");
free(response);
return MECH_ERROR;
}
free(response);
if (init_coder_context(sess) == -1)
return MECH_ERROR;
*out = NULL;
*outlen = 0;
return MECH_OK;
default:
assert(/*CONSTCOND*/0); /* impossible */
return MECH_ERROR;
}
}
/* mechanism definition */
const saslc__mech_t saslc__mech_digestmd5 = {
.name = "DIGEST-MD5",
.flags = FLAG_MUTUAL | FLAG_DICTIONARY,
.create = saslc__mech_digestmd5_create,
.cont = saslc__mech_digestmd5_cont,
.encode = saslc__mech_digestmd5_encode,
.decode = saslc__mech_digestmd5_decode,
.destroy = saslc__mech_digestmd5_destroy
};