#include <sys_defs.h>
#include <stdlib.h>
#include <string.h>
#ifdef __APPLE_OS_X_SERVER__
#include <syslog.h>
#include <stdio.h>
#include <sys/stat.h>
#endif
#include <msg.h>
#include <mymalloc.h>
#include <stringops.h>
#include <mail_params.h>
#include <xsasl.h>
#include "smtpd.h"
#include "smtpd_sasl_glue.h"
#include "smtpd_chat.h"
#ifdef __APPLE_OS_X_SERVER__
#include <OpenDirectory/OpenDirectory.h>
#include <OpenDirectory/OpenDirectoryPriv.h>
#include <DirectoryService/DirServicesTypes.h>
#include <DirectoryService/DirServices.h>
#include <DirectoryService/DirServicesUtils.h>
#include <DirectoryService/DirServicesConst.h>
#include <CoreFoundation/CFData.h>
#include <CoreFoundation/CFString.h>
#include <CoreFoundation/CFNumber.h>
#include <CoreFoundation/CFPropertyList.h>
#include <Kerberos/gssapi_krb5.h>
#include <mach/boolean.h>
#endif
#ifdef USE_SASL_AUTH
#define STR(s) vstring_str(s)
static XSASL_SERVER_IMPL *smtpd_sasl_impl;
#ifdef __APPLE_OS_X_SERVER__
static NAME_MASK smtpd_pw_server_mask[] = {
"none", PW_SERVER_NONE,
"login", PW_SERVER_LOGIN,
"plain", PW_SERVER_PLAIN,
"cram-md5", PW_SERVER_CRAM_MD5,
"gssapi", PW_SERVER_GSSAPI,
0,
};
ODSessionRef od_session_ref;
ODNodeRef od_node_ref;
#endif
#ifdef __APPLE_OS_X_SERVER__
void smtpd_sasl_initialize( int in_use_pw_server )
{
if ( in_use_pw_server )
{
smtpd_pw_server_sasl_opts = name_mask( VAR_SMTPD_PW_SERVER_OPTS, smtpd_pw_server_mask,
var_smtpd_pw_server_opts );
}
#else
void smtpd_sasl_initialize(void)
{
#endif
if (smtpd_sasl_impl)
msg_panic("smtpd_sasl_initialize: repeated call");
if ((smtpd_sasl_impl = xsasl_server_init(var_smtpd_sasl_type,
var_smtpd_sasl_path)) == 0)
msg_fatal("SASL per-process initialization failed");
}
void smtpd_sasl_connect(SMTPD_STATE *state, const char *sasl_opts_name,
const char *sasl_opts_val)
{
const char *mechanism_list;
state->sasl_reply = vstring_alloc(20);
state->sasl_mechanism_list = 0;
state->sasl_username = 0;
state->sasl_method = 0;
state->sasl_sender = 0;
#define SMTPD_SASL_SERVICE "smtp"
if ((state->sasl_server =
xsasl_server_create(smtpd_sasl_impl, state->client,
SMTPD_SASL_SERVICE, *var_smtpd_sasl_realm ?
var_smtpd_sasl_realm : (char *) 0,
sasl_opts_val)) == 0)
msg_fatal("SASL per-connection initialization failed");
if ((mechanism_list =
xsasl_server_get_mechanism_list(state->sasl_server)) == 0)
msg_fatal("no SASL authentication mechanisms");
state->sasl_mechanism_list = mystrdup(mechanism_list);
}
void smtpd_sasl_disconnect(SMTPD_STATE *state)
{
if (state->sasl_reply) {
vstring_free(state->sasl_reply);
state->sasl_reply = 0;
}
if (state->sasl_mechanism_list) {
myfree(state->sasl_mechanism_list);
state->sasl_mechanism_list = 0;
}
if (state->sasl_username) {
myfree(state->sasl_username);
state->sasl_username = 0;
}
if (state->sasl_method) {
myfree(state->sasl_method);
state->sasl_method = 0;
}
if (state->sasl_sender) {
myfree(state->sasl_sender);
state->sasl_sender = 0;
}
if (state->sasl_server) {
xsasl_server_free(state->sasl_server);
state->sasl_server = 0;
}
}
int smtpd_sasl_authenticate(SMTPD_STATE *state,
const char *sasl_method,
const char *init_response)
{
int status;
const char *sasl_username;
for (status = xsasl_server_first(state->sasl_server, sasl_method,
init_response, state->sasl_reply);
status == XSASL_AUTH_MORE;
status = xsasl_server_next(state->sasl_server, STR(state->buffer),
state->sasl_reply)) {
smtpd_chat_reply(state, "334 %s", STR(state->sasl_reply));
smtpd_chat_query(state);
if (strcmp(STR(state->buffer), "*") == 0) {
msg_warn("%s: SASL %s authentication aborted",
state->namaddr, sasl_method);
smtpd_chat_reply(state, "501 5.7.0 Authentication aborted");
return (-1);
}
}
if (status != XSASL_AUTH_DONE) {
msg_warn("%s: SASL %s authentication failed: %s",
state->namaddr, sasl_method,
STR(state->sasl_reply));
smtpd_chat_reply(state, "535 5.7.8 Error: authentication failed: %s",
STR(state->sasl_reply));
return (-1);
}
smtpd_chat_reply(state, "235 2.7.0 Authentication successful");
if ((sasl_username = xsasl_server_get_username(state->sasl_server)) == 0)
msg_panic("cannot look up the authenticated SASL username");
state->sasl_username = mystrdup(sasl_username);
printable(state->sasl_username, '?');
state->sasl_method = mystrdup(sasl_method);
printable(state->sasl_method, '?');
return (0);
}
void smtpd_sasl_logout(SMTPD_STATE *state)
{
if (state->sasl_username) {
myfree(state->sasl_username);
state->sasl_username = 0;
}
if (state->sasl_method) {
myfree(state->sasl_method);
state->sasl_method = 0;
}
}
#ifdef __APPLE_OS_X_SERVER__
#include <sys/param.h>
static bool od_open ( void );
static int od_do_clear_text_auth ( const char *in_user, const char *in_passwd );
static int od_validate_response ( const char *in_user, const char *in_chal, const char *in_resp, const char *in_auth_type );
static ODRecordRef od_get_user_record ( const char *in_user );
static void print_cf_error ( CFErrorRef in_cf_err_ref, const char *in_default_str );
static char *do_auth_login ( SMTPD_STATE *state, const char *in_method );
static char *do_auth_plain ( SMTPD_STATE *state, const char *in_method, const char *in_resp );
static char *do_auth_cram_md5 ( SMTPD_STATE *state, const char *in_method );
static void get_random_chars ( char *out_buf, int in_len );
char *smtpd_pw_server_authenticate ( SMTPD_STATE *state, const char *in_method, const char *in_resp )
{
char *myname = "smtpd_pw_server_authenticate";
if ( state->sasl_username || state->sasl_method )
{
msg_panic( "%s: already authenticated", myname );
}
if ( strcasecmp( in_method, "LOGIN" ) == 0 )
{
return( do_auth_login( state, in_method ) );
}
else if ( strcasecmp( in_method, "PLAIN" ) == 0 )
{
return( do_auth_plain( state, in_method, in_resp ) );
}
else if ( strcasecmp( in_method, "CRAM-MD5" ) == 0 )
{
return( do_auth_cram_md5( state, in_method ) );
}
msg_error( "Authentication method: &s is not supported", in_method );
return ( "504 Unsupported authentication method" );
}
static void print_cf_error ( CFErrorRef in_cf_err_ref, const char *in_default_str )
{
char c_str[1024 + 1];
CFStringRef cf_str_ref = NULL;
if ( in_cf_err_ref != NULL )
{
cf_str_ref = CFErrorCopyFailureReason( in_cf_err_ref );
if ( cf_str_ref != NULL )
{
CFStringGetCString( cf_str_ref, c_str, 1024, kCFStringEncodingUTF8 );
msg_error( "CF: %s", c_str );
return;
}
}
msg_error( "%s", in_default_str );
}
static void get_random_chars ( char *out_buf, int in_len )
{
int count = 0;
int file;
unsigned long long microseconds = 0ULL;
struct timeval tv;
struct timezone tz;
memset( out_buf, 0, in_len );
file = open( "/dev/urandom", O_RDONLY, 0 );
if ( file == -1 )
{
syslog( LOG_ERR, "Cannot open /dev/urandom" );
file = open( "/dev/random", O_RDONLY, 0 );
}
if ( file == -1 )
{
syslog( LOG_ERR, "Cannot open /dev/random" );
gettimeofday( &tv, &tz );
microseconds = (unsigned long long)tv.tv_sec;
microseconds *= 1000000ULL;
microseconds += (unsigned long long)tv.tv_usec;
snprintf( out_buf, in_len, "%llu", microseconds );
}
else
{
while ( count < (in_len - 1) )
{
read( file, &out_buf[ count ], 1 );
if ( isalnum( out_buf[ count ] ) )
{
count++;
}
}
close( file );
}
}
static char * do_auth_login ( SMTPD_STATE *state, const char *in_method )
{
static VSTRING *vs_base64 = 0;
static VSTRING *vs_user = 0;
static VSTRING *vs_pwd = 0;
vs_base64 = vstring_alloc(10);
vs_user = vstring_alloc(10);
vs_pwd = vstring_alloc(10);
if ( !(smtpd_pw_server_sasl_opts & PW_SERVER_LOGIN) )
{
msg_error( "Authentication method: LOGIN is not enabled" );
return( "504 Authentication method not enabled" );
}
base64_encode( vs_base64, "Username:", 9 );
smtpd_chat_reply( state, "334 %s", STR(vs_base64) );
smtpd_chat_query( state );
if ( strcmp(vstring_str( state->buffer ), "*") == 0 )
{
msg_error( "Authentication aborted by client" );
return ( "501 Authentication aborted" );
}
if ( base64_decode( vs_user, STR(state->buffer), VSTRING_LEN(state->buffer) ) == 0 )
{
msg_error( "Malformed response to: AUTH LOGIN" );
return( "501 Authentication failed: malformed initial response" );
}
base64_encode( vs_base64, "Password:", 9 );
smtpd_chat_reply( state, "334 %s", STR(vs_base64) );
smtpd_chat_query( state );
if ( strcmp(vstring_str( state->buffer ), "*") == 0 )
{
msg_error( "Authentication aborted by client" );
return ( "501 Authentication aborted" );
}
if ( base64_decode( vs_pwd, STR(state->buffer), VSTRING_LEN(state->buffer) ) == 0 )
{
msg_error( "Malformed response to: AUTH LOGIN" );
return ( "501 Authentication failed: malformed response" );
}
if ( od_do_clear_text_auth( STR(vs_user), STR(vs_pwd) ) == eAOD_no_error )
{
state->sasl_username = mystrdup( STR(vs_user) );
state->sasl_method = mystrdup( in_method );
return( NULL );
}
else
{
msg_error( "Authentication failed" );
return ( "535 Error: authentication failed" );
}
}
static char *do_auth_plain ( SMTPD_STATE *state, const char *in_method, const char *in_resp )
{
char *ptr = NULL;
char *p_user = NULL;
char *p_pwd = NULL;
static VSTRING *vs_base64 = 0;
vs_base64 = vstring_alloc(10);
if ( !(smtpd_pw_server_sasl_opts & PW_SERVER_PLAIN) )
{
msg_error( "Authentication method: PLAIN is not enabled" );
return ( "504 Authentication method not enabled" );
}
if ( in_resp == NULL )
{
smtpd_chat_reply( state, "334" );
smtpd_chat_query( state );
if ( base64_decode( vs_base64, STR(state->buffer), VSTRING_LEN(state->buffer) ) == 0 )
{
msg_error( "Malformed response to: AUTH PLAIN" );
return ( "501 Authentication failed: malformed initial response" );
}
}
else
{
if ( base64_decode( vs_base64, in_resp, strlen( in_resp ) ) == 0 )
{
msg_error( "Malformed response to: AUTH PLAIN" );
return ( "501 Authentication failed: malformed initial response" );
}
}
ptr = STR(vs_base64);
if ( *ptr == '\0' )
{
ptr++;
}
if ( ptr != NULL )
{
p_user = ptr;
ptr = ptr + (strlen( p_user ) + 1 );
if ( ptr != NULL )
{
p_pwd = ptr;
if ( od_do_clear_text_auth( p_user, p_pwd ) == eAOD_no_error )
{
state->sasl_username = mystrdup( p_user );
state->sasl_method = mystrdup( in_method );
return( NULL );
}
}
}
return ( "535 Error: authentication failed" );
}
static char *do_auth_cram_md5 ( SMTPD_STATE *state, const char *in_method )
{
int len = 0;
char *ptr = NULL;
char *resp_ptr = NULL;
static VSTRING *vs_base64 = 0;
static VSTRING *vs_chal = 0;
static VSTRING *vs_user = 0;
char rand_buf[ 17 ];
char host_name[ MAXHOSTNAMELEN + 1 ];
vs_base64 = vstring_alloc(10);
vs_chal = vstring_alloc(10);
vs_user = vstring_alloc(10);
if ( !(smtpd_pw_server_sasl_opts & PW_SERVER_CRAM_MD5) )
{
msg_error( "Authentication method: CRAM-MD5 is not enabled" );
return ( "504 Authentication method not enabled" );
}
gethostname( host_name, sizeof( host_name ) );
get_random_chars( rand_buf, 17 );
vstring_sprintf( vs_chal, "<%lu.-%s.-%lu-@-%s>", getpid(), rand_buf, time(0), host_name );
base64_encode( vs_base64, STR(vs_chal), VSTRING_LEN(vs_chal) );
smtpd_chat_reply( state, "334 %s", STR(vs_base64) );
smtpd_chat_query( state );
if ( strcmp( vstring_str( state->buffer ), "*" ) == 0 )
{
return( "501 Authentication aborted" );
}
if ( base64_decode( vs_base64, STR(state->buffer), VSTRING_LEN(state->buffer) ) == 0 )
{
msg_error( "Malformed response to: AUTH CRAM-MD5" );
return( "501 Authentication failed: malformed initial response" );
}
resp_ptr = STR(vs_base64);
ptr = strchr( resp_ptr, ' ' );
if ( ptr != NULL )
{
len = ptr - resp_ptr;
vs_user = vstring_strncpy( vs_user, resp_ptr, len );
ptr++;
if ( ptr != NULL )
{
if ( od_validate_response( STR(vs_user), STR(vs_chal), ptr, kDSStdAuthCRAM_MD5 ) == eAOD_no_error )
{
state->sasl_username = mystrdup( STR(vs_user) );
state->sasl_method = mystrdup( in_method );
return( NULL );
}
}
}
return ( "535 Error: authentication failed" );
}
static int od_do_clear_text_auth ( const char *in_user, const char *in_passwd )
{
int out_result = eAOD_auth_failed;
CFErrorRef cf_err_ref = NULL;
ODRecordRef od_rec_ref = NULL;
CFStringRef cf_str_user = NULL;
CFStringRef cf_str_pwd = NULL;
if ( (in_user == NULL) || (in_passwd == NULL) )
{
return( eAOD_param_error );
}
if ( od_open() == FALSE )
{
return( eAOD_open_OD_failed );
}
od_rec_ref = od_get_user_record( in_user );
if ( od_rec_ref == NULL )
{
print_cf_error( cf_err_ref, "Unable to lookup user record" );
return( eAOD_unknown_user );
}
cf_str_pwd = CFStringCreateWithCString( NULL, in_passwd, kCFStringEncodingUTF8 );
if ( cf_str_pwd == NULL )
{
CFRelease( od_rec_ref );
msg_error( "Unable to create user name CFStringRef" );
return;
}
if ( ODRecordVerifyPassword( od_rec_ref, cf_str_pwd, &cf_err_ref ) )
{
out_result = eAOD_no_error;
}
else
{
if ( cf_err_ref != NULL )
{
print_cf_error( cf_err_ref, "Auth failed" );
}
out_result = eAOD_passwd_mismatch;
}
if ( cf_err_ref != NULL )
{
CFRelease( cf_err_ref );
}
if ( od_rec_ref != NULL )
{
CFRelease( od_rec_ref );
}
if ( cf_str_pwd != NULL )
{
CFRelease( cf_str_pwd );
}
return( out_result );
}
bool od_open ( void )
{
CFErrorRef cf_err_ref;
od_session_ref = ODSessionCreate( kCFAllocatorDefault, NULL, &cf_err_ref );
if ( od_session_ref == NULL )
{
print_cf_error( cf_err_ref, "Unable to create OD Session" );
return( FALSE );
}
od_node_ref = ODNodeCreateWithNodeType( kCFAllocatorDefault, od_session_ref, kODNodeTypeAuthentication, &cf_err_ref );
if ( od_session_ref == NULL )
{
print_cf_error( cf_err_ref, "Unable to create OD Node Reference" );
CFRelease( od_session_ref );
od_session_ref = NULL;
return( FALSE );
}
CFRetain( od_session_ref );
CFRetain( od_node_ref );
return( TRUE );
}
static ODRecordRef od_get_user_record ( const char *in_user )
{
CFStringRef cf_str_user = NULL;
ODRecordRef od_rec_ref = NULL;
CFTypeRef cf_type_val[] = { CFSTR(kDSAttributesStandardAll) };
CFArrayRef cf_arry_attr = CFArrayCreate( NULL, cf_type_val, 1, &kCFTypeArrayCallBacks );
CFErrorRef cf_err_ref = NULL;
cf_str_user = CFStringCreateWithCString( NULL, in_user, kCFStringEncodingUTF8 );
if ( cf_str_user == NULL )
{
msg_error( "Unable to create user name CFStringRef" );
return( NULL );
}
od_rec_ref = ODNodeCopyRecord( od_node_ref, CFSTR(kDSStdRecordTypeUsers), cf_str_user, cf_arry_attr, &cf_err_ref );
if ( od_rec_ref == NULL )
{
print_cf_error( cf_err_ref, "Unable to lookup user record" );
}
if ( cf_str_user != NULL )
{
CFRelease( cf_str_user );
}
if ( cf_arry_attr != NULL )
{
CFRelease( cf_arry_attr );
}
return( od_rec_ref );
}
int od_validate_response ( const char *in_user, const char *in_chal, const char *in_resp, const char *in_auth_type )
{
bool b_result = FALSE;
CFStringRef cf_str_user = NULL;
CFErrorRef cf_err_ref = NULL;
ODRecordRef od_rec_ref = NULL;
CFStringRef cf_str_chal = NULL;
CFStringRef cf_str_resp = NULL;
CFMutableArrayRef cf_arry_buf = CFArrayCreateMutable( NULL, 3, &kCFTypeArrayCallBacks );
CFArrayRef cf_arry_resp = NULL;
ODContextRef od_context_ref = NULL;
ODAuthenticationType od_auth_type;
if ( (in_user == NULL) || (in_chal == NULL) || (in_resp == NULL) || (in_auth_type == NULL) )
{
msg_error( "AOD: Invalid argument passed to validate response" );
return( eAOD_param_error );
}
if ( od_open() == FALSE )
{
return( eAOD_open_OD_failed );
}
od_rec_ref = od_get_user_record( in_user );
if ( od_rec_ref == NULL )
{
print_cf_error( cf_err_ref, "Unable to lookup user record" );
return( eAOD_system_error );
}
cf_str_user = CFStringCreateWithCString( NULL, in_user, kCFStringEncodingUTF8 );
CFArrayAppendValue( cf_arry_buf, cf_str_user );
cf_str_chal = CFStringCreateWithCString( NULL, in_chal, kCFStringEncodingUTF8 );
CFArrayAppendValue( cf_arry_buf, cf_str_chal );
cf_str_resp = CFStringCreateWithCString( NULL, in_resp, kCFStringEncodingUTF8 );
CFArrayAppendValue( cf_arry_buf, cf_str_resp );
b_result = ODRecordVerifyPasswordExtended( od_rec_ref, kODAuthenticationTypeCRAM_MD5, cf_arry_buf, &cf_arry_resp, &od_context_ref, &cf_err_ref );
if ( cf_str_user != NULL )
{
CFRelease( cf_str_user );
}
if ( cf_err_ref != NULL )
{
CFRelease( cf_err_ref );
}
if ( od_rec_ref != NULL )
{
CFRelease( od_rec_ref );
}
if ( cf_str_chal != NULL )
{
CFRelease( cf_str_chal );
}
if ( cf_str_resp != NULL )
{
CFRelease( cf_str_resp );
}
if ( cf_arry_buf != NULL )
{
CFRelease( cf_arry_buf );
}
if ( cf_arry_resp != NULL )
{
CFRelease( cf_arry_resp );
}
if ( b_result == TRUE )
{
return( eAOD_no_error );
}
return( eAOD_passwd_mismatch );
}
#endif
#endif