]> arthur.barton.de Git - netatalk.git/blob - etc/uams/uams_dhx2_passwd.c
Posix/SUS portability cleanup
[netatalk.git] / etc / uams / uams_dhx2_passwd.c
1 /*
2  * $Id: uams_dhx2_passwd.c,v 1.8 2010-03-30 10:25:49 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  * All Rights Reserved.  See COPYRIGHT.
7  */
8
9 #ifdef HAVE_CONFIG_H
10 #include "config.h"
11 #endif /* HAVE_CONFIG_H */
12
13 #ifdef UAM_DHX2
14
15 #include <atalk/standards.h>
16
17 #include <stdio.h>
18 #include <stdlib.h>
19 #include <string.h>
20 #include <errno.h>
21 #include <pwd.h>
22
23 #ifdef HAVE_UNISTD_H
24 #include <unistd.h>
25 #endif
26
27 #ifdef HAVE_CRYPT_H
28 #include <crypt.h>
29 #endif
30
31 #ifdef HAVE_SYS_TIME_H
32 #include <sys/time.h>
33 #endif
34
35 #ifdef HAVE_TIME_H
36 #include <time.h>
37 #endif
38
39 #ifdef SHADOWPW
40 #include <shadow.h>
41 #endif
42
43 #ifdef HAVE_LIBGCRYPT
44 #include <gcrypt.h>
45 #endif
46
47 #include <atalk/afp.h>
48 #include <atalk/uam.h>
49 #include <atalk/logger.h>
50
51 /* Number of bits for p which we generate. Everybode out there uses 512, so we beet them */
52 #define PRIMEBITS 1024
53
54 /* hash a number to a 16-bit quantity */
55 #define dhxhash(a) ((((unsigned long) (a) >> 8) ^   \
56                      (unsigned long) (a)) & 0xffff)
57
58 /* Some parameters need be maintained across calls */
59 static gcry_mpi_t p, Ra;
60 static gcry_mpi_t serverNonce;
61 static char *K_MD5hash = NULL;
62 static int K_hash_len;
63 static u_int16_t ID;
64
65 /* The initialization vectors for CAST128 are fixed by Apple. */
66 static unsigned char dhx_c2siv[] = { 'L', 'W', 'a', 'l', 'l', 'a', 'c', 'e' };
67 static unsigned char dhx_s2civ[] = { 'C', 'J', 'a', 'l', 'b', 'e', 'r', 't' };
68
69 /* Static variables used to communicate between the conversation function
70  * and the server_login function */
71 static struct passwd *dhxpwd;
72
73 /*********************************************************
74  * Crypto helper func to generate p and g for use in DH.
75  * libgcrypt doesn't provide one directly.
76  * Algorithm taken from GNUTLS:gnutls_dh_primes.c
77  *********************************************************/
78
79 /**
80  * This function will generate a new pair of prime and generator for use in
81  * the Diffie-Hellman key exchange.
82  * The bits value should be one of 768, 1024, 2048, 3072 or 4096.
83  **/
84
85 static int
86 dh_params_generate (gcry_mpi_t *ret_p, gcry_mpi_t *ret_g, unsigned int bits) {
87
88     int result, times = 0, qbits;
89
90     gcry_mpi_t g = NULL, prime = NULL;
91     gcry_mpi_t *factors = NULL;
92     gcry_error_t err;
93
94     /* Version check should be the very first call because it
95        makes sure that important subsystems are intialized. */
96     if (!gcry_check_version (GCRYPT_VERSION)) {
97         LOG(log_info, logtype_uams, "PAM DHX2: libgcrypt versions mismatch. Need: %s", GCRYPT_VERSION);
98         result = AFPERR_MISC;
99         goto error;
100     }
101
102     if (bits < 256)
103         qbits = bits / 2;
104     else
105         qbits = (bits / 40) + 105;
106
107     if (qbits & 1) /* better have an even number */
108         qbits++;
109
110     /* find a prime number of size bits. */
111     do {
112         if (times) {
113             gcry_mpi_release (prime);
114             gcry_prime_release_factors (factors);
115         }
116         err = gcry_prime_generate (&prime, bits, qbits, &factors, NULL, NULL,
117                                    GCRY_STRONG_RANDOM, GCRY_PRIME_FLAG_SPECIAL_FACTOR);
118         if (err != 0) {
119             result = AFPERR_MISC;
120             goto error;
121         }
122         err = gcry_prime_check (prime, 0);
123         times++;
124     } while (err != 0 && times < 10);
125
126     if (err != 0) {
127         result = AFPERR_MISC;
128         goto error;
129     }
130
131     /* generate the group generator. */
132     err = gcry_prime_group_generator (&g, prime, factors, NULL);
133     if (err != 0) {
134         result = AFPERR_MISC;
135         goto error;
136     }
137
138     gcry_prime_release_factors (factors);
139     factors = NULL;
140
141     if (ret_g)
142         *ret_g = g;
143     else
144         gcry_mpi_release (g);
145     if (ret_p)
146         *ret_p = prime;
147     else
148         gcry_mpi_release (prime);
149
150     return 0;
151
152 error:
153     gcry_prime_release_factors (factors);
154     gcry_mpi_release (g);
155     gcry_mpi_release (prime);
156
157     return result;
158 }
159
160 static int dhx2_setup(void *obj, char *ibuf _U_, size_t ibuflen _U_,
161                       char *rbuf, size_t *rbuflen)
162 {
163     int ret;
164     size_t nwritten;
165     gcry_mpi_t g, Ma;
166     char *Ra_binary = NULL;
167 #ifdef SHADOWPW
168     struct spwd *sp;
169 #endif /* SHADOWPW */
170
171     *rbuflen = 0;
172
173     /* Initialize passwd/shadow */
174 #ifdef SHADOWPW
175     if (( sp = getspnam( dhxpwd->pw_name )) == NULL ) {
176         LOG(log_info, logtype_uams, "DHX2: no shadow passwd entry for this user");
177         return AFPERR_NOTAUTH;
178     }
179     dhxpwd->pw_passwd = sp->sp_pwdp;
180 #endif /* SHADOWPW */
181
182     if (!dhxpwd->pw_passwd)
183         return AFPERR_NOTAUTH;
184
185     /* Initialize DH params */
186
187     p = gcry_mpi_new(0);
188     g = gcry_mpi_new(0);
189     Ra = gcry_mpi_new(0);
190     Ma = gcry_mpi_new(0);
191
192     /* Generate p and g for DH */
193     ret = dh_params_generate( &p, &g, PRIMEBITS);
194     if (ret != 0) {
195         LOG(log_info, logtype_uams, "DHX2: Couldn't generate p and g");
196         ret = AFPERR_MISC;
197         goto error;
198     }
199
200     /* Generate our random number Ra. */
201     Ra_binary = calloc(1, PRIMEBITS/8);
202     if (Ra_binary == NULL) {
203         ret = AFPERR_MISC;
204         goto error;
205     }
206     gcry_randomize(Ra_binary, PRIMEBITS/8, GCRY_STRONG_RANDOM);
207     gcry_mpi_scan(&Ra, GCRYMPI_FMT_USG, Ra_binary, PRIMEBITS/8, NULL);
208     free(Ra_binary);
209     Ra_binary = NULL;
210
211     /* Ma = g^Ra mod p. This is our "public" key */
212     gcry_mpi_powm(Ma, g, Ra, p);
213
214     /* ------- DH Init done ------ */
215     /* Start building reply packet */
216
217     /* Session ID first */
218     ID = dhxhash(obj);
219     *(u_int16_t *)rbuf = htons(ID);
220     rbuf += 2;
221     *rbuflen += 2;
222
223     /* g is next */
224     gcry_mpi_print( GCRYMPI_FMT_USG, (unsigned char *)rbuf, 4, &nwritten, g);
225     if (nwritten < 4) {
226         memmove( rbuf+4-nwritten, rbuf, nwritten);
227         memset( rbuf, 0, 4-nwritten);
228     }
229     rbuf += 4;
230     *rbuflen += 4;
231
232     /* len = length of p = PRIMEBITS/8 */
233     *(u_int16_t *)rbuf = htons((u_int16_t) PRIMEBITS/8);
234     rbuf += 2;
235     *rbuflen += 2;
236
237     /* p */
238     gcry_mpi_print( GCRYMPI_FMT_USG, (unsigned char *)rbuf, PRIMEBITS/8, NULL, p);
239     rbuf += PRIMEBITS/8;
240     *rbuflen += PRIMEBITS/8;
241
242     /* Ma */
243     gcry_mpi_print( GCRYMPI_FMT_USG, (unsigned char *)rbuf, PRIMEBITS/8, &nwritten, Ma);
244     if (nwritten < PRIMEBITS/8) {
245         memmove(rbuf + (PRIMEBITS/8) - nwritten, rbuf, nwritten);
246         memset(rbuf, 0, (PRIMEBITS/8) - nwritten);
247     }
248     rbuf += PRIMEBITS/8;
249     *rbuflen += PRIMEBITS/8;
250
251     ret = AFPERR_AUTHCONT;
252
253 error:              /* We exit here anyway */
254     /* We will only need p and Ra later, but mustn't forget to release it ! */
255     gcry_mpi_release(g);
256     gcry_mpi_release(Ma);
257     return ret;
258 }
259
260 /* -------------------------------- */
261 static int login(void *obj, char *username, int ulen,  struct passwd **uam_pwd _U_,
262                  char *ibuf, size_t ibuflen,
263                  char *rbuf, size_t *rbuflen)
264 {
265     if (( dhxpwd = uam_getname(obj, username, ulen)) == NULL ) {
266         LOG(log_info, logtype_uams, "DHX2: unknown username");
267         return AFPERR_NOTAUTH;
268     }
269
270     LOG(log_info, logtype_uams, "DHX2 login: %s", username);
271     return dhx2_setup(obj, ibuf, ibuflen, rbuf, rbuflen);
272 }
273
274 /* -------------------------------- */
275 /* dhx login: things are done in a slightly bizarre order to avoid
276  * having to clean things up if there's an error. */
277 static int passwd_login(void *obj, struct passwd **uam_pwd,
278                         char *ibuf, size_t ibuflen,
279                         char *rbuf, size_t *rbuflen)
280 {
281     char *username;
282     size_t len, ulen;
283
284     *rbuflen = 0;
285
286     /* grab some of the options */
287     if (uam_afpserver_option(obj, UAM_OPTION_USERNAME, (void *) &username, &ulen) < 0) {
288         LOG(log_info, logtype_uams, "DHX2: uam_afpserver_option didn't meet uam_option_username  -- %s",
289             strerror(errno));
290         return AFPERR_PARAM;
291     }
292
293     len = (unsigned char) *ibuf++;
294     if ( len > ulen ) {
295         LOG(log_info, logtype_uams, "DHX2: Signature Retieval Failure -- %s",
296             strerror(errno));
297         return AFPERR_PARAM;
298     }
299
300     memcpy(username, ibuf, len );
301     ibuf += len;
302     username[ len ] = '\0';
303
304     if ((unsigned long) ibuf & 1) /* pad to even boundary */
305         ++ibuf;
306
307     return (login(obj, username, ulen, uam_pwd, ibuf, ibuflen, rbuf, rbuflen));
308 }
309
310 /* ----------------------------- */
311 static int passwd_login_ext(void *obj, char *uname, struct passwd **uam_pwd,
312                             char *ibuf, size_t ibuflen,
313                             char *rbuf, size_t *rbuflen)
314 {
315     char *username;
316     size_t len, ulen;
317     u_int16_t  temp16;
318
319     *rbuflen = 0;
320
321     /* grab some of the options */
322     if (uam_afpserver_option(obj, UAM_OPTION_USERNAME, (void *) &username, &ulen) < 0) {
323         LOG(log_info, logtype_uams, "DHX2: uam_afpserver_option didn't meet uam_option_username  -- %s",
324             strerror(errno));
325         return AFPERR_PARAM;
326     }
327
328     if (*uname != 3)
329         return AFPERR_PARAM;
330     uname++;
331     memcpy(&temp16, uname, sizeof(temp16));
332     len = ntohs(temp16);
333
334     if ( !len || len > ulen ) {
335         LOG(log_info, logtype_uams, "DHX2: Signature Retrieval Failure -- %s",
336             strerror(errno));
337         return AFPERR_PARAM;
338     }
339     memcpy(username, uname +2, len );
340     username[ len ] = '\0';
341
342     return (login(obj, username, ulen, uam_pwd, ibuf, ibuflen, rbuf, rbuflen));
343 }
344
345 /* -------------------------------- */
346
347 static int logincont1(void *obj _U_, struct passwd **uam_pwd _U_,
348                       char *ibuf, size_t ibuflen,
349                       char *rbuf, size_t *rbuflen)
350 {
351     size_t nwritten;
352     int ret;
353     gcry_mpi_t Mb, K, clientNonce;
354     unsigned char *K_bin = NULL;
355     char serverNonce_bin[16];
356     gcry_cipher_hd_t ctx;
357     gcry_error_t ctxerror;
358
359     *rbuflen = 0;
360
361     Mb = gcry_mpi_new(0);
362     K = gcry_mpi_new(0);
363     clientNonce = gcry_mpi_new(0);
364     serverNonce = gcry_mpi_new(0);
365
366     /* Packet size should be: Session ID + Ma + Encrypted client nonce */
367     if (ibuflen != 2 + PRIMEBITS/8 + 16) {
368         LOG(log_error, logtype_uams, "DHX2: Paket length not correct");
369         ret = AFPERR_PARAM;
370         goto error_noctx;
371     }
372
373     /* Skip session id */
374     ibuf += 2;
375
376     /* Extract Mb, client's "public" key */
377     gcry_mpi_scan(&Mb, GCRYMPI_FMT_USG, ibuf, PRIMEBITS/8, NULL);
378     ibuf += PRIMEBITS/8;
379
380     /* Now finally generate the Key: K = Mb^Ra mod p */
381     gcry_mpi_powm(K, Mb, Ra, p);
382
383     /* We need K in binary form in order to ... */
384     K_bin = calloc(1, PRIMEBITS/8);
385     if (K_bin == NULL) {
386         ret = AFPERR_MISC;
387         goto error_noctx;
388     }
389     gcry_mpi_print(GCRYMPI_FMT_USG, K_bin, PRIMEBITS/8, &nwritten, K);
390     if (nwritten < PRIMEBITS/8) {
391         memmove(K_bin + PRIMEBITS/8 - nwritten, K_bin, nwritten);
392         memset(K_bin, 0, PRIMEBITS/8 - nwritten);
393     }
394
395     /* ... generate the MD5 hash of K. K_MD5hash is what we actually use ! */
396     K_MD5hash = calloc(1, K_hash_len = gcry_md_get_algo_dlen(GCRY_MD_MD5));
397     if (K_MD5hash == NULL) {
398         ret = AFPERR_MISC;
399         free(K_bin);
400         K_bin = NULL;
401         goto error_noctx;
402     }
403     gcry_md_hash_buffer(GCRY_MD_MD5, K_MD5hash, K_bin, PRIMEBITS/8);
404     free(K_bin);
405     K_bin = NULL;
406
407     /* FIXME: To support the Reconnect UAM, we need to store this key somewhere */
408
409     /* Set up our encryption context. */
410     ctxerror = gcry_cipher_open( &ctx, GCRY_CIPHER_CAST5, GCRY_CIPHER_MODE_CBC, 0);
411     if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
412         ret = AFPERR_MISC;
413         goto error_ctx;
414     }
415     /* Set key */
416     ctxerror = gcry_cipher_setkey(ctx, K_MD5hash, K_hash_len);
417     if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
418         ret = AFPERR_MISC;
419         goto error_ctx;
420     }
421     /* Set the initialization vector for client->server transfer. */
422     ctxerror = gcry_cipher_setiv(ctx, dhx_c2siv, sizeof(dhx_c2siv));
423     if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
424         ret = AFPERR_MISC;
425         goto error_ctx;
426     }
427     /* Finally: decrypt client's md5_K(client nonce, C2SIV) inplace */
428     ctxerror = gcry_cipher_decrypt(ctx, ibuf, 16, NULL, 0);
429     if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
430         ret = AFPERR_MISC;
431         goto error_ctx;
432     }
433     /* Pull out clients nonce */
434     gcry_mpi_scan(&clientNonce, GCRYMPI_FMT_USG, ibuf, 16, NULL);
435     /* Increment nonce */
436     gcry_mpi_add_ui(clientNonce, clientNonce, 1);
437
438     /* Generate our nonce and remember it for Logincont2 */
439     gcry_create_nonce(serverNonce_bin, 16); /* We'll use this here */
440     gcry_mpi_scan(&serverNonce, GCRYMPI_FMT_USG, serverNonce_bin, 16, NULL); /* For use in Logincont2 */
441
442     /* ---- Start building reply packet ---- */
443
444     /* Session ID + 1 first */
445     *(u_int16_t *)rbuf = htons(ID+1);
446     rbuf += 2;
447     *rbuflen += 2;
448
449     /* Client nonce + 1 */
450     gcry_mpi_print(GCRYMPI_FMT_USG, (unsigned char *)rbuf, PRIMEBITS/8, NULL, clientNonce);
451     /* Server nonce */
452     memcpy(rbuf+16, serverNonce_bin, 16);
453
454     /* Set the initialization vector for server->client transfer. */
455     ctxerror = gcry_cipher_setiv(ctx, dhx_s2civ, sizeof(dhx_s2civ));
456     if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
457         ret = AFPERR_MISC;
458         goto error_ctx;
459     }
460     /* Encrypt md5_K(clientNonce+1, serverNonce) inplace */
461     ctxerror = gcry_cipher_encrypt(ctx, rbuf, 32, NULL, 0);
462     if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
463         ret = AFPERR_MISC;
464         goto error_ctx;
465     }
466     rbuf += 32;
467     *rbuflen += 32;
468
469     ret = AFPERR_AUTHCONT;
470     goto exit;
471
472 error_ctx:
473     gcry_cipher_close(ctx);
474 error_noctx:
475     gcry_mpi_release(serverNonce);
476     free(K_MD5hash);
477     K_MD5hash=NULL;
478 exit:
479     gcry_mpi_release(K);
480     gcry_mpi_release(Mb);
481     gcry_mpi_release(Ra);
482     gcry_mpi_release(p);
483     gcry_mpi_release(clientNonce);
484     return ret;
485 }
486
487 static int logincont2(void *obj _U_, struct passwd **uam_pwd,
488                       char *ibuf, size_t ibuflen,
489                       char *rbuf _U_, size_t *rbuflen)
490 {
491 #ifdef SHADOWPW
492     struct spwd *sp;
493 #endif /* SHADOWPW */
494     int ret;
495     char *p;
496     gcry_mpi_t retServerNonce;
497     gcry_cipher_hd_t ctx;
498     gcry_error_t ctxerror;
499
500     *rbuflen = 0;
501     retServerNonce = gcry_mpi_new(0);
502
503     /* Packet size should be: Session ID + ServerNonce + Passwd buffer (evantually +10 extra bytes, see Apples Docs)*/
504     if ((ibuflen != 2 + 16 + 256) && (ibuflen != 2 + 16 + 256 + 10)) {
505         LOG(log_error, logtype_uams, "DHX2: Paket length not correct: %d. Should be 274 or 284.", ibuflen);
506         ret = AFPERR_PARAM;
507         goto error_noctx;
508     }
509
510     /* Set up our encryption context. */
511     ctxerror = gcry_cipher_open( &ctx, GCRY_CIPHER_CAST5, GCRY_CIPHER_MODE_CBC, 0);
512     if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
513         ret = AFPERR_MISC;
514         goto error_ctx;
515     }
516     /* Set key */
517     ctxerror = gcry_cipher_setkey(ctx, K_MD5hash, K_hash_len);
518     if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
519         ret = AFPERR_MISC;
520         goto error_ctx;
521     }
522     /* Set the initialization vector for client->server transfer. */
523     ctxerror = gcry_cipher_setiv(ctx, dhx_c2siv, sizeof(dhx_c2siv));
524     if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
525         ret = AFPERR_MISC;
526         goto error_ctx;
527     }
528
529     /* Skip Session ID */
530     ibuf += 2;
531
532     /* Finally: decrypt client's md5_K(serverNonce+1, passwor, C2SIV) inplace */
533     ctxerror = gcry_cipher_decrypt(ctx, ibuf, 16+256, NULL, 0);
534     if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
535         ret = AFPERR_MISC;
536         goto error_ctx;
537     }
538     /* Pull out nonce. Should be serverNonce+1 */
539     gcry_mpi_scan(&retServerNonce, GCRYMPI_FMT_USG, ibuf, 16, NULL);
540     gcry_mpi_sub_ui(retServerNonce, retServerNonce, 1);
541     if ( gcry_mpi_cmp( serverNonce, retServerNonce) != 0) {
542         /* We're hacked!  */
543         ret = AFPERR_NOTAUTH;
544         goto error_ctx;
545     }
546     ibuf += 16;         /* ibuf now point to passwd in cleartext */
547
548     /* ---- Start authentication --- */
549     ret = AFPERR_NOTAUTH;
550
551     p = crypt( ibuf, dhxpwd->pw_passwd );
552     memset(ibuf, 0, 255);
553     if ( strcmp( p, dhxpwd->pw_passwd ) == 0 ) {
554         *uam_pwd = dhxpwd;
555         ret = AFP_OK;
556     }
557
558 #ifdef SHADOWPW
559     if (( sp = getspnam( dhxpwd->pw_name )) == NULL ) {
560         LOG(log_info, logtype_uams, "no shadow passwd entry for %s", dhxpwd->pw_name);
561         ret = AFPERR_NOTAUTH;
562         goto exit;
563     }
564
565     /* check for expired password */
566     if (sp && sp->sp_max != -1 && sp->sp_lstchg) {
567         time_t now = time(NULL) / (60*60*24);
568         int32_t expire_days = sp->sp_lstchg - now + sp->sp_max;
569         if ( expire_days < 0 ) {
570             LOG(log_info, logtype_uams, "password for user %s expired", dhxpwd->pw_name);
571             ret = AFPERR_PWDEXPR;
572             goto exit;
573         }
574     }
575 #endif /* SHADOWPW */
576
577 error_ctx:
578     gcry_cipher_close(ctx);
579 error_noctx:
580 exit:
581     free(K_MD5hash);
582     K_MD5hash=NULL;
583     gcry_mpi_release(serverNonce);
584     gcry_mpi_release(retServerNonce);
585     return ret;
586 }
587
588 static int passwd_logincont(void *obj, struct passwd **uam_pwd,
589                             char *ibuf, size_t ibuflen,
590                             char *rbuf, size_t *rbuflen)
591 {
592     u_int16_t retID;
593     int ret;
594
595     /* check for session id */
596     retID = ntohs(*(u_int16_t *)ibuf);
597     if (retID == ID)
598         ret = logincont1(obj, uam_pwd, ibuf, ibuflen, rbuf, rbuflen);
599     else if (retID == ID+1)
600         ret = logincont2(obj, uam_pwd, ibuf,ibuflen, rbuf, rbuflen);
601     else {
602         LOG(log_info, logtype_uams, "DHX2: Session ID Mismatch");
603         ret = AFPERR_PARAM;
604     }
605     return ret;
606 }
607
608 static int uam_setup(const char *path)
609 {
610     if (uam_register(UAM_SERVER_LOGIN_EXT, path, "DHX2", passwd_login,
611                      passwd_logincont, NULL, passwd_login_ext) < 0)
612         return -1;
613     return 0;
614 }
615
616 static void uam_cleanup(void)
617 {
618     uam_unregister(UAM_SERVER_LOGIN, "DHX2");
619 }
620
621
622 UAM_MODULE_EXPORT struct uam_export uams_dhx2 = {
623     UAM_MODULE_SERVER,
624     UAM_MODULE_VERSION,
625     uam_setup, uam_cleanup
626 };
627
628
629 UAM_MODULE_EXPORT struct uam_export uams_dhx2_passwd = {
630     UAM_MODULE_SERVER,
631     UAM_MODULE_VERSION,
632     uam_setup, uam_cleanup
633 };
634
635 #endif /* UAM_DHX2 */
636