File: uams_gss.c

package info (click to toggle)
netatalk 3.1.12~ds-3
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 14,756 kB
  • sloc: ansic: 104,976; sh: 8,247; xml: 7,394; perl: 1,936; makefile: 1,430; python: 1,342; yacc: 309; lex: 49
file content (638 lines) | stat: -rw-r--r-- 21,388 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
/*
 * Copyright (c) 1990,1993 Regents of The University of Michigan.
 * Copyright (c) 1999 Adrian Sun (asun@u.washington.edu)
 * Copyright (c) 2003 The Reed Institute
 * Copyright (c) 2004 Bjoern Fernhomberg
 * All Rights Reserved.  See COPYRIGHT.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */

#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

#include <atalk/logger.h>
#include <atalk/afp.h>
#include <atalk/uam.h>
#include <atalk/util.h>
#include <atalk/compat.h>
#include <atalk/globals.h>

/* Kerberos includes */
#ifdef HAVE_GSSAPI_GSSAPI_H
#include <gssapi/gssapi.h>
#else
#include <gssapi.h>
#endif // HAVE_GSSAPI_GSSAPI_H

#ifdef HAVE_KERBEROS
#ifdef HAVE_KRB5_KRB5_H
#include <krb5/krb5.h>
#else
#include <krb5.h>
#endif /* HAVE_KRB5_KRB5_H */
#endif /* HAVE_KERBEROS */

#define LOG_UAMS(log_level, ...) \
    LOG(log_level, logtype_uams, __VA_ARGS__)

#define LOG_LOGINCONT(log_level, ...) \
    LOG_UAMS(log_level, "FPLoginCont: " __VA_ARGS__)

static void log_status(char *s,
                       OM_uint32 major_status,
                       OM_uint32 minor_status)
{
    gss_buffer_desc msg = GSS_C_EMPTY_BUFFER;
    OM_uint32 min_status, maj_status;
    OM_uint32 maj_ctx = 0, min_ctx = 0;

    while (1) {
        maj_status = gss_display_status( &min_status, major_status,
                                         GSS_C_GSS_CODE, GSS_C_NULL_OID,
                                         &maj_ctx, &msg );
        LOG_UAMS(log_error, "%s %.*s (error %s)",
                 s, msg.length, msg.value, strerror(errno));
        gss_release_buffer(&min_status, &msg);

        if (!maj_ctx)
            break;
    }

    while (1) {
        maj_status = gss_display_status( &min_status, minor_status,
                                         GSS_C_MECH_CODE, GSS_C_NULL_OID,
                                         &min_ctx, &msg );
        LOG_UAMS(log_error, "%s %.*s (error %s)",
                 s, msg.length, msg.value, strerror(errno));
        gss_release_buffer(&min_status, &msg);

        if (!min_ctx)
            break;
    }
}

static void log_ctx_flags(OM_uint32 flags)
{
    if (flags & GSS_C_DELEG_FLAG)
        LOG_LOGINCONT(log_debug, "context flag: GSS_C_DELEG_FLAG");
    if (flags & GSS_C_MUTUAL_FLAG)
        LOG_LOGINCONT(log_debug, "context flag: GSS_C_MUTUAL_FLAG");
    if (flags & GSS_C_REPLAY_FLAG)
        LOG_LOGINCONT(log_debug, "context flag: GSS_C_REPLAY_FLAG");
    if (flags & GSS_C_SEQUENCE_FLAG)
        LOG_LOGINCONT(log_debug, "context flag: GSS_C_SEQUENCE_FLAG");
    if (flags & GSS_C_CONF_FLAG)
        LOG_LOGINCONT(log_debug, "context flag: GSS_C_CONF_FLAG");
    if (flags & GSS_C_INTEG_FLAG)
        LOG_LOGINCONT(log_debug, "context flag: GSS_C_INTEG_FLAG");
}

static void log_service_name(gss_ctx_id_t context)
{
    OM_uint32 major_status = 0, minor_status = 0;
    gss_name_t service_name;
    gss_buffer_desc service_name_buffer;

    major_status = gss_inquire_context(&minor_status,
                                       context,
                                       NULL,
                                       &service_name,
                                       NULL,
                                       NULL,
                                       NULL,
                                       NULL,
                                       NULL);
    if (major_status != GSS_S_COMPLETE) {
        log_status("gss_inquire_context", major_status, minor_status);
        return;
    }

    major_status = gss_display_name(&minor_status,
                                    service_name,
                                    &service_name_buffer,
                                    NULL);
    if (major_status == GSS_S_COMPLETE) {
        LOG_LOGINCONT(log_debug,
                      "service principal is `%s'",
                      service_name_buffer.value);

        gss_release_buffer(&minor_status, &service_name_buffer);
    } else
        log_status("gss_display_name", major_status, minor_status);

    gss_release_name(&minor_status, &service_name);
}

static int get_client_username(char *username,
                               int ulen,
                               gss_name_t *client_name)
{
    OM_uint32 major_status = 0, minor_status = 0;
    gss_buffer_desc client_name_buffer;
    char *p;
    int ret = 0;

    /*
     * To extract the unix username, use gss_display_name on client_name.
     * We do rely on gss_display_name returning a zero terminated string.
     * The username returned contains the realm and possibly an instance.
     * We only want the username for afpd, so we have to strip those from
     * the username before copying it to afpd's buffer.
     */

    major_status = gss_display_name(&minor_status,
                                    *client_name,
                                    &client_name_buffer,
                                    NULL);
    if (major_status != GSS_S_COMPLETE) {
        log_status("gss_display_name", major_status, minor_status);
        return 1;
    }

    LOG_LOGINCONT(log_debug,
                  "user principal is `%s'",
                  client_name_buffer.value);

    /* chop off realm */
    p = strchr(client_name_buffer.value, '@');
    if (p)
        *p = 0;
    /* FIXME: chop off instance? */
    p = strchr(client_name_buffer.value, '/');
    if (p)
        *p = 0;

    /* check if this username fits into afpd's username buffer */
    size_t cnblen = strlen(client_name_buffer.value);
    if (cnblen >= ulen) {
        /* The username is too long for afpd's buffer, bail out */
        LOG_LOGINCONT(log_info,
                      "username `%s' too long (%d)",
                      client_name_buffer.value, cnblen);
        ret = 1;
    } else {
        /* copy stripped username to afpd's buffer */
        strlcpy(username, client_name_buffer.value, ulen);
    }

    gss_release_buffer(&minor_status, &client_name_buffer);

    return ret;
}

/* wrap afpd's sessionkey */
static int wrap_sessionkey(gss_ctx_id_t context, struct session_info *sinfo)
{
    OM_uint32 major_status = 0, minor_status = 0;
    gss_buffer_desc sesskey_buff, wrap_buff;
    int ret = 0;

    /*
     * gss_wrap afpd's session_key.
     * This is needed fo OS X 10.3 clients. They request this information
     * with type 8 (kGetKerberosSessionKey) on FPGetSession.
     * See AFP 3.1 specs, page 77.
     */
    sesskey_buff.value = sinfo->sessionkey;
    sesskey_buff.length = sinfo->sessionkey_len;

    /* gss_wrap the session key with the default mechanism.
       Require both confidentiality and integrity services */
    major_status = gss_wrap(&minor_status,
                            context,
                            true,
                            GSS_C_QOP_DEFAULT,
                            &sesskey_buff,
                            NULL,
                            &wrap_buff);

    if (major_status != GSS_S_COMPLETE) {
        log_status("gss_wrap", major_status, minor_status);
        return 1;
    }

    /* store the wrapped session key in afpd's session_info struct */
    if (NULL == (sinfo->cryptedkey = malloc(wrap_buff.length))) {
        LOG_UAMS(log_error,
                 "wrap_sessionkey: out of memory tyring to allocate %u bytes",
                 wrap_buff.length);
        ret = 1;
    } else {
        /* cryptedkey is binary data */
        memcpy(sinfo->cryptedkey, wrap_buff.value, wrap_buff.length);
        sinfo->cryptedkey_len = wrap_buff.length;
    }

    /* we're done with buffer, release */
    gss_release_buffer(&minor_status, &wrap_buff);

    return ret;
}

static int accept_sec_context(gss_ctx_id_t *context,
                              gss_buffer_desc *ticket_buffer,
                              gss_name_t *client_name,
                              gss_buffer_desc *authenticator_buff)
{
    OM_uint32 major_status = 0, minor_status = 0, flags = 0;

    /* Initialize autheticator buffer. */
    authenticator_buff->length = 0;
    authenticator_buff->value = NULL;

    LOG_LOGINCONT(log_debug,
                  "accepting context (ticketlen: %u)",
                  ticket_buffer->length);

    /*
     * Try to accept the secondary context using the token in ticket_buffer.
     * We don't care about the principals or mechanisms used, nor for the time.
     * We don't act as a proxy either.
     */
    major_status = gss_accept_sec_context(&minor_status,
                                          context,
                                          GSS_C_NO_CREDENTIAL,
                                          ticket_buffer,
                                          GSS_C_NO_CHANNEL_BINDINGS,
                                          client_name,
                                          NULL,
                                          authenticator_buff,
                                          &flags,
                                          NULL,
                                          NULL);

    if (major_status != GSS_S_COMPLETE) {
        log_status("gss_accept_sec_context", major_status, minor_status);
        return 1;
    }

    log_ctx_flags(flags);
    return 0;
}

static int do_gss_auth(void *obj,
                       char *ibuf, size_t ibuflen,
                       char *rbuf, int *rbuflen,
                       char *username, size_t ulen,
                       struct session_info *sinfo )
{
    OM_uint32 status = 0;
    gss_name_t client_name;
    gss_ctx_id_t context = GSS_C_NO_CONTEXT;
    gss_buffer_desc ticket_buffer, authenticator_buff;
    int ret = 0;

    /*
     * Try to accept the secondary context, using the ticket/token the
     * client sent us. Ticket is stored at current ibuf position.
     * Don't try to release ticket_buffer later, it points into ibuf!
     */
    ticket_buffer.length = ibuflen;
    ticket_buffer.value = ibuf;

    if ((ret = accept_sec_context(&context,
                                  &ticket_buffer,
                                  &client_name,
                                  &authenticator_buff)))
        return ret;
    log_service_name(context);

    /* We succesfully acquired the secondary context, now get the
       username for afpd and gss_wrap the sessionkey */
    if ((ret = get_client_username(username, ulen, &client_name)))
        goto cleanup_client_name;

    if ((ret = wrap_sessionkey(context, sinfo)))
        goto cleanup_client_name;

    /* Authenticated, construct the reply using:
     * authenticator length (uint16_t)
     * authenticator
     */
    /* copy the authenticator length into the reply buffer */
    uint16_t auth_len = htons(authenticator_buff.length);
    memcpy(rbuf, &auth_len, sizeof(auth_len));
    *rbuflen += sizeof(auth_len);
    rbuf += sizeof(auth_len);

    /* copy the authenticator value into the reply buffer */
    memcpy(rbuf, authenticator_buff.value, authenticator_buff.length);
    *rbuflen += authenticator_buff.length;

cleanup_client_name:
    gss_release_name(&status, &client_name);
    gss_release_buffer(&status, &authenticator_buff);
    gss_delete_sec_context(&status, &context, NULL);

    return ret;
}

/* -------------------------- */

/*
 * For the gss uam, this function only needs to return a two-byte
 * login-session id. None of the data provided by the client up to this
 * point is trustworthy as we'll have a signed ticket to parse in logincont.
 */
static int gss_login(void *obj,
                     struct passwd **uam_pwd,
                     char *ibuf, size_t ibuflen,
                     char *rbuf, size_t *rbuflen)
{
    *rbuflen = 0;

    /* The reply contains a two-byte ID value - note
     * that Apple's implementation seems to always return 1 as well
     */
    uint16_t temp16 = htons(1);
    memcpy(rbuf, &temp16, sizeof(temp16));
    *rbuflen += sizeof(temp16);

    return AFPERR_AUTHCONT;
}

static int gss_logincont(void *obj,
                         struct passwd **uam_pwd,
                         char *ibuf, size_t ibuflen,
                         char *rbuf, size_t *rbuflen)
{
    struct passwd *pwd = NULL;
    uint16_t login_id;
    char *username;
    uint16_t ticket_len;
    char *p;
    int rblen;
    size_t userlen;
    struct session_info *sinfo;

    /* Apple's AFP 3.1 documentation specifies that this command
     * takes the following format:
     * pad (byte)
     * id returned in LoginExt response (uint16_t)
     * username (format unspecified)
     *   padded, when necessary, to end on an even boundary
     * ticket length (uint16_t)
     * ticket
     */

    /* Observation of AFP clients in the wild indicate that the actual
     * format of this request is as follows:
     * pad (byte) [consumed before login_ext is called]
     * ?? (byte) - always observed to be 0
     * id returned in LoginExt response (uint16_t)
     * username, encoding unspecified, null terminated C string,
     *   padded when the terminating null is an even numbered byte.
     *   The packet is formated such that the username begins on an
     *   odd numbered byte. E.g. if the username is 3 characters and the
     *   terminating null makes 4, expect to pad the the result.
     *   The encoding of this string is unknown.
     * ticket length (uint16_t)
     * ticket
     */

    rblen = *rbuflen = 0;

    if (ibuflen < 1 +sizeof(login_id)) {
        LOG_LOGINCONT(log_info, "received incomplete packet");
        return AFPERR_PARAM;
    }
    ibuf++, ibuflen--; /* ?? */

    /* 2 byte ID from LoginExt -- always '00 01' in this implementation */
    memcpy(&login_id, ibuf, sizeof(login_id));
    ibuf += sizeof(login_id), ibuflen -= sizeof(login_id);
    login_id = ntohs(login_id);

    /* get the username buffer from apfd */
    if (uam_afpserver_option(obj, UAM_OPTION_USERNAME, &username, &userlen) < 0)
        return AFPERR_MISC;

    /* get the session_info structure from afpd. We need the session key */
    if (uam_afpserver_option(obj, UAM_OPTION_SESSIONINFO, &sinfo, NULL) < 0)
        return AFPERR_MISC;

    if (sinfo->sessionkey == NULL || sinfo->sessionkey_len == 0) {
        /* Should never happen. Most likely way too old afpd version */
        LOG_LOGINCONT(log_error, "internal error: afpd's sessionkey not set");
        return AFPERR_MISC;
    }

    /* We skip past the 'username' parameter because all that matters is the ticket */
    p = ibuf;
    while ( *ibuf && ibuflen ) { ibuf++, ibuflen--; }
    if (ibuflen < 4) {
        LOG_LOGINCONT(log_info, "user is %s, no ticket", p);
        return AFPERR_PARAM;
    }

    ibuf++, ibuflen--; /* null termination */

    if ((ibuf - p + 1) % 2) ibuf++, ibuflen--; /* deal with potential padding */

    LOG_LOGINCONT(log_debug, "client thinks user is %s", p);

    /* get the length of the ticket the client sends us */
    memcpy(&ticket_len, ibuf, sizeof(ticket_len));
    ibuf += sizeof(ticket_len); ibuflen -= sizeof(ticket_len);
    ticket_len = ntohs(ticket_len);

    /* a little bounds checking */
    if (ticket_len > ibuflen) {
        LOG_LOGINCONT(log_info,
                      "invalid ticket length (%u > %u)",
                      ticket_len, ibuflen);
        return AFPERR_PARAM;
    }

    /* now try to authenticate */
    if (do_gss_auth(obj, ibuf, ticket_len, rbuf, &rblen, username, userlen, sinfo)) {
        LOG_LOGINCONT(log_info, "do_gss_auth() failed" );
        *rbuflen = 0;
        return AFPERR_MISC;
    }

    /* We use the username we got back from the gssapi client_name.
       Should we compare this to the username the client sent in the clear?
       We know the character encoding of the cleartext username (UTF8), what
       encoding is the gssapi name in? */
    if ((pwd = uam_getname( obj, username, userlen )) == NULL) {
        LOG_LOGINCONT(log_info, "uam_getname() failed for %s", username);
        return AFPERR_NOTAUTH;
    }
    if (uam_checkuser(pwd) < 0) {
        LOG_LOGINCONT(log_info, "`%s'' not a valid user", username);
        return AFPERR_NOTAUTH;
    }

    *rbuflen = rblen;
    *uam_pwd = pwd;
    return AFP_OK;
}

/* logout */
static void gss_logout() {
}

static int gss_login_ext(void *obj,
                         char *uname,
                         struct passwd **uam_pwd,
                         char *ibuf, size_t ibuflen,
                         char *rbuf, size_t *rbuflen)
{
    return gss_login(obj, uam_pwd, ibuf, ibuflen, rbuf, rbuflen);
}

static int set_principal(AFPObj *obj, char *principal)
{
    size_t len = strlen(principal);

    if (len > 255) {
        LOG(log_error, logtype_afpd, "set_principal: principal '%s' too long (max=255)", principal, len);
        return -1;
    }

    /* We store: 1 byte number principals (1) + 1 byte principal name length + zero terminated principal */
    if ((obj->options.k5principal = malloc(len + 3)) == NULL) {
        LOG(log_error, logtype_afpd, "set_principal: OOM");
        return -1;
    }

    LOG(log_info, logtype_afpd, "Using AFP Kerberos service principal name: %s", principal);

    obj->options.k5principal[0] = 1;
    obj->options.k5principal[1] = (unsigned char)len;
    obj->options.k5principal_buflen = len + 2;
    strncpy(obj->options.k5principal + 2, principal, len);

    return 0;
}

static int gss_create_principal(AFPObj *obj)
{
    int rv = -1;
#ifdef HAVE_KERBEROS
    krb5_context context;
    krb5_error_code ret;
    const char *error_msg;
    krb5_keytab keytab;
    krb5_keytab_entry entry;
    krb5_principal service_principal;
    char *principal;
    krb5_kt_cursor cursor;

    if (krb5_init_context(&context)) {
        LOG(log_error, logtype_afpd, "gss_create_principal: failed to initialize a krb5_context");
        goto exit;
    }
    if ((ret = krb5_kt_default(context, &keytab)))
        goto krb5_error;

    if (obj->options.k5service && obj->options.fqdn && obj->options.k5realm) {
        LOG(log_debug, logtype_afpd, "gss_create_principal: using service principal specified in options");
            
        if ((ret = krb5_build_principal(context,
                                        &service_principal,
                                        strlen(obj->options.k5realm),
                                        obj->options.k5realm,
                                        obj->options.k5service,
                                        obj->options.fqdn,
                                        NULL)))
            goto krb5_error;

        if ((ret = krb5_kt_get_entry(context,
                                     keytab,
                                     service_principal,
                                     0, // kvno - wildcard
                                     0, // enctype - wildcard
                                     &entry)) == KRB5_KT_NOTFOUND) {
            krb5_unparse_name(context, service_principal, &principal);
            LOG(log_error, logtype_afpd, "gss_create_principal: specified service principal '%s' not found in keytab", principal);
#ifdef HAVE_KRB5_FREE_UNPARSED_NAME
            krb5_free_unparsed_name(context, principal);
#else
            krb5_xfree(principal);
#endif
            goto krb5_cleanup;
        }
        krb5_free_principal(context, service_principal);
        if (ret)
            goto krb5_error;
    } else {
        LOG(log_debug, logtype_afpd, "gss_create_principal: using first entry from keytab as service principal");
        if ((ret = krb5_kt_start_seq_get(context, keytab, &cursor)))
            goto krb5_error;
        ret = krb5_kt_next_entry(context, keytab, &entry, &cursor);
        krb5_kt_end_seq_get(context, keytab, &cursor);
        if (ret)
            goto krb5_error;
    }

    krb5_unparse_name(context, entry.principal, &principal);
#ifdef HAVE_KRB5_FREE_KEYTAB_ENTRY_CONTENTS
    krb5_free_keytab_entry_contents(context, &entry);
#elif defined(HAVE_KRB5_KT_FREE_ENTRY)
    krb5_kt_free_entry(context, &entry);
#endif
    set_principal(obj, principal);
    free(principal);
    rv = 0;
    goto krb5_cleanup;

krb5_error:
    if (ret) {
        error_msg = krb5_get_error_message(context, ret);
        LOG(log_note, logtype_afpd, "Can't get principal from default keytab: %s",
            (char *)error_msg);
#ifdef HAVE_KRB5_FREE_ERROR_MESSAGE
        krb5_free_error_message(context, error_msg);
#else
        krb5_xfree(error_msg);
#endif
    }

krb5_cleanup:
    krb5_kt_close(context, keytab);
    krb5_free_context(context);

#else /* ! HAVE_KERBEROS */
    if (!obj->options.k5service || !obj->options.fqdn || !obj->options.k5realm)
        goto exit;

    char principal[255];
    size_t len = snprintf(principal, sizeof(principal), "%s/%s@%s",
                          obj->options.k5service, obj->options.fqdn, obj->options.k5realm);
    (void)set_principal(obj, principal);
    rv = 0;
#endif /* HAVE_KERBEROS */

exit:
    return rv;
}

static int uam_setup(void *handle, const char *path)
{
    AFPObj *obj = (AFPObj *)handle;
    if (gss_create_principal(obj) != 0)
        return -1;

    return uam_register(UAM_SERVER_LOGIN_EXT, path, "Client Krb v2",
                        gss_login, gss_logincont, gss_logout, gss_login_ext);
}

static void uam_cleanup(void)
{
    uam_unregister(UAM_SERVER_LOGIN_EXT, "Client Krb v2");
}

UAM_MODULE_EXPORT struct uam_export uams_gss = {
    UAM_MODULE_SERVER,
    UAM_MODULE_VERSION,
    uam_setup,
    uam_cleanup
};