- crypto/external/bsd/heimdal - crypto/external/bsd/libsaslc - crypto/external/bsd/netpgp - crypto/external/bsd/openssl Change-Id: I91dbf05f33e637edf5b9bb408d5baddd7ba8cf75
2340 lines
67 KiB
C
2340 lines
67 KiB
C
/* $NetBSD: mech_digestmd5.c,v 1.11 2013/06/28 15:04:35 joerg 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.11 2013/06/28 15:04:35 joerg 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;
|
|
|
|
/*****************************************************************/
|
|
/* 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:
|
|
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
|
|
};
|