/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright 2005 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ #pragma ident "%Z%%M% %I% %E% SMI" /* * Copyright (c) 1998-1999 Innosoft International, Inc. All Rights Reserved. * * Copyright (c) 1996-1997 Critical Angle Inc. All Rights Reserved. */ #include #include #include #include #include #include #include #include #include "lber.h" #include "ldap.h" #include "ldap-int.h" /* * DIGEST-MD5 SASL Mechanism */ /* use this instead of "const unsigned char" to eliminate compiler warnings */ typedef /* const */ unsigned char CONST_UCHAR; /* size of a digest result */ #define DIGEST_SIZE 16 /* size of a digest hex string */ #define DIGEST_HEX_SIZE (DIGEST_SIZE * 2 + 1) /* * extra bytes which a client response needs in addition to size of * server challenge */ #define DIGEST_CLIENT_EXTRA (DIGEST_HEX_SIZE + 128) /* erase a digest_attrs_t structure */ #define digest_clear(attrs) memset((attrs), 0, sizeof (digest_attrs_t)) /* * broken-out digest attributes (with quotes removed) * probably not NUL terminated. */ typedef struct { const char *realm, *nonce, *cnonce, *qop, *user, *resp, *dom; const char *max, *stale, *ncount, *uri, *charset; int rlen, nlen, clen, qlen, ulen, resplen, dlen; int mlen, slen, nclen, urilen, charsetlen; char ncbuf[9]; } digest_attrs_t; static const char hextab[] = "0123456789abcdef"; static CONST_UCHAR colon[] = ":"; /* * Make a nonce (NUL terminated) * buf -- buffer for result * maxlen -- max length of result * returns final length or -1 on error */ static int digest_nonce(char *buf, int maxlen) { /* * it shouldn't matter too much if two threads step on this counter * at the same time, but mutexing it wouldn't hurt */ static int counter; char *dst; int len; struct chal_info { time_t mytime; unsigned char digest[16]; } cinfo; MD5_CTX ctx; long r; static int set_rand = 0; unsigned char *p; int j; int fd; int got_random; /* initialize challenge */ if (maxlen < 2 * sizeof (cinfo)) return (-1); dst = buf; /* get a timestamp */ time(&cinfo.mytime); /* get some randomness */ got_random = 0; fd = open("/dev/urandom", O_RDONLY); if (fd != -1) { got_random = (read(fd, &r, sizeof (r)) == sizeof (r)); close(fd); } if (!got_random) { if (set_rand == 0) { struct timeval tv; r = cinfo.mytime - (getpid() *65536) + (random() & 0xffff); gettimeofday(&tv, NULL); r ^= tv.tv_usec; r ^= gethostid(); srandom(r); set_rand = 1; } r = random(); } MD5Init(&ctx); MD5Update(&ctx, (unsigned char *) &r, sizeof (r)); MD5Update(&ctx, (unsigned char *) &counter, sizeof (counter)); ++counter; MD5Final(cinfo.digest, &ctx); /* compute hex for result */ for (j = 0, p = (unsigned char *)&cinfo; j < sizeof (cinfo); ++j) { dst[j * 2] = hextab[p[j] >> 4]; dst[j * 2 + 1] = hextab[p[j] & 0xf]; } /* take the entire time_t, plus at least 6 bytes of MD5 output */ len = ((sizeof (time_t) + 6) * 2); dst += len; maxlen -= len; *dst = '\0'; return (dst - buf); } /* * if the string is entirely in the 8859-1 subset of UTF-8, then translate * to 8859-1 prior to MD5 */ static void MD5_UTF8_8859_1(MD5_CTX *ctx, CONST_UCHAR *base, int len) { CONST_UCHAR *scan, *end; unsigned char cbuf; end = base + len; for (scan = base; scan < end; ++scan) { if (*scan > 0xC3) break; /* abort if outside 8859-1 */ if (*scan >= 0xC0 && *scan <= 0xC3) { if (++scan == end || *scan < 0x80 || *scan > 0xBF) break; } } /* if we found a character outside 8859-1, don't alter string */ if (scan < end) { MD5Update(ctx, base, len); return; } /* convert to 8859-1 prior to applying hash */ do { for (scan = base; scan < end && *scan < 0xC0; ++scan) ; if (scan != base) MD5Update(ctx, base, scan - base); if (scan + 1 >= end) break; cbuf = ((scan[0] & 0x3) << 6) | (scan[1] & 0x3f); MD5Update(ctx, &cbuf, 1); base = scan + 2; } while (base < end); } /* * Compute MD5( "::" ) * if use8859_1 is non-zero, then user/realm is 8859-1 charset * if supplied lengths are 0, strlen() is used * places result in hash_pass (of size DIGEST_SIZE) and returns it. */ static unsigned char * digest_hash_pass(const char *user, int ulen, const char *realm, int rlen, const char *pass, int passlen, int use8859_1, unsigned char *hash_pass) { MD5_CTX ctx; MD5Init(&ctx); if (ulen == 0) ulen = strlen(user); if (use8859_1) { MD5Update(&ctx, (CONST_UCHAR *) user, ulen); } else { MD5_UTF8_8859_1(&ctx, (CONST_UCHAR *) user, ulen); } MD5Update(&ctx, colon, 1); if (rlen == 0) rlen = strlen(realm); if (use8859_1) { MD5Update(&ctx, (CONST_UCHAR *) realm, rlen); } else { MD5_UTF8_8859_1(&ctx, (CONST_UCHAR *) realm, rlen); } MD5Update(&ctx, colon, 1); if (passlen == 0) passlen = strlen(pass); MD5Update(&ctx, (CONST_UCHAR *) pass, passlen); MD5Final(hash_pass, &ctx); return (hash_pass); } /* * Compute MD5("::") * places result in hash_a1 and returns hash_a1 * note that hash_pass and hash_a1 may be the same */ static unsigned char * digest_hash_a1(const digest_attrs_t *attr, CONST_UCHAR *hash_pass, unsigned char *hash_a1) { MD5_CTX ctx; MD5Init(&ctx); MD5Update(&ctx, hash_pass, DIGEST_SIZE); MD5Update(&ctx, colon, 1); MD5Update(&ctx, (CONST_UCHAR *) attr->nonce, attr->nlen); MD5Update(&ctx, colon, 1); MD5Update(&ctx, (CONST_UCHAR *) attr->cnonce, attr->clen); MD5Final(hash_a1, &ctx); return (hash_a1); } /* * calculate hash response for digest auth. * outresp must be buffer of at least DIGEST_HEX_SIZE * outresp and hex_int may be the same * method may be NULL if mlen is 0 */ static void digest_calc_resp(const digest_attrs_t *attr, CONST_UCHAR *hash_a1, const char *method, int mlen, CONST_UCHAR *hex_int, char *outresp) { static CONST_UCHAR defncount[] = ":00000001:"; static CONST_UCHAR empty_hex_int[] = "00000000000000000000000000000000"; MD5_CTX ctx; unsigned char resp[DIGEST_SIZE]; unsigned char *hex_a1 = (unsigned char *) outresp; unsigned char *hex_a2 = (unsigned char *) outresp; unsigned j; /* compute hash of A2 and put in resp */ MD5Init(&ctx); if (mlen == 0 && method != NULL) mlen = strlen(method); if (mlen) MD5Update(&ctx, (CONST_UCHAR *) method, mlen); MD5Update(&ctx, colon, 1); if (attr->urilen != 0) { MD5Update(&ctx, (CONST_UCHAR *) attr->uri, attr->urilen); } if (attr->qlen != 4 || strncasecmp(attr->qop, "auth", 4) != 0) { MD5Update(&ctx, colon, 1); if (hex_int == NULL) hex_int = empty_hex_int; MD5Update(&ctx, hex_int, DIGEST_SIZE * 2); } MD5Final(resp, &ctx); /* compute hex_a1 from hash_a1 */ for (j = 0; j < DIGEST_SIZE; ++j) { hex_a1[j * 2] = hextab[hash_a1[j] >> 4]; hex_a1[j * 2 + 1] = hextab[hash_a1[j] & 0xf]; } /* compute response */ MD5Init(&ctx); MD5Update(&ctx, hex_a1, DIGEST_SIZE * 2); MD5Update(&ctx, colon, 1); MD5Update(&ctx, (CONST_UCHAR *) attr->nonce, attr->nlen); if (attr->ncount != NULL) { MD5Update(&ctx, colon, 1); MD5Update(&ctx, (CONST_UCHAR *) attr->ncount, attr->nclen); MD5Update(&ctx, colon, 1); } else { MD5Update(&ctx, defncount, sizeof (defncount) - 1); } MD5Update(&ctx, (CONST_UCHAR *) attr->cnonce, attr->clen); MD5Update(&ctx, colon, 1); MD5Update(&ctx, (CONST_UCHAR *) attr->qop, attr->qlen); MD5Update(&ctx, colon, 1); /* compute hex_a2 from hash_a2 */ for (j = 0; j < DIGEST_SIZE; ++j) { hex_a2[j * 2] = hextab[resp[j] >> 4]; hex_a2[j * 2 + 1] = hextab[resp[j] & 0xf]; } MD5Update(&ctx, hex_a2, DIGEST_SIZE * 2); MD5Final(resp, &ctx); /* generate hex output */ for (j = 0; j < DIGEST_SIZE; ++j) { outresp[j * 2] = hextab[resp[j] >> 4]; outresp[j * 2 + 1] = hextab[resp[j] & 0xf]; } outresp[DIGEST_SIZE * 2] = '\0'; memset(resp, 0, sizeof (resp)); } /* * generate the client response from attributes * either one of hash_pass and hash_a1 may be NULL * hash_a1 is used on re-authentication and takes precedence over hash_pass */ static int digest_client_resp(const char *method, int mlen, CONST_UCHAR *hash_pass, CONST_UCHAR *hash_a1, digest_attrs_t *attr, /* in/out attributes */ char *outbuf, int maxout, int *plen) { #define prefixsize (sizeof (prefix) - 4 * 4 - 1) #define suffixsize (sizeof (rstr) + sizeof (qstr) - 1 + DIGEST_SIZE * 2) static const char prefix[] = "username=\"%.*s\",realm=\"%.*s\",nonce=\"%.*s\",nc=%.*s,cnonce=\""; static const char rstr[] = "\",response="; static const char qstr[] = ",qop=auth"; static const char chstr[] = "charset="; char *scan; int len; char hexbuf[DIGEST_HEX_SIZE]; unsigned char hashbuf[DIGEST_SIZE]; /* make sure we have mandatory attributes */ if (attr->nonce == NULL || attr->nlen == 0 || attr->realm == NULL || attr->rlen == 0 || attr->qop == NULL || attr->qlen == 0 || (attr->nclen != 0 && attr->nclen != 8)) { return (-5); } if (mlen != 0 && method == NULL) return (-7); /* initialize ncount */ if (attr->ncount == NULL) { strcpy(attr->ncbuf, "00000001"); attr->ncount = attr->ncbuf; attr->nclen = 8; } else if (attr->ncount == attr->ncbuf) { /* increment ncount */ scan = attr->ncbuf + 7; while (scan >= attr->ncbuf) { if (*scan == '9') { *scan = 'a'; break; } else if (*scan != 'f') { ++*scan; break; } *scan = '0'; --scan; } } /* sanity check length */ len = prefixsize + attr->ulen + attr->rlen + attr->nlen + attr->nclen; if (attr->charsetlen > 0) { /* includes 1 for a comma */ len += sizeof (chstr) + attr->charsetlen; } if (len + suffixsize >= maxout) return (-3); scan = outbuf; /* charset */ if (attr->charsetlen > 0 && attr->charset != NULL) { memcpy(scan, chstr, sizeof (chstr) - 1); scan += sizeof (chstr) - 1; memcpy(scan, attr->charset, attr->charsetlen); scan += attr->charsetlen; *scan++ = ','; } /* generate string up to the client nonce */ sprintf(scan, prefix, attr->ulen, attr->user, attr->rlen, attr->realm, attr->nlen, attr->nonce, attr->nclen, attr->ncount); scan = outbuf + len; /* generate client nonce */ len = digest_nonce(scan, maxout - (scan - outbuf)); if (len < 0) return (len); attr->cnonce = scan; attr->clen = len; scan += len; if (scan - outbuf + suffixsize > maxout) return (-3); /* compute response */ if (hash_a1 == NULL) { if (hash_pass == NULL) return (-7); hash_a1 = digest_hash_a1(attr, hash_pass, hashbuf); } digest_calc_resp(attr, hash_a1, method, mlen, NULL, hexbuf); /* finish it */ memcpy(scan, rstr, sizeof (rstr) - 1); scan += sizeof (rstr) - 1; memcpy(scan, hexbuf, DIGEST_SIZE * 2); attr->resp = scan; attr->resplen = DIGEST_SIZE * 2; scan += DIGEST_SIZE * 2; memcpy(scan, qstr, sizeof (qstr)); /* set final length */ if (plen != NULL) *plen = scan - outbuf + sizeof (qstr) - 1; return (0); } #define lstreqcase(conststr, val, len) ((len) == sizeof (conststr) - 1 && \ strncasecmp((conststr), (val), sizeof (conststr) - 1) == 0) /* parse a digest auth string */ static int digest_parse(const char *str, int len, digest_attrs_t *attr_out) { static const char rstr[] = "realm"; static const char nstr[] = "nonce"; static const char cstr[] = "cnonce"; static const char qstr[] = "qop"; static const char ustr[] = "username"; static const char respstr[] = "response"; static const char dstr[] = "domain"; static const char mstr[] = "maxbuf"; static const char sstr[] = "stale"; static const char ncstr[] = "nc"; static const char uristr[] = "digest-uri"; static const char charsetstr[] = "charset"; const char *scan, *attr, *val, *end; int alen, vlen; if (len == 0) len = strlen(str); scan = str; end = str + len; for (;;) { /* skip over commas */ while (scan < end && (*scan == ',' || isspace(*scan))) ++scan; /* parse attribute */ attr = scan; while (scan < end && *scan != '=') ++scan; alen = scan - attr; if (!alen || scan == end || scan + 1 == end) { return (-5); } /* parse value */ if (scan[1] == '"') { scan += 2; val = scan; while (scan < end && *scan != '"') { /* skip over "\" quoting, but don't remove it */ if (*scan == '\\') { if (scan + 1 == end) return (-5); scan += 2; } else { ++scan; } } vlen = scan - val; if (*scan != '"') return (-5); ++scan; } else { ++scan; val = scan; while (scan < end && *scan != ',') ++scan; vlen = scan - val; } if (!vlen) return (-5); /* lookup the attribute */ switch (*attr) { case 'c': case 'C': if (lstreqcase(cstr, attr, alen)) { attr_out->cnonce = val; attr_out->clen = vlen; } if (lstreqcase(charsetstr, attr, alen)) { attr_out->charset = val; attr_out->charsetlen = vlen; } break; case 'd': case 'D': if (lstreqcase(dstr, attr, alen)) { attr_out->dom = val; attr_out->dlen = vlen; } if (lstreqcase(uristr, attr, alen)) { attr_out->uri = val; attr_out->urilen = vlen; } break; case 'm': case 'M': if (lstreqcase(mstr, attr, alen)) { attr_out->max = val; attr_out->mlen = vlen; } break; case 'n': case 'N': if (lstreqcase(nstr, attr, alen)) { attr_out->nonce = val; attr_out->nlen = vlen; } if (lstreqcase(ncstr, attr, alen)) { attr_out->ncount = val; attr_out->nclen = vlen; } break; case 'q': case 'Q': if (lstreqcase(qstr, attr, alen)) { attr_out->qop = val; attr_out->qlen = vlen; } break; case 'r': case 'R': if (lstreqcase(rstr, attr, alen)) { attr_out->realm = val; attr_out->rlen = vlen; } if (lstreqcase(respstr, attr, alen)) { attr_out->resp = val; attr_out->resplen = vlen; } break; case 's': case 'S': if (lstreqcase(sstr, attr, alen)) { attr_out->stale = val; attr_out->slen = vlen; } break; case 'u': case 'U': if (lstreqcase(ustr, attr, alen)) { attr_out->user = val; attr_out->ulen = vlen; } break; } /* we should be at the end of the string or a comma */ if (scan == end) break; if (*scan != ',') return (-5); } return (0); } static int ldap_digest_md5_encode( const char *challenge, const char *username, const char *passwd, char **digest ) { unsigned char hash_pass[DIGEST_SIZE]; digest_attrs_t attrs; char *outbuf; int outlen; int ret; /* validate args */ if (challenge == NULL || username == NULL || passwd == NULL) { return (LDAP_PARAM_ERROR); } /* parse the challenge */ digest_clear(&attrs); ret = digest_parse(challenge, 0, &attrs); if (ret != 0) return (LDAP_DECODING_ERROR); /* server MUST specify support for charset=utf-8 */ if (attrs.charsetlen != 5 || strncasecmp(attrs.charset, "utf-8", 5) != 0) { LDAPDebug(LDAP_DEBUG_TRACE, "server did not specify charset=utf-8\n", 0, 0, 0); return (LDAP_NOT_SUPPORTED); } /* set up digest attributes */ attrs.user = username; attrs.ulen = strlen(attrs.user); /* allocate the output buffer */ outlen = strlen(challenge) + DIGEST_CLIENT_EXTRA + 1; outbuf = (char *)malloc(outlen); if (outbuf == NULL) return (LDAP_NO_MEMORY); /* hash the password */ digest_hash_pass(username, 0, attrs.realm, attrs.rlen, passwd, 0, 0, hash_pass), /* create the response */ ret = digest_client_resp("AUTHENTICATE", 12, hash_pass, NULL, &attrs, outbuf, outlen, &outlen); memset(hash_pass, 0, DIGEST_SIZE); if (ret != 0) { free(outbuf); return (LDAP_DECODING_ERROR); } /* null terminate the response */ *(outbuf+outlen) = '\0'; *digest = outbuf; return (LDAP_SUCCESS); } int ldap_x_sasl_digest_md5_bind_s( LDAP *ld, char *user_name, struct berval *cred, LDAPControl **serverctrls, LDAPControl **clientctrls) { struct berval *challenge = NULL; int errnum; char *digest = NULL; struct berval resp; LDAPDebug(LDAP_DEBUG_TRACE, "ldap_x_sasl_digest_md5_bind_s\n", 0, 0, 0); /* Add debug */ if (ld == NULL || user_name == NULL || cred == NULL || cred->bv_val == NULL) return (LDAP_PARAM_ERROR); if (ld->ld_version < LDAP_VERSION3) return (LDAP_PARAM_ERROR); errnum = ldap_sasl_bind_s(ld, NULL, LDAP_SASL_DIGEST_MD5, NULL, serverctrls, clientctrls, &challenge); if (errnum == LDAP_SASL_BIND_IN_PROGRESS) { if (challenge != NULL) { LDAPDebug(LDAP_DEBUG_TRACE, "SASL challenge: %s\n", challenge->bv_val, 0, 0); errnum = ldap_digest_md5_encode(challenge->bv_val, user_name, cred->bv_val, &digest); ber_bvfree(challenge); challenge = NULL; if (errnum == LDAP_SUCCESS) { resp.bv_val = digest; resp.bv_len = strlen(digest); LDAPDebug(LDAP_DEBUG_TRACE, "SASL reply: %s\n", digest, 0, 0); errnum = ldap_sasl_bind_s(ld, NULL, LDAP_SASL_DIGEST_MD5, &resp, serverctrls, clientctrls, &challenge); free(digest); } if (challenge != NULL) ber_bvfree(challenge); } else { errnum = LDAP_NO_MEMORY; /* TO DO: What val? */ } } LDAP_MUTEX_LOCK(ld, LDAP_ERR_LOCK); ld->ld_errno = errnum; LDAP_MUTEX_UNLOCK(ld, LDAP_ERR_LOCK); return (errnum); } static int sasl_digest_md5_bind_1( LDAP *ld, char *user_name, LDAPControl **serverctrls, LDAPControl **clientctrls, int *msgidp) { if (ld == NULL || user_name == NULL || msgidp == NULL) return (LDAP_PARAM_ERROR); if (ld->ld_version < LDAP_VERSION3) return (LDAP_PARAM_ERROR); return (ldap_sasl_bind(ld, NULL, LDAP_SASL_DIGEST_MD5, NULL, serverctrls, clientctrls, msgidp)); } static int sasl_digest_md5_bind_2( LDAP *ld, char *user_name, struct berval *cred, LDAPControl **serverctrls, LDAPControl **clientctrls, LDAPMessage *result, int *msgidp) { struct berval *challenge = NULL; struct berval resp; int errnum; char *digest = NULL; int err; if (ld == NULL || user_name == NULL || cred == NULL || cred->bv_val == NULL || result == NULL || msgidp == NULL) return (LDAP_PARAM_ERROR); if (ld->ld_version < LDAP_VERSION3) return (LDAP_PARAM_ERROR); err = ldap_result2error(ld, result, 0); if (err != LDAP_SASL_BIND_IN_PROGRESS) return (err); if ((err = ldap_parse_sasl_bind_result(ld, result, &challenge, 0)) != LDAP_SUCCESS) return (err); if (challenge == NULL) return (LDAP_NO_MEMORY); err = ldap_digest_md5_encode(challenge->bv_val, user_name, cred->bv_val, &digest); ber_bvfree(challenge); if (err == LDAP_SUCCESS) { resp.bv_val = digest; resp.bv_len = strlen(digest); LDAPDebug(LDAP_DEBUG_TRACE, "SASL reply: %s\n", digest, 0, 0); err = ldap_sasl_bind(ld, NULL, LDAP_SASL_DIGEST_MD5, &resp, serverctrls, clientctrls, msgidp); free(digest); } return (err); } int ldap_x_sasl_digest_md5_bind( LDAP *ld, char *user_name, struct berval *cred, LDAPControl **serverctrls, LDAPControl **clientctrls, struct timeval *timeout, LDAPMessage **result) { LDAPMessage *res = NULL; int msgid; int rc; if (ld == NULL || user_name == NULL || cred == NULL || result == NULL) return (LDAP_PARAM_ERROR); if (ld->ld_version < LDAP_VERSION3) return (LDAP_PARAM_ERROR); *result = NULL; rc = sasl_digest_md5_bind_1(ld, user_name, serverctrls, clientctrls, &msgid); if (rc != LDAP_SUCCESS) return (rc); rc = ldap_result(ld, msgid, 1, timeout, &res); if (rc == -1) { if (res != NULL) ldap_msgfree(res); return (ldap_get_lderrno(ld, NULL, NULL)); } rc = ldap_result2error(ld, res, 0); if (rc != LDAP_SASL_BIND_IN_PROGRESS) { *result = res; return (rc); } rc = sasl_digest_md5_bind_2(ld, user_name, cred, serverctrls, clientctrls, res, &msgid); ldap_msgfree(res); res = NULL; if (rc != LDAP_SUCCESS) return (rc); rc = ldap_result(ld, msgid, 1, timeout, &res); if (rc == -1) { if (res != NULL) ldap_msgfree(res); return (ldap_get_lderrno(ld, NULL, NULL)); } *result = res; rc = ldap_result2error(ld, res, 0); return (rc); }