diff -Naur tnftpd-20100324/configure tnftpd/configure --- tnftpd-20100324/configure 2010-06-18 15:33:24.000000000 -0700 +++ tnftpd/configure 2010-06-18 15:47:13.000000000 -0700 @@ -881,6 +881,7 @@ with_pam with_sia with_skey +with_gssapi enable_dependency_tracking enable_shared enable_static @@ -1552,6 +1553,8 @@ Architecture (SIA) authentication [default=auto] --with-skey enable support for S/Key authentication (not compatible with --with-pam) [default=no] + --with-gssapi enable support for GSSAPI Kerberos authentication + [default=no] --with-gnu-ld assume the C compiler uses GNU ld [default=no] --with-pic try to use only PIC/non-PIC objects [default=use both] @@ -3487,6 +3490,14 @@ fi +# Check whether --with-gssapi was given. +if test "${with_gssapi+set}" = set; then : + withval=$with_gssapi; +else + with_gssapi=no +fi + + # # Autoheader templates symbols. # @@ -22030,6 +22042,80 @@ fi +# Check for GSSAPI +# +if test "$with_gssapi" != no; then : + { $as_echo "$as_me:${as_lineno-$LINENO}: --with-gssapi=$with_gssapi; checking for required GSSAPI features" >&5 +$as_echo "$as_me: --with-gssapi=$with_gssapi; checking for required GSSAPI features" >&6;} + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for gss_krb5_copy_ccache in -lgssapi_krb5" >&5 +$as_echo_n "checking for gss_krb5_copy_ccache in -lgssapi_krb5... " >&6; } +if test "${ac_cv_lib_gssapi_krb5_gss_krb5_copy_ccache+set}" = set; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lgssapi_krb5 $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char gss_krb5_copy_ccache (); +int +main () +{ +return gss_krb5_copy_ccache (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_gssapi_krb5_gss_krb5_copy_ccache=yes +else + ac_cv_lib_gssapi_krb5_gss_krb5_copy_ccache=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_gssapi_krb5_gss_krb5_copy_ccache" >&5 +$as_echo "$ac_cv_lib_gssapi_krb5_gss_krb5_copy_ccache" >&6; } +if test "x$ac_cv_lib_gssapi_krb5_gss_krb5_copy_ccache" = x""yes; then : + LIBS="-lgssapi_krb5 $LIBS" + $as_echo "#define USE_GSSAPI 1" >>confdefs.h + + for ac_header in gssapi.h +do : + ac_fn_c_check_header_mongrel "$LINENO" "gssapi.h" "ac_cv_header_gssapi_h" "$ac_includes_default" +if test "x$ac_cv_header_gssapi_h" = x""yes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_GSSAPI_H 1 +_ACEOF + +fi + +done + + { $as_echo "$as_me:${as_lineno-$LINENO}: enabling GSSAPI/Kerberos authentication" >&5 +$as_echo "$as_me: enabling GSSAPI/Kerberos authentication" >&6;} + with_gssapi=yes +else + if test "$with_gssapi" != auto; then : + { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error "--with-gssapi was given, but gss_krb5_copy_ccache() wasn't found +See \`config.log' for more details." "$LINENO" 5; } +fi + { $as_echo "$as_me:${as_lineno-$LINENO}: disabling --with-gssapi" >&5 +$as_echo "$as_me: disabling --with-gssapi" >&6;} + with_gssapi=no +fi + +fi + # Tests for items required for inbuilt ls. # if test "$opt_builtinls" = yes; then : @@ -23548,5 +23634,7 @@ $as_echo "$as_me: SIA authentication: $with_sia" >&6;} { $as_echo "$as_me:${as_lineno-$LINENO}: S/Key authentication: $with_skey" >&5 $as_echo "$as_me: S/Key authentication: $with_skey" >&6;} +{ $as_echo "$as_me:${as_lineno-$LINENO}: GSSAPI authentication: $with_gssapi" >&5 +$as_echo "$as_me: GSSAPI authentication: $with_gssapi" >&6;} { $as_echo "$as_me:${as_lineno-$LINENO}: =============================" >&5 $as_echo "$as_me: =============================" >&6;} diff -Naur tnftpd-20100324/configure.ac tnftpd/configure.ac --- tnftpd-20100324/configure.ac 2010-06-18 15:33:24.000000000 -0700 +++ tnftpd/configure.ac 2010-06-18 15:47:06.000000000 -0700 @@ -57,6 +57,12 @@ [default=no]])], [], [with_skey=no]) +AC_ARG_WITH([gssapi], + [AS_HELP_STRING([--with-gssapi], + [enable support for GSSAPI Kerberos authentication + [default=no]])], + [], + [with_gssapi=no]) # # Autoheader templates symbols. @@ -81,6 +87,8 @@ [Define if using SIA authentication.]) AH_TEMPLATE([USE_SKEY], [Define if using S/Key authentication.]) +AH_TEMPLATE([USE_GSSAPI], + [Define if using GSSAPI authentication.]) # # Checks for programs. @@ -414,6 +433,23 @@ AC_MSG_NOTICE([disabling --with-skey]) with_skey=no])]) +# Check for GSSAPI +# +AS_IF([test "$with_gssapi" != no], + [AC_MSG_NOTICE([--with-gssapi=$with_gssapi; checking for required GSSAPI features]) + AC_CHECK_LIB([gssapi_krb5], + [gss_krb5_copy_ccache], + [LIBS="-lgssapi_krb5 $LIBS" + AC_DEFINE([USE_GSSAPI], [1]) + AC_CHECK_HEADERS([gssapi.h]) + AC_MSG_NOTICE([enabling GSSAPI/Kerberos authentication]) + with_gssapi=yes], + [AS_IF([test "$with_gssapi" != auto], + [AC_MSG_FAILURE( + [--with-gssapi was given, but gss_krb5_copy_ccache() wasn't found])]) + AC_MSG_NOTICE([disabling --with-gssapi]) + with_gssapi=no])]) + # Tests for items required for inbuilt ls. # AS_IF([test "$opt_builtinls" = yes], @@ -451,4 +487,5 @@ AC_MSG_NOTICE([PAM authentication: $with_pam]) AC_MSG_NOTICE([SIA authentication: $with_sia]) AC_MSG_NOTICE([S/Key authentication: $with_skey]) +AC_MSG_NOTICE([GSSAPI authentication: $with_gssapi]) AC_MSG_NOTICE([ =============================]) diff -Naur tnftpd-20100324/src/cmds.c tnftpd/src/cmds.c --- tnftpd-20100324/src/cmds.c 2009-11-06 19:26:48.000000000 -0800 +++ tnftpd/src/cmds.c 2010-06-18 15:35:42.000000000 -0700 @@ -134,7 +134,6 @@ } factelem; static void ack(const char *); -static void base64_encode(const char *, size_t, char *, int); static void fact_type(const char *, FILE *, factelem *); static void fact_size(const char *, FILE *, factelem *); static void fact_modify(const char *, FILE *, factelem *); @@ -558,7 +557,7 @@ * If nulterm is non-zero, terminate with \0 otherwise pad to 3 byte boundary * with `='. */ -static void +void base64_encode(const char *clear, size_t len, char *encoded, int nulterm) { static const char base64[] = @@ -589,6 +588,60 @@ *e = '\0'; } +/* + * decode base64-encoded into clear, to a max of len clear bytes + * return (positive) length if decode okay, + * or negative code for use with base64_decode_error() below + */ + +int +base64_decode(const char *encoded, char *clear, size_t len) +{ + static const char base64[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + int i, j = 0; + int Q = 0; + char *p; + unsigned char c = 0; + len--; + + for (i = 0; encoded[i] && encoded[i] != '='; i++) { + if ((p = strchr(base64, encoded[i])) == NULL) return(-1); + if (j == len) return(-7); + Q = p - base64; + switch (i & 3) { + case 0: c = Q<<2; break; + case 1: clear[j++] = c | Q>>4; c = (Q&15)<<4; break; + case 2: clear[j++] = c | Q>>2; c = (Q&3)<<6; break; + case 3: clear[j++] = c | Q; break; + } + } + /* failure: bad padding, or decoded bits % 8 */ + switch (i & 3) { + case 1: return(-3); + case 2: if (Q&15) return(-3); + if (strcmp((char *)&clear[i], "==")) return(-2); + break; + case 3: if (Q&3) return(-3); + if (strcmp((char *)&clear[i], "=")) return(-2); + } + clear[j] = '\0'; + return(j); +} + +char * +base64_decode_error(int reason) +{ + switch(reason) { + case 0: return("OK"); + case -1: return("Bad character in encoded message"); + case -2: return("Encoded message not properly padded"); + case -3: return("Encoded message has odd number of bits"); + case -7: return("Message too long for decode buffer"); + default: return("Unknown error"); + } +} + static void fact_modify(const char *fact, FILE *fd, factelem *fe) { diff -Naur tnftpd-20100324/src/extern.h tnftpd/src/extern.h --- tnftpd-20100324/src/extern.h 2009-11-06 19:26:48.000000000 -0800 +++ tnftpd/src/extern.h 2010-06-18 15:35:42.000000000 -0700 @@ -123,6 +123,9 @@ #define FTP_BUFLEN 512 void abor(void); +int base64_decode(const char *, char *, size_t); +char *base64_decode_error(int); +void base64_encode(const char *, size_t, char *, int); void closedataconn(FILE *); char *conffilename(const char *); void count_users(void); @@ -347,6 +350,33 @@ GLOBAL const char *version; GLOBAL int is_oob; +#if (USE_GSSAPI && HAVE_GSSAPI_H) +#include +#include + +GLOBAL gss_ctx_id_t gcontext; +GLOBAL gss_buffer_desc client_name; +GLOBAL krb5_context kcontext; +GLOBAL krb5_ccache kccache; +#endif + +GLOBAL char *auth_type; +GLOBAL char *auth_type_tmp; /* post-AUTH && pre-ADAT */ +GLOBAL int auth_control_prot; /* protection levels */ +GLOBAL int auth_data_prot; +GLOBAL int authorized; /* via supported AUTH */ +GLOBAL int creds_valid; +GLOBAL int auth_want_creds; +GLOBAL int auth_have_creds; + +/* these should be defined in */ +#ifndef PROT_C +#define PROT_C 1 /* clear */ +#define PROT_S 2 /* safe */ +#define PROT_P 3 /* private */ +#define PROT_E 4 /* confidential */ +#endif +GLOBAL char *protnames[]; /* total file data bytes */ GLOBAL off_t total_data_in, total_data_out, total_data; /* total number of data files */ diff -Naur tnftpd-20100324/src/ftpcmd.y tnftpd/src/ftpcmd.y --- tnftpd-20100324/src/ftpcmd.y 2009-11-06 19:26:48.000000000 -0800 +++ tnftpd/src/ftpcmd.y 2010-06-18 15:35:42.000000000 -0700 @@ -119,6 +119,15 @@ char *cmdp; char *fromname; +extern int auth_required; +extern int auth_ccc_allowed; +extern int auth_ccc_enabled; +extern int auth_control_prot; +unsigned int auth_max_buffer; +unsigned char *auth_max_buffer_ptr; +unsigned int auth_max_buffer_tmp; +static int prot_type; + extern int epsvall; struct tab sitetab[]; @@ -748,33 +757,77 @@ /* RFC 2228 */ | AUTH SP mechanism_name CRLF { - reply(502, "RFC 2228 authentication not implemented."); + auth((char *) $3); free($3); } | ADAT SP base64data CRLF { - reply(503, - "Please set authentication state with AUTH."); + adat((unsigned char *) $3); free($3); } | PROT SP prot_code CRLF { - reply(503, - "Please set protection buffer size with PBSZ."); - free($3); + if (auth_max_buffer) + { + switch (prot_type) { + case PROT_S: +#ifdef DO_NOT_ENCRYPT + case PROT_P: +#endif + if (auth_type) + case PROT_C: + reply(200, "Data channel protected at level: %s", + (auth_data_prot = prot_type) == PROT_S ? + "safe" : auth_data_prot == PROT_C ? + "clear" : "private"); + else + default: + reply(536, "protection level %s not supported", + protnames[prot_type]); + } + + } else + reply(503, "Must set PBSZ first."); } - | PBSZ SP decimal_integer CRLF + | PBSZ SP STRING CRLF { - reply(503, - "Please set authentication state with AUTH."); + if (!auth_type) + reply(503, "Must perform authentication first"); + else if (strlen($3) > 10 || + (strlen($3) == 10 && strcmp($3,"4294967296") >= 0)) + reply(501, "Bad value for PBSZ: %s", $3); + else { + if (auth_max_buffer_ptr) (void) free(auth_max_buffer_ptr); + auth_max_buffer_tmp = (unsigned int) atol($3); + /* try requested, then smaller buffers until too bad */ + while ((auth_max_buffer_ptr = (unsigned char *)malloc(auth_max_buffer_tmp)) == NULL) + if (auth_max_buffer_tmp) + reply(-200, "Trying %u", auth_max_buffer_tmp >>= 2); + else { + perror_reply(421, + "Local resource failure: malloc"); + dologout(1); + } + reply(200, "PBSZ=%u", auth_max_buffer = auth_max_buffer_tmp); + } + free ($3); } | CCC CRLF { - reply(533, "No protection enabled."); + if (!auth_ccc_allowed) { + reply(534, "CCC not supported"); + } else { + if(auth_control_prot == PROT_C && !auth_ccc_enabled) { + reply(533, "CCC command must be integrity protected"); + } else { + reply(200, "CCC command successful."); + auth_ccc_enabled = 1; + } + } } | MIC SP base64data CRLF @@ -1017,6 +1070,25 @@ } ; +prot_code + : C + { + prot_type = PROT_C; + } + | S + { + prot_type = PROT_S; + } + | P + { + prot_type = PROT_P; + } + | E + { + prot_type = PROT_E; + } + ; + type_code : A { @@ -1264,7 +1336,7 @@ { "AUTH", AUTH, STR1, 1, " mechanism-name", 0, }, { "ADAT", ADAT, STR1, 1, " base-64-data", 0, }, { "PROT", PROT, STR1, 1, " prot-code", 0, }, - { "PBSZ", PBSZ, ARGS, 1, " decimal-integer", 0, }, + { "PBSZ", PBSZ, STR1, 1, " decimal-integer", 0, }, { "CCC", CCC, NOARGS, 1, "(Disable data protection)", 0, }, { "MIC", MIC, STR1, 4, " base64data", 0, }, { "CONF", CONF, STR1, 4, " base64data", 0, }, @@ -1361,6 +1433,16 @@ return (0); } +void +upper(register char *s) +{ + while (*s != '\0') { + if (islower((int)(*s))) + *s = toupper((int) *s); + s++; + } +} + #include /* @@ -1444,6 +1526,130 @@ if (c == EOF && cs == s) return (-1); *cs++ = '\0'; + + if (auth_type) { + unsigned char out[sizeof(cbuf)]; + char *cp; + int len; + int mic; + + + /* do we have a protected command? */ + if (!((mic = strncmp(s, "ENC", 3)) && strncmp(s, "MIC", 3) + && strncmp(s, "AUTH", 4) +#ifdef DO_CONFIDENTIAL + && strncmp(s, "CONF", 4) +#endif + ) && (cs = strpbrk(s, " \r\n"))) { + *cs++ = '\0'; /* yes, split into s and cs. */ + } else { /* no, check if that's okay */ + if (auth_ccc_enabled) { + auth_control_prot = PROT_C; + upper(s); + return (0); + } else { + reply(533, "Commands must be protected."); + syslog(LOG_ERR, "Unprotected command received"); + *s = '\0'; + return (0); + } + } + upper(s); + if (ftpd_debug) + syslog(LOG_INFO, "command %s received (mic=%d)", s, mic); +#ifndef DO_CONFIDENTIAL + if (!strcmp(s, "CONF")) { + reply(537, "CONF protected commands not supported."); + *s = '\0'; + return(0); + } +#endif +#ifdef DO_PARANOID + /* truly paranoid sites will also require command encryption */ + if (mic) { + reply(533, "All commands must be ENC protected. Retry command under ENC."); + *s = '\0'; + return(0); + } +#endif /* DO_PARANOID */ +#ifdef DO_NOT_ENCRYPT + if (!mic) { + reply(533, "ENC protection not supported. Retry command under MIC."); + *s = '\0'; + return(0); + } +#endif /* DO_NOT_ENCRYPT */ + if ((cp = strpbrk(cs, " \r\n"))) + *cp = '\0'; + len = base64_decode((unsigned char)cs, out, strlen(cs)); + if (len < 0) { + reply(501, "could not decode argument to %s command: %s", + mic ? "MIC" : "ENC", base64_decode_error(len)); + *s = '\0'; + return(0); + } + if (ftpd_debug) syslog(LOG_DEBUG, "getline got %lu from %s <%s>\n", + (unsigned long) len, cs, mic ? "MIC" : "ENC"); + auth_control_prot = mic ? PROT_S : PROT_P; +#ifdef USE_GSSAPI + /* we know this is a MIC or ENC already, and out/len already has the bits */ + if (strcmp(auth_type, "GSSAPI") == 0) { + gss_buffer_desc transmit_buf, message_buf; + OM_uint32 major, minor; + int conf_state; + + transmit_buf.value = out; + transmit_buf.length = len; + /* decrypt the message */ + conf_state = !mic; + major = gss_unseal(&minor, gcontext, &transmit_buf, + &message_buf, &conf_state, NULL); + if (major == GSS_S_CONTINUE_NEEDED) { + if (ftpd_debug) syslog(LOG_DEBUG, "%s-unseal continued", + mic ? "MIC" : "ENC"); + reply(535, "%s-unseal continued, oops", + mic ? "MIC" : "ENC"); + *s = '\0'; + return(0); + } + if (major != GSS_S_COMPLETE) { + reply_gss_error(535, major, minor, + mic ? "failed unsealing MIC message" : + "failed unsealing ENC message"); + *s = '\0'; + return(0); + } + + memcpy(s, message_buf.value, message_buf.length); + memcpy(s + message_buf.length - (s[message_buf.length-1] ? 0 : 1), "\r\n", 3); + gss_release_buffer(&minor, &message_buf); + } +#endif /* USE_GSSAPI */ + /* other auth types ... */ + + /* some antique clients misunderstood the spec */ + if (auth_required && mic && !strncmp(s, "PASS", 4)) { + reply(-530, "Password refused: there is a problem with your ftp client."); + reply(530, "Enable encryption before logging in, or update your ftp program."); + *s = '\0'; + return(0); + } + + } +#ifdef USE_GSSAPI /* or other auth types */ + else { /* !auth_type */ + if ( (!(strncmp(s, "ENC", 3))) || (!(strncmp(s, "MIC", 3))) +#ifdef DO_CONFIDENTIAL + || (!(strncmp(s, "CONF", 4))) +#endif + ) { + reply(503, "Must perform authentication before sending protected commands"); + *s = '\0'; + return(0); + } + } +#endif /* */ + if (ftpd_debug) { if ((curclass.type != CLASS_GUEST && strncasecmp(s, "PASS ", 5) == 0) || diff -Naur tnftpd-20100324/src/ftpd.c tnftpd/src/ftpd.c --- tnftpd-20100324/src/ftpd.c 2010-06-18 15:33:24.000000000 -0700 +++ tnftpd/src/ftpd.c 2010-06-18 15:35:42.000000000 -0700 @@ -176,6 +176,14 @@ #include #endif +#ifdef USE_GSSAPI +#include +#include +#include +#include +#include +#endif + #endif /* !defined(HAVE_TNFTPD_H) */ #define GLOBAL @@ -235,6 +243,41 @@ int login_krb5_forwardable_tgt = 0; #endif +/* + * RFC2228 theoretically supports other auth types, + * so these are not necessarily GSSAPI-specific + */ +int auth_required = 0; /* needs cli flag */ +int auth_want_creds = 0; /* needs cli flag */ +int auth_have_creds = 0; +int auth_ccc_allowed; /* needs cli flag */ +int auth_ccc_enabled; /* CCC command */ +char *auth_type; /* AUTH command */ +char *auth_type_tmp; /* post-AUTH && pre-ADAT */ + +int auth_control_prot; +int auth_data_prot; +int auth_user_ok; + + /* should be */ +char *protnames[] = + {"0", "Clear", "Safe", "Private", "Confidential" }; + +#define AUTH_NONE 0 +#define AUTH_MUST_AUTHENTICATE 1 +#define AUTH_MUST_AUTHORIZE 2 + +#ifdef USE_GSSAPI +gss_buffer_desc auth_gss_client_name; +krb5_context kcontext; +krb5_ccache kccache; + +static void ftpd_gss_convert_creds(char *name, gss_cred_id_t); +static int ftpd_gss_userok(gss_buffer_t, char *name); +void reply_gss_error(int code, OM_uint32 major, OM_uint32 minor, char *s); + +#endif + int epsvall = 0; /* @@ -340,6 +383,10 @@ gidcount = 0; is_oob = 0; version = FTPD_VERSION; + auth_required = AUTH_NONE; + auth_ccc_allowed = 0; + auth_ccc_enabled = 0; + auth_want_creds = 0; /* * LOG_NDELAY sets up the logging connection immediately, @@ -348,7 +395,7 @@ openlog("ftpd", LOG_PID | LOG_NDELAY, FTPD_LOGTYPE); while ((ch = getopt(argc, argv, - "46a:c:C:Dde:h:HlL:nP:qQrst:T:uUvV:wWX")) != -1) { + "46a:c:C:Dde:h:HkKlL:nP:qQrst:T:uUvV:wWXzZ")) != -1) { switch (ch) { case '4': af = AF_INET; @@ -417,6 +464,14 @@ hostname[sizeof(hostname) - 1] = '\0'; break; + case 'k': + auth_ccc_allowed = 1; + break; + + case 'K': + auth_want_creds = 1; + break; + case 'l': logging++; /* > 1 == extra logging */ break; @@ -493,6 +548,14 @@ doxferlog |= 1; break; + case 'z': + auth_required = AUTH_MUST_AUTHORIZE; + break; + + case 'Z': + auth_required = AUTH_MUST_AUTHENTICATE; + break; + default: if (optopt == 'a' || optopt == 'C') exit(1); @@ -766,6 +829,9 @@ mode = MODE_S; tmpline[0] = '\0'; hasyyerrored = 0; +#ifdef USE_GSSAPI + auth_control_prot = auth_data_prot = PROT_C; +#endif #ifdef KERBEROS5 kerror = krb5_init_context(&kcontext); @@ -998,6 +1064,9 @@ #if defined(KERBEROS5) k5destroy(); #endif +#ifdef USE_GSSAPI /* or other auth */ + auth_user_ok = 0; +#endif curclass.type = CLASS_REAL; askpasswd = 0; @@ -1024,6 +1093,9 @@ goto cleanup_user; } name = "ftp"; + } else if (auth_required && !auth_type) { + reply(530, "must authenticate before USER"); + goto cleanup_user; } else { pw = sgetpwnam(name); @@ -1102,6 +1174,47 @@ goto cleanup_user; } + if (auth_type) { + int result; + char buf[MAXPATHLEN]; +#ifdef USE_GSSAPI + if (auth_type && strcmp(auth_type, "GSSAPI") == 0) { + size_t len; + + auth_user_ok = ftpd_gss_userok(&auth_gss_client_name, (char*)name) == 0; + len = sizeof("GSSAPI user is not authorized as " + "; Password required.") + + strlen(auth_gss_client_name.value) + + strlen(name); + if (len >= sizeof(buf)) { + syslog(LOG_ERR, "user: username too long"); + name = "[username too long]"; + } + snprintf(buf, sizeof(buf), + "GSSAPI user %s is%s authorized as %s", + (char *) auth_gss_client_name.value, + auth_user_ok ? "" : " not", + name); + } +#endif + /* "ok" should apply to other auth types as well */ + if (!auth_user_ok && auth_required == AUTH_MUST_AUTHORIZE) { + strncat(buf, "; Access denied.", + sizeof(buf) - strlen(buf) - 1); + result = 530; + pw = NULL; + } else if (!auth_user_ok || (auth_want_creds && !auth_have_creds)) { + strncat(buf, "; Password required.", + sizeof(buf) - strlen(buf) - 1); + askpasswd = 1; + result = 331; + } /* else result = 232; */ /* login somehow? */ + reply(result, "%s", buf); + syslog(auth_user_ok ? LOG_INFO : LOG_ERR, "%s", buf); + + goto cleanup_user; + } + /* if haven't asked yet (i.e, not anon), ask now */ if (!askpasswd) { askpasswd = 1; @@ -1421,6 +1534,96 @@ pamh = NULL; } #endif + if (auth_have_creds) { +#ifdef USE_GSSAPI + krb5_cc_destroy(kcontext, kccache); +#endif + auth_have_creds = 0; + } +} + +void +setdataprot(int new_prot) +{ + switch (new_prot) { + case PROT_S: +#ifdef DO_NOT_ENCRYPT + case PROT_P: +#endif + if (auth_type) + case PROT_C: + reply(200, "Data channel protected at level: %s", + (auth_data_prot = new_prot) == PROT_S ? + "safe" : auth_data_prot == PROT_C ? + "clear" : "private"); + else + default: + reply(536, "protection level %s not supported", + protnames[new_prot]); + } +} + + +static int +kpass(name, passwd) +char *name, *passwd; +{ +#ifdef USE_GSSAPI + krb5_principal server, me; + krb5_creds my_creds; + krb5_timestamp now; +#endif /* USE_GSSAPI */ + char ccname[MAXPATHLEN]; + +#ifdef USE_GSSAPI + memset(&my_creds, 0, sizeof(my_creds)); + if (krb5_parse_name(kcontext, name, &me)) + return 0; + my_creds.client = me; + + snprintf(ccname, sizeof(ccname), "FILE:/tmp/krb5cc_ftpd%ld", + (long) getpid()); + if (krb5_cc_resolve(kcontext, ccname, &kccache)) + return(0); + if (krb5_cc_initialize(kcontext, kccache, me)) + return(0); + if (krb5_build_principal_ext(kcontext, &server, + krb5_princ_realm(kcontext, me)->length, + krb5_princ_realm(kcontext, me)->data, + KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME, + krb5_princ_realm(kcontext, me)->length, + krb5_princ_realm(kcontext, me)->data, + 0)) + goto wipe_ccache; + + my_creds.server = server; + if (krb5_timeofday(kcontext, &now)) + goto wipe_ccache; + my_creds.times.starttime = 0; /* real time */ + my_creds.times.endtime = now + 60 * 60 * 10; + my_creds.times.renew_till = 0; + + if (krb5_get_init_creds_password(kcontext, &my_creds, me, + passwd, NULL, NULL, 0, NULL, NULL)) + goto wipe_ccache; + + if (krb5_cc_store_cred(kcontext, kccache, &my_creds)) + goto wipe_ccache; + + if (!auth_want_creds) { + krb5_cc_destroy(kcontext, kccache); + return(1); + } + + auth_have_creds = 1; + return(1); + +#endif /* USE_GSSAPI */ +wipe_ccache: +#ifdef USE_GSSAPI + krb5_cc_destroy(kcontext, kccache); +#endif /* USE_GSSAPI */ + return(0); } void @@ -1510,6 +1713,12 @@ } } #endif +#ifdef USE_GSSAPI + /* creds desired and kpass fails */ + rval = auth_want_creds && kpass(pw->pw_name, passwd); + goto skip; +#endif + if (!sflag) rval = checkpassword(pw, passwd); else @@ -1567,6 +1779,12 @@ goto bad; } #endif /* HAVE_MEMBERSHIP_H */ +#ifdef USE_GSSAPI + if (auth_have_creds) { + char *ccname = krb5_cc_get_name(kcontext, kccache); + chown(ccname, pw->pw_uid, pw->pw_gid); + } +#endif login_attempts = 0; /* this time successful */ if (setegid((gid_t)pw->pw_gid) < 0) { reply(550, "Can't set gid."); @@ -2727,6 +2945,7 @@ else reply(0, "Connected to %s", remotehost); + if (auth_type) reply(0, " Authentication type: %s", auth_type); if (logged_in) { if (curclass.type == CLASS_GUEST) reply(0, "Logged in anonymously"); @@ -2735,8 +2954,11 @@ curclass.type == CLASS_CHROOT ? " (chroot)" : ""); } else if (askpasswd) reply(0, "Waiting for password"); + else if (auth_type_tmp) + reply(0, " Waiting for authentication data"); else reply(0, "Waiting for user name"); + reply(0, " Protection level: %s", protnames[auth_data_prot]); cprintf(stdout, " TYPE: %s", typenames[type]); if (type == TYPE_A || type == TYPE_E) cprintf(stdout, ", FORM: %s", formnames[form]); @@ -2990,12 +3212,14 @@ * n < -1 prefix the message with abs(n) + "-" (initial line) * n == 0 prefix the message with 4 spaces (middle lines) * n > 0 prefix the message with n + " " (final line) + * encrypted replies will use the continuation of the cleartext message */ void reply(int n, const char *fmt, ...) { char msg[MAXPATHLEN * 2 + 100]; size_t b; + int secure = 0; va_list ap; b = 0; @@ -3008,7 +3232,52 @@ va_start(ap, fmt); vsnprintf(msg + b, sizeof(msg) - b, fmt, ap); va_end(ap); - cprintf(stdout, "%s\r\n", msg); + + if (auth_type) { + /* base64 expansion, signature, slop */ + char msg_secure[(MAXPATHLEN * 2 + 100) * 3 / 2]; + size_t msg_length = 0; + +#ifdef USE_GSSAPI + /* reply (based on level) */ + if (strcmp(auth_type, "GSSAPI") == 0) { + gss_buffer_desc input_buf, output_buf; + OM_uint32 major, minor; + int conf_state; + + input_buf.value = msg; + input_buf.length = strlen(msg); + major = gss_seal(&minor, gcontext, + auth_control_prot == PROT_P, /* private */ + GSS_C_QOP_DEFAULT, + &input_buf, &conf_state, + &output_buf); + if (major != GSS_S_COMPLETE) { + /* gss_seal didn't complete */ + /* xxx no good way to handle this without infinite loop */ + } else if ((auth_control_prot == PROT_P) && !conf_state) { + /* message wasn't encrypted */ + /* xxx no good way to handle this without infinite loop */ + } else { + memcpy(msg_secure, output_buf.value, + msg_length = output_buf.length); + gss_release_buffer(&minor, &output_buf); + } + } +#endif /* USE_GSSAPI */ + /* other auth types ... */ + if (msg_length >= sizeof(msg) / 4 * 3) { + syslog(LOG_ERR, "input to base64_encode too long"); + } else { + /* rewrite the msg with encoded encrypted copy */ + base64_encode(msg_secure, msg_length, msg, 0); + secure = 1; + } + } + + if(secure) cprintf(stdout, "%s%c%s\r\n", + auth_control_prot == PROT_P ? "632" : "631", n > 0 ? ' ' : '-', msg); + else cprintf(stdout, "%s\r\n", msg); (void)fflush(stdout); if (ftpd_debug) syslog(LOG_DEBUG, "<--- %s", msg); @@ -3063,6 +3332,11 @@ unlink(krbtkfile_env); #endif } +#ifdef USE_GSSAPI + if (auth_have_creds) + krb5_cc_destroy(kcontext, kccache); + auth_ccc_enabled = 0; +#endif /* beware of flushing buffers after a SIGPIPE */ if (xferlogfd != -1) close(xferlogfd); @@ -4247,3 +4521,261 @@ return 0; } #endif /* USE_SIA */ + +#ifdef USE_GSSAPI + +static void +ftpd_gss_convert_creds(char *name, gss_cred_id_t creds) +{ + OM_uint32 major, minor; + krb5_principal me; + char ccname[MAXPATHLEN]; + + /* get ccache */ + if (krb5_parse_name(kcontext, name, &me)) + return; + + snprintf(ccname, sizeof(ccname), "FILE:/tmp/krb5cc_ftpd%ld", + (long) getpid()); + if (krb5_cc_resolve(kcontext, ccname, &kccache)) + return; + if (krb5_cc_initialize(kcontext, kccache, me)) + return; + + /* extract gss-wrapped krb creds into our cache */ + major = gss_krb5_copy_ccache(&minor, creds, kccache); + if (major != GSS_S_COMPLETE) + goto cleanup; + + auth_have_creds = 1; + return; + + cleanup: + krb5_cc_destroy(kcontext, kccache); +} + +/* verify gss-wrapped kerberos user */ +static int +ftpd_gss_userok(gss_buffer_t gclient_name, char *name) +{ + int retval = -1; + krb5_principal p; + + if (krb5_parse_name(kcontext, gclient_name->value, &p) != 0) + return -1; + if (krb5_kuserok(kcontext, p, name)) + retval = 0; + else + retval = 1; + krb5_free_principal(kcontext, p); + return retval; +} + +/* callback seems overkill, but easiest way to handle gssapi */ +static void +reply_gss_error_text(void (*cb)(const char *, int, int), + OM_uint32 major, OM_uint32 minor, int misc) +{ + OM_uint32 gmajor, gminor; + gss_buffer_desc msg; + OM_uint32 msg_ctx; + msg_ctx = 0; + while (!msg_ctx) { + gmajor= gss_display_status(&gminor, major, + GSS_C_GSS_CODE, + GSS_C_NULL_OID, + &msg_ctx, &msg); + if ((gmajor == GSS_S_COMPLETE)|| + (gmajor == GSS_S_CONTINUE_NEEDED)) { + (*cb)((char*)msg.value, misc, 1); + (void) gss_release_buffer(&gminor, &msg); + } + if (gmajor != GSS_S_CONTINUE_NEEDED) + break; + } + msg_ctx = 0; + while (!msg_ctx) { + gmajor = gss_display_status(&gminor, minor, + GSS_C_MECH_CODE, + GSS_C_NULL_OID, + &msg_ctx, &msg); + if ((gmajor == GSS_S_COMPLETE)|| + (gmajor == GSS_S_CONTINUE_NEEDED)) { + (*cb)((char*)msg.value, misc, 0); + (void) gss_release_buffer(&gminor, &msg); + } + if (gmajor != GSS_S_CONTINUE_NEEDED) + break; + } +} + +static void +reply_gss_error_wrapper(const char *msg, int code, int major) +{ + if(code > 0) code *= -1; /* all are continuation messages */ + reply(code, "GSSAPI error %s: %s", major ? "major" : "minor", msg); +} + +void +reply_gss_error(int code, OM_uint32 major, OM_uint32 minor, char *s) +{ + reply_gss_error_text(reply_gss_error_wrapper, major, minor, code); + reply(code, "GSSAPI error: %s", s); +} + +#endif + +/* + * AUTH command + */ +void +auth(char *new_type) +{ + if (auth_type) + reply(534, "Authentication type is already %s", auth_type); + else +#ifdef USE_GSSAPI + if (strcmp(new_type, "GSSAPI") == 0) + reply(334, "ADAT must follow for authentication type %s", + auth_type_tmp = new_type); +#endif + reply(504, "Authentication via %s not supported", new_type); +} + +/* + * ADAT command + * + */ +void +adat(unsigned char *new_key) +{ + if (auth_type) { + reply(503, "Authentication has already been set"); + return; + } + if (!auth_type_tmp) { + reply(503, "AUTH type required before ADAT"); + return; + } + +#ifdef USE_GSSAPI + if (strcmp(auth_type_tmp, "GSSAPI") == 0) { + gss_cred_id_t deleg_creds; + gss_name_t client; + OM_uint32 ret_flags, major, minor; + gss_OID mech_type; + gss_buffer_desc g_token, g_out_token; + char buf[MAXPATHLEN]; + unsigned char g_buf[MAXPATHLEN]; + u_char g_out_buf[MAXPATHLEN]; + int length, replied = 0; + size_t rad_len; + char localname[MAXHOSTNAMELEN]; + struct hostent *hp; + + length = base64_decode(new_key, g_out_buf, MAXPATHLEN); + if (length < 0) { + reply(501, "Couldn't decode ADAT (%s)", + base64_decode_error(length)); + return; + } + g_token.value = g_out_buf; + g_token.length = length; + + if (gethostname(localname, MAXHOSTNAMELEN)) { + reply(501, "couldn't get local hostname (%d)\n", errno); + return; + } + if (!(hp = gethostbyname(localname))) { + reply(501, "couldn't canonicalize local hostname\n"); + return; + } + strncpy(localname, hp->h_name, sizeof(localname) - 1); + localname[sizeof(localname) - 1] = '\0'; + + major = gss_accept_sec_context(&minor, + &gcontext, + GSS_C_NO_CREDENTIAL, /* no verifier */ + &g_token, /* input */ + GSS_C_NO_CHANNEL_BINDINGS, + &client, /* source */ + &mech_type, + &g_out_token, + &ret_flags, + NULL, /* ignore time_rec */ + &deleg_creds /* forwarded credentials */ + ); + + if (major != GSS_S_COMPLETE && major != GSS_S_CONTINUE_NEEDED) { + reply_gss_error(535, major, minor, "accepting context"); + if (ret_flags & GSS_C_DELEG_FLAG) + (void) gss_release_cred(&minor, &deleg_creds); + return; + } + + if (g_out_token.length) { + if (g_out_token.length >= ((MAXPATHLEN - sizeof("ADAT=")) + / 4 * 3)) { + reply(535, "ADAT: reply too long"); + /* syslog(LOG_ERR, "ADAT: reply too long"); + if (ret_flags & GSS_C_DELEG_FLAG) + (void) gss_release_cred(&minor, &deleg_creds); + return; + } + + base64_encode(g_out_token.value, g_out_token.length, g_buf, 1); + + if (major == GSS_S_COMPLETE) { + /* 2xx finished */ + reply(235, "ADAT=%s", g_buf); + } else { + /* 3xx require additional data */ + reply(335, "ADAT=%s", g_buf); + } + replied = 1; + (void) gss_release_buffer(&minor, &g_out_token); + } + if (major == GSS_S_COMPLETE) { + /* authentication succeeded */ + major = gss_display_name(&minor, client, &auth_gss_client_name, + &mech_type); + if (major != GSS_S_COMPLETE) { + /* rejecting security data ... reply with 535 */ + reply_gss_error(535, major, minor, + "extracting GSSAPI identity name"); + if (ret_flags & GSS_C_DELEG_FLAG) + (void) gss_release_cred(&minor, &deleg_creds); + return; + } + auth_type = auth_type_tmp; /* auth complete */ + auth_type_tmp = NULL; + + if (ret_flags & GSS_C_DELEG_FLAG) { + if (auth_want_creds) + ftpd_gss_convert_creds(auth_gss_client_name.value, deleg_creds); + (void) gss_release_cred(&minor, &deleg_creds); + } + + /* more data not required, so must reply 2xx */ + if (!replied) + { + /* this 235 not accurate but "correct" */ + if (ret_flags & GSS_C_DELEG_FLAG && !auth_have_creds) + reply(235, "GSSAPI Authentication succeeded; could not accept forwarded credentials"); + else + reply(235, "GSSAPI Authentication succeeded"); + } + + return; + } else { + if (!replied) + reply(335, "more data needed"); + if (ret_flags & GSS_C_DELEG_FLAG) + (void) gss_release_cred(&minor, &deleg_creds); + return; + } + } +#endif /* USE_GSSAPI */ + return; +} + diff -Naur tnftpd-20100324/src/tnftpd.manin tnftpd/src/tnftpd.manin --- tnftpd-20100324/src/tnftpd.manin 2010-01-03 17:10:43.000000000 -0800 +++ tnftpd/src/tnftpd.manin 2010-06-18 15:35:42.000000000 -0700 @@ -66,7 +66,7 @@ Internet File Transfer Protocol server .Sh SYNOPSIS .Nm -.Op Fl 46DdHlnQqrsUuWwX +.Op Fl 46DdHkKlnQqrsUuWwXzZ .Op Fl a Ar anondir .Op Fl C Ar user Ns Op @ Ns Ar host .Op Fl c Ar confdir @@ -178,6 +178,10 @@ Refer to .Xr inetd.conf 5 for more information on starting services to listen on specific IP addresses. +.It Fl k +Allow use of the CCC command. +.It Fl K +Require authenticated credentials. .It Fl L Ar xferlogfile Log .Tn wu-ftpd @@ -280,6 +284,10 @@ file suitable for input into a third-party log analysis tool with a command similar to: .Dl "sed -ne 's/^.*xferlog: //p' /var/log/xferlog \*[Gt] wuxferlog" +.It Fl z +Require authorization. +.It Fl Z +Require authentication. .El .Pp The file @@ -317,11 +325,15 @@ .It Sy Request Ta Sy Description .It ABOR Ta "abort previous command" .It ACCT Ta "specify account (ignored)" +.It ADAT Ta "supply authentication data" .It ALLO Ta "allocate storage (vacuously)" .It APPE Ta "append to a file" +.It AUTH Ta "select authentication paradigm" +.It CCC Ta "force cleartext commands" .It CDUP Ta "change to parent of current working directory" .It CWD Ta "change working directory" .It DELE Ta "delete a file" +.It ENC Ta "send privacy protected command" .It EPSV Ta "prepare for server-to-server transfer" .It EPRT Ta "specify data connection port" .It FEAT Ta "list extra features that are not defined in" Cm "RFC 959" @@ -329,6 +341,7 @@ .It LIST Ta "give list files in a directory" Pq Dq Li "ls -lA" .It LPSV Ta "prepare for server-to-server transfer" .It LPRT Ta "specify data connection port" +.It MIC Ta "send integrity protected command" .It MLSD Ta "list contents of directory in a machine-processable form" .It MLST Ta "show a pathname in a machine-processable form" .It MKD Ta "make a directory" @@ -339,7 +352,9 @@ .It OPTS Ta "define persistent options for a given command" .It PASS Ta "specify password" .It PASV Ta "prepare for server-to-server transfer" +.It PBSZ Ta "set protection buffer size" .It PORT Ta "specify data connection port" +.It PROT Ta "set data protection mode" .It PWD Ta "print the current working directory" .It QUIT Ta "terminate session" .It REST Ta "restart incomplete transfer" diff -Naur tnftpd-20100324/tnftpd.h tnftpd/tnftpd.h --- tnftpd-20100324/tnftpd.h 2010-06-18 15:33:24.000000000 -0700 +++ tnftpd/tnftpd.h 2010-06-21 18:17:41.000000000 -0700 @@ -1,9 +1,15 @@ /* $NetBSD: tnftpd.h,v 1.33 2009/11/07 09:59:09 lukem Exp $ */ -#define FTPD_VERSION PACKAGE_STRING - #include "tnftpd_config.h" +#if defined(USE_GSSAPI) +# warning "GSSAPI enabled!" +# define FTPD_VERSION PACKAGE_STRING "+GSSAPI" +#else /* !USE_GSSAPI */ +# warning "GSSAPI disabled." +# define FTPD_VERSION PACKAGE_STRING +#endif + #include #include #include diff -Naur tnftpd-20100324/tnftpd_config.h.in tnftpd/tnftpd_config.h.in --- tnftpd-20100324/tnftpd_config.h.in 2010-06-18 15:33:24.000000000 -0700 +++ tnftpd/tnftpd_config.h.in 2010-06-21 18:12:40.000000000 -0700 @@ -148,6 +148,9 @@ /* Define to 1 if you have the header file. */ #undef HAVE_GRP_H +/* Define to 1 if you have the header file. */ +#undef HAVE_GSSAPI_H + /* Define to 1 if you have the `inet_net_pton' function. */ #undef HAVE_INET_NET_PTON @@ -488,6 +491,9 @@ /* Define to 1 if your declares `struct tm'. */ #undef TM_IN_SYS_TIME +/* Define if using GSSAPI authentication. */ +#undef USE_GSSAPI + /* Define if using IPv6 support. */ #undef USE_INET6