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