krb5 commit: KfW auto-complete support

Benjamin Kaduk kaduk at MIT.EDU
Fri Aug 24 15:55:27 EDT 2012


https://github.com/krb5/krb5/commit/6201bbc23f1c307e6278af72eaa8e93dc898fedf
commit 6201bbc23f1c307e6278af72eaa8e93dc898fedf
Author: Kevin Wasserman <kevin.wasserman at painless-security.com>
Date:   Sat Jun 2 10:34:09 2012 -0400

    KfW auto-complete support
    
    Use the registry to store and retrieve principals for auto-complete.
    Remember principals from successful autentications.
    TODO: combine realm/username in principal; 'remember principal' checkbox;
    reset button; add to support 'change password' dialog as well.
    
    Signed-off-by: Kevin Wasserman <kevin.wasserman at painless-security.com>
    
    [kaduk at mit.edu: style cleanup, copyright/license on new file.]
    
    ticket: 7264 (new)
    queue: kfw
    target_version: 1.10.4
    tags: pullup

 src/windows/leashdll/Makefile.in |    1 +
 src/windows/leashdll/lsh_pwd.c   |   13 +
 src/windows/leashdll/lshutil.cpp |  586 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 600 insertions(+), 0 deletions(-)

diff --git a/src/windows/leashdll/Makefile.in b/src/windows/leashdll/Makefile.in
index 8a7eaa4..e443504 100644
--- a/src/windows/leashdll/Makefile.in
+++ b/src/windows/leashdll/Makefile.in
@@ -29,6 +29,7 @@ OBJS=	$(OUTPRE)AFSroutines.$(OBJEXT) \
 	$(OUTPRE)lsh_pwd.$(OBJEXT) \
 	$(OUTPRE)lshcallb.$(OBJEXT) \
 	$(OUTPRE)lshfunc.$(OBJEXT) \
+	$(OUTPRE)lshutil.$(OBJEXT) \
 	$(OUTPRE)timesync.$(OBJEXT) \
 	$(OUTPRE)winerr.$(OBJEXT) \
 	$(OUTPRE)winutil.$(OBJEXT) \
diff --git a/src/windows/leashdll/lsh_pwd.c b/src/windows/leashdll/lsh_pwd.c
index c7d8310..5ea59df 100644
--- a/src/windows/leashdll/lsh_pwd.c
+++ b/src/windows/leashdll/lsh_pwd.c
@@ -1458,6 +1458,9 @@ AdjustOptions(HWND hDialog, int show, int hideDiff)
 
 }
 
+extern void *lacInit(HWND hEditCtl);
+extern void lacTerm(void *pAutoComplete);
+extern void lacAddPrincipal(char *principal);
 
 /* Callback function for the Authentication Dialog box that initializes and
    renews tickets. */
@@ -1487,15 +1490,20 @@ AuthenticateProc(
     static HWND hSliderRenew=0;
     static RECT dlgRect;
     static int  hideDiff = 0;
+    static void *pAutoComplete = 0;
     char principal[256];
     long realm_count = 0;
     int disable_noaddresses = 0;
+    HWND hEditCtrl=0;
 
     switch (message) {
 
     case WM_INITDIALOG:
 	hDlg = hDialog;
 
+        hEditCtrl = GetDlgItem(hDialog, IDC_EDIT_PRINCIPAL);
+        if (hEditCtrl)
+            pAutoComplete = lacInit(hEditCtrl);
         SetVersionInfo(hDialog,IDC_STATIC_VERSION,IDC_STATIC_COPYRIGHT);
 	hSliderLifetime = GetDlgItem(hDialog, IDC_STATIC_LIFETIME_VALUE);
 	hSliderRenew = GetDlgItem(hDialog, IDC_STATIC_RENEW_TILL_VALUE);
@@ -1828,6 +1836,10 @@ AuthenticateProc(
 		CleanupSliders();
 		memset(password,0,sizeof(password));
 		RemoveProp(hDialog, "HANDLES_HELP");
+        if (pAutoComplete) {
+            lacTerm(pAutoComplete);
+            pAutoComplete = NULL;
+        }
 		EndDialog(hDialog, (int)lParam);
                 return TRUE;
 	    }
@@ -1957,6 +1969,7 @@ AuthenticateProc(
                     strncpy(lpdi->out.realm, realm, LEASH_REALM_SZ);
                     lpdi->out.realm[LEASH_REALM_SZ-1] = 0;
                 }
+                lacAddPrincipal(username);
 
                 CloseMe(TRUE); /* success */
                 return FALSE;
diff --git a/src/windows/leashdll/lshutil.cpp b/src/windows/leashdll/lshutil.cpp
new file mode 100644
index 0000000..2061e93
--- /dev/null
+++ b/src/windows/leashdll/lshutil.cpp
@@ -0,0 +1,586 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* prototype/prototype.c - <<< One-line description of file >>> */
+/* leashdll/lshutil.cpp - text maniuplation for principal entry */
+/*
+ * Copyright (C) 2012 by the Massachusetts Institute of Technology.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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
+ * COPYRIGHT HOLDER 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.
+ */
+
+/*
+ *
+ * Leash Principal Edit Control
+ *
+ * Edit control customized to enter a principal.
+ * -Autocomplete functionality using history of previously successful
+ *  authentications
+ * -Option to automatically convert realm to uppercase as user types
+ * -Suggest default realm when no matches available from history
+ */
+
+#include <windows.h>
+#include <wtypes.h>  // LPOLESTR
+#include <Shldisp.h> // IAutoComplete
+#include <ShlGuid.h> // CLSID_AutoComplete
+#include <shobjidl.h> // IAutoCompleteDropDown
+#include <objbase.h> // CoCreateInstance
+#include <tchar.h>
+#include <map>
+#include <vector>
+
+#include "leashwin.h"
+#include "leashdll.h"
+
+#pragma comment(lib, "ole32.lib") // CoCreateInstance
+
+//
+// DynEnumString:
+// IEnumString implementation that can be dynamically updated after creation.
+//
+class DynEnumString : public IEnumString
+{
+public:
+    // IUnknown
+    STDMETHODIMP_(ULONG) AddRef()
+    {
+        return ++m_refcount;
+    }
+
+    STDMETHODIMP_(ULONG) Release()
+    {
+        ULONG refcount = --m_refcount;
+        if (refcount == 0)
+            delete this;
+        return refcount;
+    }
+
+    STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject)
+    {
+        IUnknown *punk = NULL;
+        if (riid == IID_IUnknown)
+            punk = static_cast<IUnknown*>(this);
+        else if (riid == IID_IEnumString)
+            punk = static_cast<IEnumString*>(this);
+        *ppvObject = punk;
+        if (punk == NULL)
+            return E_NOINTERFACE;
+        punk->AddRef();
+        return S_OK;
+    }
+
+    // IEnumString
+public:
+    STDMETHODIMP Clone(IEnumString **ppclone)
+    {
+        LPTSTR *data = m_aStrings.data();
+        ULONG count = m_aStrings.size();
+        *ppclone = new DynEnumString(count, data);
+        return S_OK;
+    }
+
+    STDMETHODIMP Next(ULONG count, LPOLESTR *elements, ULONG *pFetched)
+    {
+        ULONG fetched = 0;
+        while (fetched < count) {
+            if (m_iter == m_aStrings.end())
+                break;
+            LPTSTR src = *m_iter++;
+            // @TODO: add _UNICODE version
+            DWORD nLengthW = ::MultiByteToWideChar(CP_ACP,
+                                                   0, &src[0], -1, NULL, 0);
+            LPOLESTR copy =
+                (LPOLESTR )::CoTaskMemAlloc(sizeof(OLECHAR) * nLengthW);
+            if (copy != NULL) {
+                if (::MultiByteToWideChar(CP_ACP,
+                                          0, &src[0], -1, copy, nLengthW)) {
+                    elements[fetched++] = copy;
+                } else {
+                    // failure...
+                    // TODO: debug spew
+                    ::CoTaskMemFree(copy);
+                    copy = NULL;
+                }
+            }
+        }
+        *pFetched = fetched;
+
+        return fetched == count ? S_OK : S_FALSE;
+    }
+
+    STDMETHODIMP Reset()
+    {
+        m_iter = m_aStrings.begin();
+        return S_OK;
+    }
+
+    STDMETHODIMP Skip(ULONG count)
+    {
+        for (ULONG i=0; i<count; i++) {
+            if (m_iter == m_aStrings.end()) {
+                m_iter = m_aStrings.begin();
+                break;
+            }
+            m_iter++;
+        }
+        return S_OK;
+    }
+
+    // Custom interface
+    DynEnumString(ULONG count, LPTSTR *strings)
+    {
+        m_aStrings.reserve(count + 1);
+        for (ULONG i = 0; i < count; i++) {
+            AddString(strings[i]);
+        }
+        m_iter = m_aStrings.begin();
+        m_refcount = 1;
+    }
+
+    virtual ~DynEnumString()
+    {
+        for (m_iter = m_aStrings.begin();
+             m_iter != m_aStrings.end();
+             m_iter++)
+            delete[] (*m_iter);
+        m_aStrings.erase(m_aStrings.begin(), m_aStrings.end());
+    }
+
+    void AddString(LPTSTR str)
+    {
+        LPTSTR copy = NULL;
+        if (str) {
+            copy = _tcsdup(str);
+            if (copy)
+                m_aStrings.push_back(copy);
+        }
+    }
+
+
+    void RemoveString(LPTSTR str)
+    {
+        std::vector<LPTSTR>::const_iterator i;
+        for (i = m_aStrings.begin(); i != m_aStrings.end(); i++) {
+            if (_tcscmp(*i, str) == 0) {
+                delete[] (*i);
+                m_aStrings.erase(i);
+                break;
+            }
+        }
+    }
+
+private:
+    ULONG m_refcount;
+    std::vector<LPTSTR>::iterator m_iter;
+    std::vector<LPTSTR> m_aStrings;
+};
+
+// Registry key to store history of successfully authenticated principals
+#define LEASH_REGISTRY_PRINCIPALS_KEY_NAME "Software\\MIT\\Leash\\Principals"
+
+// Free principal list obtained by getPrincipalList()
+static void freePrincipalList(LPTSTR *princs, int count)
+{
+    int i;
+    if (count) {
+        for (i = 0; i < count; i++)
+            if (princs[i])
+                free(princs[i]);
+        delete[] princs;
+    }
+}
+
+// Retrieve history of successfully authenticated principals from registry
+static void getPrincipalList(LPTSTR **outPrincs, int *outPrincCount)
+{
+    DWORD count = 0;
+    DWORD valCount = 0;
+    DWORD maxLen = 0;
+    LPTSTR tempValName = NULL;
+    LPTSTR *princs = NULL;
+    *outPrincs = NULL;
+    HKEY hKey = NULL;
+    unsigned long rc = RegCreateKeyEx(HKEY_CURRENT_USER,
+                                      LEASH_REGISTRY_PRINCIPALS_KEY_NAME, 0, 0,
+                                      0, KEY_READ, 0, &hKey, 0);
+    if (rc == S_OK) {
+        // get string count
+        rc = RegQueryInfoKey(
+            hKey,
+            NULL, // __out_opt    LPTSTR lpClass,
+            NULL, // __inout_opt  LPDWORD lpcClass,
+            NULL, // __reserved   LPDWORD lpReserved,
+            NULL, // __out_opt    LPDWORD lpcSubKeys,
+            NULL, // __out_opt    LPDWORD lpcMaxSubKeyLen,
+            NULL, // __out_opt    LPDWORD lpcMaxClassLen,
+            &valCount, //__out_opt    LPDWORD lpcValues,
+            &maxLen, // __out_opt    LPDWORD lpcMaxValueNameLen,
+            NULL, // __out_opt    LPDWORD lpcMaxValueLen,
+            NULL, // __out_opt    LPDWORD lpcbSecurityDescriptor,
+            NULL  // __out_opt    PFILETIME lpftLastWriteTime
+        );
+    }
+    if (valCount == 0)
+        goto cleanup;
+
+    princs = new LPTSTR[valCount];
+    if (princs == NULL)
+        goto cleanup;
+
+    tempValName = new TCHAR[maxLen+1];
+    if (tempValName == NULL)
+        goto cleanup;
+
+    // enumerate values...
+    for (DWORD iReg = 0; iReg < valCount; iReg++) {
+        LPTSTR princ = NULL;
+        DWORD size = maxLen+1;
+        rc = RegEnumValue(hKey, iReg, tempValName, &size,
+                          NULL, NULL, NULL, NULL);
+        if (rc == ERROR_SUCCESS)
+            princ = _tcsdup(tempValName);
+        if (princ != NULL)
+            princs[count++] = princ;
+    }
+
+    *outPrincCount = count;
+    count = 0;
+    *outPrincs = princs;
+    princs = NULL;
+
+cleanup:
+    if (tempValName)
+        delete[] tempValName;
+    if (princs)
+        freePrincipalList(princs, count);
+    if (hKey)
+        RegCloseKey(hKey);
+    return;
+}
+
+
+// HookWindow
+// Utility class to process messages relating to the specified hwnd
+class HookWindow
+{
+public:
+    typedef std::pair<HWND, HookWindow*> map_elem;
+    typedef std::map<HWND, HookWindow*> map;
+
+    HookWindow(HWND in_hwnd) : m_hwnd(in_hwnd)
+    {
+        // add 'this' to static hash
+        m_ctrl_id = GetDlgCtrlID(in_hwnd);
+        m_parent = ::GetParent(m_hwnd);
+        sm_map.insert(map_elem(m_parent, this));
+        // grab current window proc and replace with our wndproc
+        m_parent_wndproc = SetWindowLongPtr(m_parent,
+                                            GWLP_WNDPROC,
+                                            (ULONG_PTR)(&sWindowProc));
+    }
+
+    virtual ~HookWindow()
+    {
+        // unhook hwnd and restore old wndproc
+        SetWindowLongPtr(m_parent, GWLP_WNDPROC, m_parent_wndproc);
+        sm_map.erase(m_parent);
+    }
+
+    // Process a message
+    // return 'false' to forward message to parent wndproc
+    virtual bool WindowProc(UINT msg, WPARAM wParam, LPARAM lParam,
+                            LRESULT *lr) = 0;
+
+protected:
+    static LRESULT sWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam,
+                               LPARAM lParam);
+
+    HWND m_hwnd;
+    HWND m_parent;
+    ULONG_PTR m_parent_wndproc;
+    int m_ctrl_id;
+
+    static map sm_map;
+};
+
+HookWindow::map HookWindow::sm_map;
+
+LRESULT HookWindow::sWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam,
+                                LPARAM lParam)
+{
+    LRESULT result;
+    // hash hwnd to get object and call actual window proc,
+    // then parent window proc as necessary
+    HookWindow::map::const_iterator iter = sm_map.find(hwnd);
+    if (iter != sm_map.end()) {
+        if (!iter->second->WindowProc(uMsg, wParam, lParam, &result))
+            result = CallWindowProc((WNDPROC )iter->second->m_parent_wndproc,
+                           hwnd, uMsg, wParam, lParam);
+    } else {
+        result = ::DefWindowProc(hwnd, uMsg, wParam, lParam);
+    }
+    return result;
+}
+
+//
+class PrincipalEditControl : public HookWindow
+{
+public:
+    PrincipalEditControl(HWND hwnd, bool bUpperCaseRealm) : HookWindow(hwnd)
+        ,m_ignore_change(0)
+        ,m_bUpperCaseRealm(bUpperCaseRealm)
+        ,m_defaultRealm(NULL)
+        ,m_ctx(0)
+        ,m_acdd(NULL)
+        ,m_princStr(NULL)
+    {
+        pkrb5_init_context(&m_ctx);
+        GetDefaultRealm();
+        InitAutocomplete();
+    }
+
+    ~PrincipalEditControl()
+    {
+        DestroyAutocomplete();
+        if (m_princStr)
+            delete[] m_princStr;
+        if (m_ctx && m_defaultRealm)
+            pkrb5_free_default_realm(m_ctx, m_defaultRealm);
+        if (m_ctx)
+            pkrb5_free_context(m_ctx);
+    }
+
+protected:
+    // Convert str to upper case
+    // This should be more-or-less _UNICODE-agnostic
+    static bool StrToUpper(LPTSTR str)
+    {
+        bool bChanged = false;
+        int c;
+        if (str != NULL) {
+            while ((c = *str) != NULL) {
+                if (__isascii(c) && islower(c)) {
+                    bChanged = true;
+                    *str = _toupper(c);
+                }
+                str++;
+            }
+        }
+        return bChanged;
+    }
+
+    void GetDefaultRealm()
+    {
+        // @TODO: _UNICODE support here
+        if ((m_defaultRealm == NULL) && m_ctx) {
+            pkrb5_get_default_realm(m_ctx, &m_defaultRealm);
+        }
+    }
+
+    // Append default realm to user and add to the autocomplete enum string
+    void SuggestDefaultRealm(LPTSTR user)
+    {
+        if (m_defaultRealm == NULL)
+            return;
+
+        int princ_len = _tcslen(user) + _tcslen(m_defaultRealm) + 1;
+        LPTSTR princStr = new TCHAR[princ_len];
+        if (princStr) {
+            _sntprintf_s(princStr, princ_len, _TRUNCATE, "%s%s", user,
+                         m_defaultRealm);
+            if (m_princStr != NULL && (_tcscmp(princStr, m_princStr) == 0)) {
+                // this string is already added, ok to just bail
+                delete[] princStr;
+            } else {
+                if (m_princStr != NULL) {
+                    // get rid of the old suggestion
+                    m_enumString->RemoveString(m_princStr);
+                    delete[] m_princStr;
+                }
+                // add the new one
+                m_enumString->AddString(princStr);
+                m_acdd->ResetEnumerator();
+                m_princStr = princStr;
+            }
+        }
+    }
+
+    bool AdjustRealmCase(LPTSTR princStr, LPTSTR realmStr)
+    {
+        bool bChanged = StrToUpper(realmStr);
+        if (bChanged) {
+            DWORD selStart, selEnd;
+            ::SendMessage(m_hwnd, EM_GETSEL, (WPARAM)&selStart,
+                          (LPARAM)&selEnd);
+            ::SetWindowText(m_hwnd, princStr);
+            ::SendMessage(m_hwnd, EM_SETSEL, (WPARAM)selStart, (LPARAM)selEnd);
+        }
+        return bChanged;
+    }
+
+    bool ProcessText()
+    {
+        bool bChanged = false;
+        int text_len = GetWindowTextLength(m_hwnd);
+        if (text_len > 0) {
+            LPTSTR str = new TCHAR [++text_len];
+            if (str != NULL) {
+                GetWindowText(m_hwnd, str, text_len);
+                LPTSTR realmStr = strchr(str, '@');
+                if (realmStr != NULL) {
+                    ++realmStr;
+                    if (*realmStr == 0) {
+                        SuggestDefaultRealm(str);
+                    }
+                    else if (m_bUpperCaseRealm) {
+                        AdjustRealmCase(str, realmStr);
+                        bChanged = true;
+                    }
+                }
+                delete[] str;
+            }
+        }
+        return bChanged;
+    }
+
+    virtual bool WindowProc(UINT msg, WPARAM wp, LPARAM lp, LRESULT *lr)
+    {
+        bool bChanged = false;
+        switch (msg) {
+        case WM_COMMAND:
+            if ((LOWORD(wp)==m_ctrl_id) &&
+                (HIWORD(wp)==EN_CHANGE)) {
+                if ((!m_ignore_change++) && ProcessText()) {
+                    bChanged = true;
+                    *lr = 0;
+                }
+                m_ignore_change--;
+            }
+        default:
+            break;
+        }
+        return bChanged;
+    }
+
+    void InitAutocomplete()
+    {
+        CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
+
+        // read strings from registry
+        LPTSTR *princs = NULL;
+        int count = 0;
+        getPrincipalList(&princs, &count);
+
+        // Create our custom IEnumString implementation
+        HRESULT hRes;
+        DynEnumString *pEnumString = new DynEnumString(count, princs);
+        if (princs)
+            freePrincipalList(princs, count);
+
+        m_enumString = pEnumString;
+
+        // Create and initialize IAutoComplete object using IEnumString
+        IAutoComplete *pac = NULL;
+        hRes = CoCreateInstance(CLSID_AutoComplete, NULL, CLSCTX_INPROC_SERVER,
+                                IID_PPV_ARGS(&pac));
+        // @TODO: error handling
+
+        pac->Init(m_hwnd, pEnumString, NULL, NULL);
+
+        IAutoCompleteDropDown* pacdd = NULL;
+        hRes = pac->QueryInterface(IID_IAutoCompleteDropDown, (LPVOID*)&pacdd);
+        pac->Release();
+
+        // @TODO: auto-suggest; other advanced options?
+#if 0
+        IAutoComplete2 *pac2;
+
+        if (SUCCEEDED(pac->QueryInterface(IID_IAutoComplete2, (LPVOID*)&pac2)))
+        {
+            pac2->SetOptions(ACO_AUTOSUGGEST);
+            pac2->Release();
+        }
+#endif
+        m_acdd = pacdd;
+    }
+
+    void DestroyAutocomplete()
+    {
+        if (m_acdd != NULL)
+            m_acdd->Release();
+        if (m_enumString != NULL)
+            m_enumString->Release();
+    }
+
+    int m_ignore_change;
+    bool m_bUpperCaseRealm;
+    LPTSTR m_defaultRealm;
+    LPTSTR m_princStr;
+    krb5_context m_ctx;
+    DynEnumString *m_enumString;
+    IAutoCompleteDropDown *m_acdd;
+};
+
+
+
+extern "C" void lacAddPrincipal(char *principal)
+{
+    // write princ to registry
+    HKEY hKey;
+    unsigned long rc = RegCreateKeyEx(HKEY_CURRENT_USER,
+                                      LEASH_REGISTRY_PRINCIPALS_KEY_NAME,
+                                      0, 0, 0, KEY_WRITE, 0, &hKey, 0);
+    if (rc) {
+        // TODO: log failure
+        return;
+    }
+    rc = RegSetValueEx(hKey, principal, 0, REG_NONE, NULL, 0);
+    if (rc) {
+        // TODO: log failure
+    }
+    if (hKey)
+        RegCloseKey(hKey);
+}
+
+extern "C" void lacReset()
+{
+    // clear princs from registry
+    //RegDeleteKeyEx()
+}
+
+
+extern "C" void *lacInit(HWND hEdit)
+{
+    return new PrincipalEditControl(
+        hEdit,
+        Leash_get_default_uppercaserealm() ? true : false);
+}
+
+extern "C" void lacTerm(void *pHook)
+{
+    if (pHook != NULL)
+        delete ((PrincipalEditControl *)pHook);
+}


More information about the cvs-krb5 mailing list