]> arthur.barton.de Git - netatalk.git/blob - etc/uams/uams_gss.c
More debugging
[netatalk.git] / etc / uams / uams_gss.c
1 /*
2  * $Id: uams_gss.c,v 1.9 2009-10-15 14:54:43 franklahm Exp $
3  *
4  * Copyright (c) 1990,1993 Regents of The University of Michigan.
5  * Copyright (c) 1999 Adrian Sun (asun@u.washington.edu)
6  * Copyright (c) 2003 The Reed Institute
7  * Copyright (c) 2004 Bjoern Fernhomberg
8  * All Rights Reserved.  See COPYRIGHT.
9  */
10
11 #ifdef HAVE_CONFIG_H
12 #include "config.h"
13 #endif /* HAVE_CONFIG_H */
14
15 #include <stdio.h>
16 #include <stdlib.h>
17 #ifdef HAVE_UNISTD_H
18 #include <unistd.h>
19 #endif /* HAVE_UNISTD_H */
20
21 /* STDC check */
22 #if STDC_HEADERS
23 #include <string.h>
24 #else /* STDC_HEADERS */
25 #ifndef HAVE_STRCHR
26 #define strchr index
27 #define strrchr index
28 #endif /* HAVE_STRCHR */
29 char *strchr (), *strrchr ();
30 #ifndef HAVE_MEMCPY
31 #define memcpy(d,s,n) bcopy ((s), (d), (n))
32 #define memmove(d,s,n) bcopy ((s), (d), (n))
33 #endif /* ! HAVE_MEMCPY */
34 #endif /* STDC_HEADERS */
35
36 #include <errno.h>
37 #include <atalk/logger.h>
38 #include <atalk/afp.h>
39 #include <atalk/uam.h>
40 #include <atalk/util.h>
41
42 /* Kerberos includes */
43
44 #if HAVE_GSSAPI_H
45 #include <gssapi.h>
46 #endif
47
48 #if HAVE_GSSAPI_GSSAPI_H
49 #include <gssapi/gssapi.h>
50 #endif
51
52 #if HAVE_GSSAPI_GSSAPI_GENERIC_H
53 #include <gssapi/gssapi_generic.h>
54 #endif
55
56 #if HAVE_GSSAPI_GSSAPI_KRB5_H
57 #include <gssapi/gssapi_krb5.h>
58 #endif
59
60 #if HAVE_COM_ERR_H
61 #include <com_err.h>
62 #endif
63
64 /* We work around something I don't entirely understand... */
65 /* BF: This is a Heimdal/MIT compatibility fix */
66 #ifndef HAVE_GSS_C_NT_HOSTBASED_SERVICE
67 #define GSS_C_NT_HOSTBASED_SERVICE gss_nt_service_name
68 #endif
69
70 #ifdef MIN
71 #undef MIN
72 #endif
73
74 #define MIN(a, b) ((a > b) ? b : a)
75
76 static void log_status( char *s, OM_uint32 major_status,
77                         OM_uint32 minor_status )
78 {
79     gss_buffer_desc msg = GSS_C_EMPTY_BUFFER;
80     OM_uint32 min_status, maj_status;
81     OM_uint32 maj_ctx = 0, min_ctx = 0;
82
83     while (1) {
84         maj_status = gss_display_status( &min_status, major_status,
85                                          GSS_C_GSS_CODE, GSS_C_NULL_OID,
86                                          &maj_ctx, &msg );
87         LOG(log_info, logtype_uams, "uams_gss.c :do_gss_auth: %s %.*s (error %s)", s,
88             (int)msg.length, msg.value, strerror(errno));
89         gss_release_buffer(&min_status, &msg);
90
91         if (!maj_ctx)
92             break;
93     }
94     while (1) {
95         maj_status = gss_display_status( &min_status, minor_status,
96                                          GSS_C_MECH_CODE, GSS_C_NULL_OID, // gss_mech_krb5,
97                                          &min_ctx, &msg );
98         LOG(log_info, logtype_uams, "uams_gss.c :do_gss_auth: %s %.*s (error %s)", s,
99             (int)msg.length, msg.value, strerror(errno));
100         gss_release_buffer(&min_status, &msg);
101
102         if (!min_ctx)
103             break;
104     }
105 }
106
107
108 static void log_ctx_flags( OM_uint32 flags )
109 {
110 #ifdef DEBUG1
111     if (flags & GSS_C_DELEG_FLAG)
112         LOG(log_debug, logtype_uams, "uams_gss.c :context flag: GSS_C_DELEG_FLAG" );
113     if (flags & GSS_C_MUTUAL_FLAG)
114         LOG(log_debug, logtype_uams, "uams_gss.c :context flag: GSS_C_MUTUAL_FLAG" );
115     if (flags & GSS_C_REPLAY_FLAG)
116         LOG(log_debug, logtype_uams, "uams_gss.c :context flag: GSS_C_REPLAY_FLAG" );
117     if (flags & GSS_C_SEQUENCE_FLAG)
118         LOG(log_debug, logtype_uams, "uams_gss.c :context flag: GSS_C_SEQUENCE_FLAG" );
119     if (flags & GSS_C_CONF_FLAG)
120         LOG(log_debug, logtype_uams, "uams_gss.c :context flag: GSS_C_CONF_FLAG" );
121     if (flags & GSS_C_INTEG_FLAG)
122         LOG(log_debug, logtype_uams, "uams_gss.c :context flag: GSS_C_INTEG_FLAG" );
123 #endif
124 }
125
126 static void log_principal(gss_name_t server_name)
127 {
128 #ifdef DEBUG1
129     OM_uint32 major_status = 0, minor_status = 0;
130     gss_buffer_desc exported_name;
131
132     /* Only for debugging purposes, check the gssapi internal representation */
133     major_status = gss_export_name(&minor_status, server_name, &exported_name);
134     LOG(log_debug, logtype_uams, "log_principal: exported server name is %s", (char*) exported_name.value);
135     gss_release_buffer( &minor_status, &exported_name );
136 #endif
137 }
138
139 /* get the principal from afpd and import it into server_name */
140 static int get_afpd_principal(void *obj, gss_name_t *server_name)
141 {
142     OM_uint32 major_status = 0, minor_status = 0;
143     char *realm, *fqdn, *service, *principal, *p;
144     size_t realmlen=0, fqdnlen=0, servicelen=0;
145     size_t principal_length;
146     gss_buffer_desc s_princ_buffer;
147
148     /* get all the required information from afpd */
149     if (uam_afpserver_option(obj, UAM_OPTION_KRB5REALM, (void*) &realm, &realmlen) < 0)
150         return 1;
151     LOG(log_debug, logtype_uams, "get_afpd_principal: REALM: %s", realm);
152
153     if (uam_afpserver_option(obj, UAM_OPTION_FQDN, (void*) &fqdn, &fqdnlen) < 0)
154         return 1;
155     LOG(log_debug, logtype_uams, "get_afpd_principal: fqdn: %s", fqdn);
156
157     if (uam_afpserver_option(obj, UAM_OPTION_KRB5SERVICE, (void *)&service, &servicelen) < 0)
158         return 1;
159     LOG(log_debug, logtype_uams, "get_afpd_principal: service: %s", service);
160
161     /* we need all the info, log error and return if one's missing */
162     if (!service || !servicelen || !fqdn || !fqdnlen || !realm || !realmlen) {
163         LOG(log_error, logtype_uams,
164             "get_afpd_principal: could not retrieve required information from afpd.");
165         return 1;
166     }
167
168     /* allocate memory to hold the temporary principal string */
169     principal_length = servicelen + 1 + fqdnlen + 1 + realmlen + 1;
170     if ( NULL == (principal = (char*) malloc( principal_length)) ) {
171         LOG(log_error, logtype_uams,
172             "get_afpd_principal: out of memory allocating %u bytes",
173             principal_length);
174         return 1;
175     }
176
177     /*
178      * Build the principal string.
179      * Format: 'service/fqdn@realm'
180      */
181     strlcpy( principal, service, principal_length);
182     strlcat( principal, "/", principal_length);
183
184     /*
185      * The fqdn we get from afpd may contain a port.
186      * We need to strip the port from fqdn for principal.
187      */
188     p = strchr(fqdn, ':');
189     if (p)
190         *p = '\0';
191     strlcat( principal, fqdn, principal_length);
192     if (p)
193         *p = ':';
194     strlcat( principal, "@", principal_length);
195     strlcat( principal, realm, principal_length);
196
197     /*
198      * Import our principal into the gssapi internal representation
199      * stored in server_name.
200      */
201     s_princ_buffer.value = principal;
202     s_princ_buffer.length = strlen( principal ) + 1;
203
204     LOG(log_debug, logtype_uams, "get_afpd_principal: importing principal `%s'", principal);
205     major_status = gss_import_name( &minor_status,
206                                     &s_princ_buffer,
207                                     GSS_C_NO_OID,
208                                     server_name );
209
210     /*
211      * Get rid of malloc'ed memmory.
212      * Don't release the s_princ_buffer, we free principal instead.
213      */
214     free(principal);
215
216     if (major_status != GSS_S_COMPLETE) {
217         /* Importing our service principal failed, bail out. */
218         log_status( "import_principal", major_status, minor_status );
219         return 1;
220     }
221     return 0;
222 }
223
224
225 /* get the username */
226 static int get_client_username(char *username, int ulen, gss_name_t *client_name)
227 {
228     OM_uint32 major_status = 0, minor_status = 0;
229     gss_buffer_desc client_name_buffer;
230     char *p;
231     int namelen, ret=0;
232
233     /*
234      * To extract the unix username, use gss_display_name on client_name.
235      * We do rely on gss_display_name returning a zero terminated string.
236      * The username returned contains the realm and possibly an instance.
237      * We only want the username for afpd, so we have to strip those from
238      * the username before copying it to afpd's buffer.
239      */
240
241     major_status = gss_display_name( &minor_status, *client_name,
242                                      &client_name_buffer, (gss_OID *)NULL );
243     if (major_status != GSS_S_COMPLETE) {
244         log_status( "display_name", major_status, minor_status );
245         return 1;
246     }
247
248     LOG(log_debug, logtype_uams, "get_client_username: user is `%s'", client_name_buffer.value);
249
250     /* chop off realm */
251     p = strchr( client_name_buffer.value, '@' );
252     if (p)
253         *p = 0;
254     /* FIXME: chop off instance? */
255     p = strchr( client_name_buffer.value, '/' );
256     if (p)
257         *p = 0;
258
259     /* check if this username fits into afpd's username buffer */
260     namelen = strlen(client_name_buffer.value);
261     if ( namelen >= ulen ) {
262         /* The username is too long for afpd's buffer, bail out */
263         LOG(log_error, logtype_uams,
264             "get_client_username: username `%s' too long", client_name_buffer.value);
265         ret = 1;
266     }
267     else {
268         /* copy stripped username to afpd's buffer */
269         strlcpy(username, client_name_buffer.value, ulen);
270     }
271
272     /* we're done with client_name_buffer, release it */
273     gss_release_buffer(&minor_status, &client_name_buffer );
274
275     return ret;
276 }
277
278 /* wrap afpd's sessionkey */
279 static int wrap_sessionkey(gss_ctx_id_t context, struct session_info *sinfo)
280 {
281     OM_uint32 status = 0;
282     int ret=0;
283     gss_buffer_desc sesskey_buff, wrap_buff;
284
285     /*
286      * gss_wrap afpd's session_key.
287      * This is needed fo OS X 10.3 clients. They request this information
288      * with type 8 (kGetKerberosSessionKey) on FPGetSession.
289      * See AFP 3.1 specs, page 77.
290      */
291
292     sesskey_buff.value = sinfo->sessionkey;
293     sesskey_buff.length = sinfo->sessionkey_len;
294
295     /* gss_wrap the session key with the default machanism.
296        Require both confidentiality and integrity services */
297     gss_wrap (&status, context, 1, GSS_C_QOP_DEFAULT, &sesskey_buff, NULL, &wrap_buff);
298
299     if ( status != GSS_S_COMPLETE) {
300         LOG(log_error, logtype_uams, "wrap_sessionkey: failed to gss_wrap sessionkey");
301         log_status( "GSS wrap", 0, status );
302         return 1;
303     }
304
305     /* store the wrapped session key in afpd's session_info struct */
306     if ( NULL == (sinfo->cryptedkey = malloc ( wrap_buff.length )) ) {
307         LOG(log_error, logtype_uams,
308             "wrap_sessionkey: out of memory tyring to allocate %u bytes",
309             wrap_buff.length);
310         ret = 1;
311     } else {
312         /* cryptedkey is binary data */
313         memcpy (sinfo->cryptedkey, wrap_buff.value, wrap_buff.length);
314         sinfo->cryptedkey_len = wrap_buff.length;
315     }
316
317     /* we're done with buffer, release */
318     gss_release_buffer( &status, &wrap_buff );
319
320     return ret;
321 }
322
323 /*-------------*/
324 static int acquire_credentials (gss_name_t *server_name, gss_cred_id_t *server_creds)
325 {
326     OM_uint32 major_status = 0, minor_status = 0;
327     char *envp;
328
329     if (envp = getenv("KRB5_KTNAME"))
330         LOG(log_debug, logtype_uams,
331             "acquire credentials: acquiring credentials (uid = %d, keytab = %s)",
332             (int)geteuid(), envp);
333     else
334         LOG(log_debug, logtype_uams,
335             "acquire credentials: acquiring credentials (uid = %d) - $KRB5_KTNAME not found in env",
336             (int)geteuid());
337         
338     /*
339      * Acquire credentials usable for accepting context negotiations.
340      * Credentials are for server_name, have an indefinite lifetime,
341      * have no specific mechanisms, are to be used for accepting context
342      * negotiations and are to be placed in server_creds.
343      * We don't care about the mechanisms or about the time for which they are valid.
344      */
345     major_status = gss_acquire_cred( &minor_status, *server_name,
346                                      GSS_C_INDEFINITE, GSS_C_NO_OID_SET, GSS_C_ACCEPT,
347                                      server_creds, NULL, NULL );
348
349     if (major_status != GSS_S_COMPLETE) {
350         log_status( "acquire_cred", major_status, minor_status );
351         return 1;
352     }
353
354     return 0;
355 }
356
357 /*-------------*/
358 static int accept_sec_context (gss_ctx_id_t *context, gss_cred_id_t server_creds,
359                                gss_buffer_desc *ticket_buffer, gss_name_t *client_name,
360                                gss_buffer_desc *authenticator_buff)
361 {
362     OM_uint32 major_status = 0, minor_status = 0, ret_flags;
363
364     /* Initialize autheticator buffer. */
365     authenticator_buff->length = 0;
366     authenticator_buff->value = NULL;
367
368     LOG(log_debug, logtype_uams, "accept_context: accepting context (ticketlen: %u)",
369         ticket_buffer->length);
370
371     /*
372      * Try to accept the secondary context using the tocken in ticket_buffer.
373      * We don't care about the mechanisms used, nor for the time.
374      * We don't act as a proxy either.
375      */
376     major_status = gss_accept_sec_context( &minor_status, context,
377                                            server_creds, ticket_buffer, GSS_C_NO_CHANNEL_BINDINGS,
378                                            client_name, NULL, authenticator_buff,
379                                            &ret_flags, NULL, NULL );
380
381     if (major_status != GSS_S_COMPLETE) {
382         log_status( "accept_sec_context", major_status, minor_status );
383         return 1;
384     }
385     log_ctx_flags( ret_flags );
386     return 0;
387 }
388
389
390 /* return 0 on success */
391 static int do_gss_auth(void *obj, char *ibuf, int ticket_len,
392                        char *rbuf, int *rbuflen, char *username, int ulen,
393                        struct session_info *sinfo )
394 {
395     OM_uint32 status = 0;
396     gss_name_t server_name, client_name;
397     gss_cred_id_t server_creds;
398     gss_ctx_id_t context_handle = GSS_C_NO_CONTEXT;
399     gss_buffer_desc ticket_buffer, authenticator_buff;
400     int ret = 0;
401
402     /* import our principal name from afpd */
403     if (get_afpd_principal(obj, &server_name) != 0) {
404         return 1;
405     }
406     log_principal(server_name);
407
408     /* Now we have to acquire our credentials */
409     if ((ret = acquire_credentials (&server_name, &server_creds)))
410         goto cleanup_vars;
411
412     /*
413      * Try to accept the secondary context, using the ticket/token the
414      * client sent us. Ticket is stored at current ibuf position.
415      * Don't try to release ticket_buffer later, it points into ibuf!
416      */
417     ticket_buffer.length = ticket_len;
418     ticket_buffer.value = ibuf;
419
420     ret = accept_sec_context (&context_handle, server_creds, &ticket_buffer,
421                               &client_name, &authenticator_buff);
422
423     if (!ret) {
424         /* We succesfully acquired the secondary context, now get the
425            username for afpd and gss_wrap the sessionkey */
426         if ( 0 == (ret = get_client_username(username, ulen, &client_name)) ) {
427             ret = wrap_sessionkey(context_handle, sinfo);
428         }
429
430         if (!ret) {
431             /* FIXME: Is copying the authenticator really necessary?
432                Where is this documented? */
433             u_int16_t auth_len = htons( authenticator_buff.length );
434
435             /* copy the authenticator length into the reply buffer */
436             memcpy( rbuf, &auth_len, sizeof(auth_len) );
437             *rbuflen += sizeof(auth_len);
438             rbuf += sizeof(auth_len);
439
440             /* copy the authenticator value into the reply buffer */
441             memcpy( rbuf, authenticator_buff.value, authenticator_buff.length );
442             *rbuflen += authenticator_buff.length;
443         }
444
445         /* Clean up after ourselves */
446         gss_release_name( &status, &client_name );
447         if ( authenticator_buff.value)
448             gss_release_buffer( &status, &authenticator_buff );
449
450         gss_delete_sec_context( &status, &context_handle, NULL );
451     }
452     gss_release_cred( &status, &server_creds );
453
454 cleanup_vars:
455     gss_release_name( &status, &server_name );
456
457     return ret;
458 }
459
460 /* -------------------------- */
461 static int gss_login(void *obj, struct passwd **uam_pwd,
462                      char *ibuf, size_t ibuflen,
463                      char *rbuf, size_t *rbuflen)
464 {
465
466     u_int16_t  temp16;
467
468     *rbuflen = 0;
469
470     /* The reply contains a two-byte ID value - note
471      * that Apple's implementation seems to always return 1 as well
472      */
473     temp16 = htons( 1 );
474     memcpy(rbuf, &temp16, sizeof(temp16));
475     *rbuflen += sizeof(temp16);
476     return AFPERR_AUTHCONT;
477 }
478
479 static int gss_logincont(void *obj, struct passwd **uam_pwd,
480                          char *ibuf, size_t ibuflen,
481                          char *rbuf, size_t *rbuflen)
482 {
483     struct passwd *pwd = NULL;
484     u_int16_t login_id;
485     char *username;
486     u_int16_t ticket_len;
487     char *p;
488     int rblen;
489     size_t userlen;
490     struct session_info *sinfo;
491
492     /* Apple's AFP 3.1 documentation specifies that this command
493      * takes the following format:
494      * pad (byte)
495      * id returned in LoginExt response (u_int16_t)
496      * username (format unspecified) padded, when necessary, to end on an even boundary
497      * ticket length (u_int16_t)
498      * ticket
499      */
500
501     /* Observation of AFP clients in the wild indicate that the actual
502      * format of this request is as follows:
503      * pad (byte) [consumed before login_ext is called]
504      * ?? (byte) - always observed to be 0
505      * id returned in LoginExt response (u_int16_t)
506      * username, encoding unspecified, null terminated C string,
507      *   padded when the terminating null is an even numbered byte.
508      *   The packet is formated such that the username begins on an
509      *   odd numbered byte. Eg if the username is 3 characters and the
510      *   terminating null makes 4, expect to pad the the result.
511      *   The encoding of this string is unknown.
512      * ticket length (u_int16_t)
513      * ticket
514      */
515
516     rblen = *rbuflen = 0;
517
518     if (ibuflen < 1 +sizeof(login_id)) {
519         LOG(log_info, logtype_uams, "uams_gss.c :LoginCont: received incomplete packet");
520         return AFPERR_PARAM;
521     }
522     ibuf++, ibuflen--; /* ?? */
523
524     /* 2 byte ID from LoginExt -- always '00 01' in this implementation */
525     memcpy( &login_id, ibuf, sizeof(login_id) );
526     ibuf += sizeof(login_id), ibuflen -= sizeof(login_id);
527     login_id = ntohs( login_id );
528
529     /* get the username buffer from apfd */
530     if (uam_afpserver_option(obj, UAM_OPTION_USERNAME, (void *) &username, &userlen) < 0)
531         return AFPERR_MISC;
532
533     /* get the session_info structure from afpd. We need the session key */
534     if (uam_afpserver_option(obj, UAM_OPTION_SESSIONINFO, (void *)&sinfo, NULL) < 0)
535         return AFPERR_MISC;
536
537     if (sinfo->sessionkey == NULL || sinfo->sessionkey_len == 0) {
538         /* Should never happen. Most likely way too old afpd version */
539         LOG(log_info, logtype_uams, "internal error: afpd's sessionkey not set");
540         return AFPERR_MISC;
541     }
542
543     /* We skip past the 'username' parameter because all that matters is the ticket */
544     p = ibuf;
545     while( *ibuf && ibuflen ) { ibuf++, ibuflen--; }
546     if (ibuflen < 4) {
547         LOG(log_info, logtype_uams, "uams_gss.c :LoginCont: user is %s, no ticket", p);
548         return AFPERR_PARAM;
549     }
550
551     ibuf++, ibuflen--; /* null termination */
552
553     if ((ibuf - p + 1) % 2) ibuf++, ibuflen--; /* deal with potential padding */
554
555     LOG(log_info, logtype_uams, "uams_gss.c :LoginCont: client thinks user is %s", p);
556
557     /* get the length of the ticket the client sends us */
558     memcpy(&ticket_len, ibuf, sizeof(ticket_len));
559     ibuf += sizeof(ticket_len); ibuflen -= sizeof(ticket_len);
560     ticket_len = ntohs( ticket_len );
561
562     /* a little bounds checking */
563     if (ticket_len > ibuflen) {
564         LOG(log_info, logtype_uams,
565             "uams_gss.c :LoginCont: invalid ticket length (%u > %u)", ticket_len, ibuflen);
566         return AFPERR_PARAM;
567     }
568
569     /* now try to authenticate */
570     if (!do_gss_auth(obj, ibuf, ticket_len, rbuf, &rblen, username, userlen, sinfo)) {
571         /* We use the username we got back from the gssapi client_name.
572            Should we compare this to the username the client sent in the clear?
573            We know the character encoding of the cleartext username (UTF8), what
574            encoding is the gssapi name in? */
575         if((pwd = uam_getname( obj, username, userlen )) == NULL) {
576             LOG(log_info, logtype_uams, "uam_getname() failed for %s", username);
577             return AFPERR_PARAM;
578         }
579         if (uam_checkuser(pwd) < 0) {
580             LOG(log_info, logtype_uams, "%s not a valid user", username);
581             return AFPERR_NOTAUTH;
582         }
583         *rbuflen = rblen;
584         *uam_pwd = pwd;
585         return AFP_OK;
586     } else {
587         LOG(log_info, logtype_uams, "do_gss_auth failed" );
588         *rbuflen = 0;
589         return AFPERR_MISC;
590     }
591 }
592
593 /*
594  * For the krb5 uam, this function only needs to return a two-byte
595  * login-session id. None of the data provided by the client up to this
596  * point is trustworthy as we'll have a signed ticket to parse in logincont.
597  */
598 static int gss_login_ext(void *obj, char *uname, struct passwd **uam_pwd,
599                          char *ibuf, size_t ibuflen,
600                          char *rbuf, size_t *rbuflen)
601 {
602     u_int16_t  temp16;
603
604     *rbuflen = 0;
605
606     /* The reply contains a two-byte ID value - note
607      * that Apple's implementation seems to always return 1 as well
608      */
609     temp16 = htons( 1 );
610     memcpy(rbuf, &temp16, sizeof(temp16));
611     *rbuflen += sizeof(temp16);
612     return AFPERR_AUTHCONT;
613 }
614
615 /* logout */
616 static void gss_logout() {
617 }
618
619 int uam_setup(const char *path)
620 {
621     if (uam_register(UAM_SERVER_LOGIN_EXT, path, "Client Krb v2",
622                      gss_login, gss_logincont, gss_logout, gss_login_ext) < 0)
623         if (uam_register(UAM_SERVER_LOGIN, path, "Client Krb v2",
624                          gss_login, gss_logincont, gss_logout) < 0)
625             return -1;
626
627     return 0;
628 }
629
630 static void uam_cleanup(void)
631 {
632     uam_unregister(UAM_SERVER_LOGIN_EXT, "Client Krb v2");
633 }
634
635 UAM_MODULE_EXPORT struct uam_export uams_gss = {
636     UAM_MODULE_SERVER,
637     UAM_MODULE_VERSION,
638     uam_setup, uam_cleanup
639 };