Update copyright and licensing information.
[libradsec.git] / tlscommon.c
1 /* Copyright (c) 2006-2009, Stig Venaas, UNINETT AS.
2  * Copyright (c) 2010, UNINETT AS, NORDUnet A/S.
3  * Copyright (c) 2010-2012, NORDUnet A/S. */
4 /* See LICENSE for licensing information. */
5
6 #if defined(RADPROT_TLS) || defined(RADPROT_DTLS)
7 #include <signal.h>
8 #include <sys/socket.h>
9 #include <netinet/in.h>
10 #include <netdb.h>
11 #include <string.h>
12 #include <unistd.h>
13 #include <limits.h>
14 #ifdef SYS_SOLARIS9
15 #include <fcntl.h>
16 #endif
17 #include <sys/time.h>
18 #include <sys/types.h>
19 #include <sys/select.h>
20 #include <ctype.h>
21 #include <sys/wait.h>
22 #include <arpa/inet.h>
23 #include <regex.h>
24 #include <libgen.h>
25 #include <pthread.h>
26 #include <openssl/ssl.h>
27 #include <openssl/rand.h>
28 #include <openssl/err.h>
29 #include <openssl/md5.h>
30 #include <openssl/x509v3.h>
31 #include "debug.h"
32 #include "hash.h"
33 #include "util.h"
34 #include "hostport.h"
35 #include "radsecproxy.h"
36
37 static struct hash *tlsconfs = NULL;
38
39 static int pem_passwd_cb(char *buf, int size, int rwflag, void *userdata) {
40     int pwdlen = strlen(userdata);
41     if (rwflag != 0 || pwdlen > size) /* not for decryption or too large */
42         return 0;
43     memcpy(buf, userdata, pwdlen);
44     return pwdlen;
45 }
46
47 static int verify_cb(int ok, X509_STORE_CTX *ctx) {
48     char *buf = NULL;
49     X509 *err_cert;
50     int err, depth;
51
52     err_cert = X509_STORE_CTX_get_current_cert(ctx);
53     err = X509_STORE_CTX_get_error(ctx);
54     depth = X509_STORE_CTX_get_error_depth(ctx);
55
56     if (depth > MAX_CERT_DEPTH) {
57         ok = 0;
58         err = X509_V_ERR_CERT_CHAIN_TOO_LONG;
59         X509_STORE_CTX_set_error(ctx, err);
60     }
61
62     if (!ok) {
63         if (err_cert)
64             buf = X509_NAME_oneline(X509_get_subject_name(err_cert), NULL, 0);
65         debug(DBG_WARN, "verify error: num=%d:%s:depth=%d:%s", err, X509_verify_cert_error_string(err), depth, buf ? buf : "");
66         free(buf);
67         buf = NULL;
68
69         switch (err) {
70         case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
71             if (err_cert) {
72                 buf = X509_NAME_oneline(X509_get_issuer_name(err_cert), NULL, 0);
73                 if (buf) {
74                     debug(DBG_WARN, "\tIssuer=%s", buf);
75                     free(buf);
76                     buf = NULL;
77                 }
78             }
79             break;
80         case X509_V_ERR_CERT_NOT_YET_VALID:
81         case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
82             debug(DBG_WARN, "\tCertificate not yet valid");
83             break;
84         case X509_V_ERR_CERT_HAS_EXPIRED:
85             debug(DBG_WARN, "Certificate has expired");
86             break;
87         case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
88             debug(DBG_WARN, "Certificate no longer valid (after notAfter)");
89             break;
90         case X509_V_ERR_NO_EXPLICIT_POLICY:
91             debug(DBG_WARN, "No Explicit Certificate Policy");
92             break;
93         }
94     }
95 #ifdef DEBUG
96     printf("certificate verify returns %d\n", ok);
97 #endif
98     return ok;
99 }
100
101 #ifdef DEBUG
102 static void ssl_info_callback(const SSL *ssl, int where, int ret) {
103     const char *s;
104     int w;
105
106     w = where & ~SSL_ST_MASK;
107
108     if (w & SSL_ST_CONNECT)
109         s = "SSL_connect";
110     else if (w & SSL_ST_ACCEPT)
111         s = "SSL_accept";
112     else
113         s = "undefined";
114
115     if (where & SSL_CB_LOOP)
116         debug(DBG_DBG, "%s:%s\n", s, SSL_state_string_long(ssl));
117     else if (where & SSL_CB_ALERT) {
118         s = (where & SSL_CB_READ) ? "read" : "write";
119         debug(DBG_DBG, "SSL3 alert %s:%s:%s\n", s, SSL_alert_type_string_long(ret), SSL_alert_desc_string_long(ret));
120     }
121     else if (where & SSL_CB_EXIT) {
122         if (ret == 0)
123             debug(DBG_DBG, "%s:failed in %s\n", s, SSL_state_string_long(ssl));
124         else if (ret < 0)
125             debug(DBG_DBG, "%s:error in %s\n", s, SSL_state_string_long(ssl));
126     }
127 }
128 #endif
129
130 static X509_VERIFY_PARAM *createverifyparams(char **poids) {
131     X509_VERIFY_PARAM *pm;
132     ASN1_OBJECT *pobject;
133     int i;
134
135     pm = X509_VERIFY_PARAM_new();
136     if (!pm)
137         return NULL;
138
139     for (i = 0; poids[i]; i++) {
140         pobject = OBJ_txt2obj(poids[i], 0);
141         if (!pobject) {
142             X509_VERIFY_PARAM_free(pm);
143             return NULL;
144         }
145         X509_VERIFY_PARAM_add0_policy(pm, pobject);
146     }
147
148     X509_VERIFY_PARAM_set_flags(pm, X509_V_FLAG_POLICY_CHECK | X509_V_FLAG_EXPLICIT_POLICY);
149     return pm;
150 }
151
152 static int tlsaddcacrl(SSL_CTX *ctx, struct tls *conf) {
153     STACK_OF(X509_NAME) *calist;
154     X509_STORE *x509_s;
155     unsigned long error;
156
157     if (!SSL_CTX_load_verify_locations(ctx, conf->cacertfile, conf->cacertpath)) {
158         while ((error = ERR_get_error()))
159             debug(DBG_ERR, "SSL: %s", ERR_error_string(error, NULL));
160         debug(DBG_ERR, "tlsaddcacrl: Error updating TLS context %s", conf->name);
161         return 0;
162     }
163
164     calist = conf->cacertfile ? SSL_load_client_CA_file(conf->cacertfile) : NULL;
165     if (!conf->cacertfile || calist) {
166         if (conf->cacertpath) {
167             if (!calist)
168                 calist = sk_X509_NAME_new_null();
169             if (!SSL_add_dir_cert_subjects_to_stack(calist, conf->cacertpath)) {
170                 sk_X509_NAME_free(calist);
171                 calist = NULL;
172             }
173         }
174     }
175     if (!calist) {
176         while ((error = ERR_get_error()))
177             debug(DBG_ERR, "SSL: %s", ERR_error_string(error, NULL));
178         debug(DBG_ERR, "tlsaddcacrl: Error adding CA subjects in TLS context %s", conf->name);
179         return 0;
180     }
181     ERR_clear_error(); /* add_dir_cert_subj returns errors on success */
182     SSL_CTX_set_client_CA_list(ctx, calist);
183
184     SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb);
185     SSL_CTX_set_verify_depth(ctx, MAX_CERT_DEPTH + 1);
186
187     if (conf->crlcheck || conf->vpm) {
188         x509_s = SSL_CTX_get_cert_store(ctx);
189         if (conf->crlcheck)
190             X509_STORE_set_flags(x509_s, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL);
191         if (conf->vpm)
192             X509_STORE_set1_param(x509_s, conf->vpm);
193     }
194
195     debug(DBG_DBG, "tlsaddcacrl: updated TLS context %s", conf->name);
196     return 1;
197 }
198
199 static SSL_CTX *tlscreatectx(uint8_t type, struct tls *conf) {
200     SSL_CTX *ctx = NULL;
201     unsigned long error;
202     long sslversion = SSLeay();
203
204     switch (type) {
205 #ifdef RADPROT_TLS
206     case RAD_TLS:
207         ctx = SSL_CTX_new(TLSv1_method());
208 #ifdef DEBUG
209         SSL_CTX_set_info_callback(ctx, ssl_info_callback);
210 #endif
211         break;
212 #endif
213 #ifdef RADPROT_DTLS
214     case RAD_DTLS:
215         ctx = SSL_CTX_new(DTLSv1_method());
216 #ifdef DEBUG
217         SSL_CTX_set_info_callback(ctx, ssl_info_callback);
218 #endif
219         SSL_CTX_set_read_ahead(ctx, 1);
220         break;
221 #endif
222     }
223     if (!ctx) {
224         debug(DBG_ERR, "tlscreatectx: Error initialising SSL/TLS in TLS context %s", conf->name);
225         return NULL;
226     }
227
228     if (sslversion < 0x00908100L ||
229         (sslversion >= 0x10000000L && sslversion < 0x10000020L)) {
230         debug(DBG_WARN, "%s: %s seems to be of a version with a "
231               "certain security critical bug (fixed in OpenSSL 0.9.8p and "
232               "1.0.0b).  Disabling OpenSSL session caching for context %p.",
233               __func__, SSLeay_version(SSLEAY_VERSION), ctx);
234         SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
235     }
236
237     if (conf->certkeypwd) {
238         SSL_CTX_set_default_passwd_cb_userdata(ctx, conf->certkeypwd);
239         SSL_CTX_set_default_passwd_cb(ctx, pem_passwd_cb);
240     }
241     if (!SSL_CTX_use_certificate_chain_file(ctx, conf->certfile) ||
242         !SSL_CTX_use_PrivateKey_file(ctx, conf->certkeyfile, SSL_FILETYPE_PEM) ||
243         !SSL_CTX_check_private_key(ctx)) {
244         while ((error = ERR_get_error()))
245             debug(DBG_ERR, "SSL: %s", ERR_error_string(error, NULL));
246         debug(DBG_ERR, "tlscreatectx: Error initialising SSL/TLS in TLS context %s", conf->name);
247         SSL_CTX_free(ctx);
248         return NULL;
249     }
250
251     if (conf->policyoids) {
252         if (!conf->vpm) {
253             conf->vpm = createverifyparams(conf->policyoids);
254             if (!conf->vpm) {
255                 debug(DBG_ERR, "tlscreatectx: Failed to add policyOIDs in TLS context %s", conf->name);
256                 SSL_CTX_free(ctx);
257                 return NULL;
258             }
259         }
260     }
261
262     if (!tlsaddcacrl(ctx, conf)) {
263         if (conf->vpm) {
264             X509_VERIFY_PARAM_free(conf->vpm);
265             conf->vpm = NULL;
266         }
267         SSL_CTX_free(ctx);
268         return NULL;
269     }
270
271     debug(DBG_DBG, "tlscreatectx: created TLS context %s", conf->name);
272     return ctx;
273 }
274
275 struct tls *tlsgettls(char *alt1, char *alt2) {
276     struct tls *t;
277
278     t = hash_read(tlsconfs, alt1, strlen(alt1));
279     if (!t)
280         t = hash_read(tlsconfs, alt2, strlen(alt2));
281     return t;
282 }
283
284 SSL_CTX *tlsgetctx(uint8_t type, struct tls *t) {
285     struct timeval now;
286
287     if (!t)
288         return NULL;
289     gettimeofday(&now, NULL);
290
291     switch (type) {
292 #ifdef RADPROT_TLS
293     case RAD_TLS:
294         if (t->tlsexpiry && t->tlsctx) {
295             if (t->tlsexpiry < now.tv_sec) {
296                 t->tlsexpiry = now.tv_sec + t->cacheexpiry;
297                 tlsaddcacrl(t->tlsctx, t);
298             }
299         }
300         if (!t->tlsctx) {
301             t->tlsctx = tlscreatectx(RAD_TLS, t);
302             if (t->cacheexpiry)
303                 t->tlsexpiry = now.tv_sec + t->cacheexpiry;
304         }
305         return t->tlsctx;
306 #endif
307 #ifdef RADPROT_DTLS
308     case RAD_DTLS:
309         if (t->dtlsexpiry && t->dtlsctx) {
310             if (t->dtlsexpiry < now.tv_sec) {
311                 t->dtlsexpiry = now.tv_sec + t->cacheexpiry;
312                 tlsaddcacrl(t->dtlsctx, t);
313             }
314         }
315         if (!t->dtlsctx) {
316             t->dtlsctx = tlscreatectx(RAD_DTLS, t);
317             if (t->cacheexpiry)
318                 t->dtlsexpiry = now.tv_sec + t->cacheexpiry;
319         }
320         return t->dtlsctx;
321 #endif
322     }
323     return NULL;
324 }
325
326 X509 *verifytlscert(SSL *ssl) {
327     X509 *cert;
328     unsigned long error;
329
330     if (SSL_get_verify_result(ssl) != X509_V_OK) {
331         debug(DBG_ERR, "verifytlscert: basic validation failed");
332         while ((error = ERR_get_error()))
333             debug(DBG_ERR, "verifytlscert: TLS: %s", ERR_error_string(error, NULL));
334         return NULL;
335     }
336
337     cert = SSL_get_peer_certificate(ssl);
338     if (!cert)
339         debug(DBG_ERR, "verifytlscert: failed to obtain certificate");
340     return cert;
341 }
342
343 static int subjectaltnameaddr(X509 *cert, int family, struct in6_addr *addr) {
344     int loc, i, l, n, r = 0;
345     char *v;
346     X509_EXTENSION *ex;
347     STACK_OF(GENERAL_NAME) *alt;
348     GENERAL_NAME *gn;
349
350     debug(DBG_DBG, "subjectaltnameaddr");
351
352     loc = X509_get_ext_by_NID(cert, NID_subject_alt_name, -1);
353     if (loc < 0)
354         return r;
355
356     ex = X509_get_ext(cert, loc);
357     alt = X509V3_EXT_d2i(ex);
358     if (!alt)
359         return r;
360
361     n = sk_GENERAL_NAME_num(alt);
362     for (i = 0; i < n; i++) {
363         gn = sk_GENERAL_NAME_value(alt, i);
364         if (gn->type != GEN_IPADD)
365             continue;
366         r = -1;
367         v = (char *)ASN1_STRING_data(gn->d.ia5);
368         l = ASN1_STRING_length(gn->d.ia5);
369         if (((family == AF_INET && l == sizeof(struct in_addr)) || (family == AF_INET6 && l == sizeof(struct in6_addr)))
370             && !memcmp(v, &addr, l)) {
371             r = 1;
372             break;
373         }
374     }
375     GENERAL_NAMES_free(alt);
376     return r;
377 }
378
379 static int subjectaltnameregexp(X509 *cert, int type, char *exact,  regex_t *regex) {
380     int loc, i, l, n, r = 0;
381     char *s, *v;
382     X509_EXTENSION *ex;
383     STACK_OF(GENERAL_NAME) *alt;
384     GENERAL_NAME *gn;
385
386     debug(DBG_DBG, "subjectaltnameregexp");
387
388     loc = X509_get_ext_by_NID(cert, NID_subject_alt_name, -1);
389     if (loc < 0)
390         return r;
391
392     ex = X509_get_ext(cert, loc);
393     alt = X509V3_EXT_d2i(ex);
394     if (!alt)
395         return r;
396
397     n = sk_GENERAL_NAME_num(alt);
398     for (i = 0; i < n; i++) {
399         gn = sk_GENERAL_NAME_value(alt, i);
400         if (gn->type != type)
401             continue;
402         r = -1;
403         v = (char *)ASN1_STRING_data(gn->d.ia5);
404         l = ASN1_STRING_length(gn->d.ia5);
405         if (l <= 0)
406             continue;
407 #ifdef DEBUG
408         printfchars(NULL, gn->type == GEN_DNS ? "dns" : "uri", NULL, v, l);
409 #endif
410         if (exact) {
411             if (memcmp(v, exact, l))
412                 continue;
413         } else {
414             s = stringcopy((char *)v, l);
415             if (!s) {
416                 debug(DBG_ERR, "malloc failed");
417                 continue;
418             }
419             if (regexec(regex, s, 0, NULL, 0)) {
420                 free(s);
421                 continue;
422             }
423             free(s);
424         }
425         r = 1;
426         break;
427     }
428     GENERAL_NAMES_free(alt);
429     return r;
430 }
431
432 static int cnregexp(X509 *cert, char *exact, regex_t *regex) {
433     int loc, l;
434     char *v, *s;
435     X509_NAME *nm;
436     X509_NAME_ENTRY *e;
437     ASN1_STRING *t;
438
439     nm = X509_get_subject_name(cert);
440     loc = -1;
441     for (;;) {
442         loc = X509_NAME_get_index_by_NID(nm, NID_commonName, loc);
443         if (loc == -1)
444             break;
445         e = X509_NAME_get_entry(nm, loc);
446         t = X509_NAME_ENTRY_get_data(e);
447         v = (char *) ASN1_STRING_data(t);
448         l = ASN1_STRING_length(t);
449         if (l < 0)
450             continue;
451         if (exact) {
452             if (l == strlen(exact) && !strncasecmp(exact, v, l))
453                 return 1;
454         } else {
455             s = stringcopy((char *)v, l);
456             if (!s) {
457                 debug(DBG_ERR, "malloc failed");
458                 continue;
459             }
460             if (regexec(regex, s, 0, NULL, 0)) {
461                 free(s);
462                 continue;
463             }
464             free(s);
465             return 1;
466         }
467     }
468     return 0;
469 }
470
471 /* this is a bit sloppy, should not always accept match to any */
472 int certnamecheck(X509 *cert, struct list *hostports) {
473     struct list_node *entry;
474     struct hostportres *hp;
475     int r;
476     uint8_t type = 0; /* 0 for DNS, AF_INET for IPv4, AF_INET6 for IPv6 */
477     struct in6_addr addr;
478
479     for (entry = list_first(hostports); entry; entry = list_next(entry)) {
480         hp = (struct hostportres *)entry->data;
481         if (hp->prefixlen != 255) {
482             /* we disable the check for prefixes */
483             return 1;
484         }
485         if (inet_pton(AF_INET, hp->host, &addr))
486             type = AF_INET;
487         else if (inet_pton(AF_INET6, hp->host, &addr))
488             type = AF_INET6;
489         else
490             type = 0;
491
492         r = type ? subjectaltnameaddr(cert, type, &addr) : subjectaltnameregexp(cert, GEN_DNS, hp->host, NULL);
493         if (r) {
494             if (r > 0) {
495                 debug(DBG_DBG, "certnamecheck: Found subjectaltname matching %s %s", type ? "address" : "host", hp->host);
496                 return 1;
497             }
498             debug(DBG_WARN, "certnamecheck: No subjectaltname matching %s %s", type ? "address" : "host", hp->host);
499         } else {
500             if (cnregexp(cert, hp->host, NULL)) {
501                 debug(DBG_DBG, "certnamecheck: Found cn matching host %s", hp->host);
502                 return 1;
503             }
504             debug(DBG_WARN, "certnamecheck: cn not matching host %s", hp->host);
505         }
506     }
507     return 0;
508 }
509
510 int verifyconfcert(X509 *cert, struct clsrvconf *conf) {
511     if (conf->certnamecheck) {
512         if (!certnamecheck(cert, conf->hostports)) {
513             debug(DBG_WARN, "verifyconfcert: certificate name check failed");
514             return 0;
515         }
516         debug(DBG_WARN, "verifyconfcert: certificate name check ok");
517     }
518     if (conf->certcnregex) {
519         if (cnregexp(cert, NULL, conf->certcnregex) < 1) {
520             debug(DBG_WARN, "verifyconfcert: CN not matching regex");
521             return 0;
522         }
523         debug(DBG_DBG, "verifyconfcert: CN matching regex");
524     }
525     if (conf->certuriregex) {
526         if (subjectaltnameregexp(cert, GEN_URI, NULL, conf->certuriregex) < 1) {
527             debug(DBG_WARN, "verifyconfcert: subjectaltname URI not matching regex");
528             return 0;
529         }
530         debug(DBG_DBG, "verifyconfcert: subjectaltname URI matching regex");
531     }
532     return 1;
533 }
534
535 int conftls_cb(struct gconffile **cf, void *arg, char *block, char *opt, char *val) {
536     struct tls *conf;
537     long int expiry = LONG_MIN;
538
539     debug(DBG_DBG, "conftls_cb called for %s", block);
540
541     conf = malloc(sizeof(struct tls));
542     if (!conf) {
543         debug(DBG_ERR, "conftls_cb: malloc failed");
544         return 0;
545     }
546     memset(conf, 0, sizeof(struct tls));
547
548     if (!getgenericconfig(cf, block,
549                           "CACertificateFile", CONF_STR, &conf->cacertfile,
550                           "CACertificatePath", CONF_STR, &conf->cacertpath,
551                           "CertificateFile", CONF_STR, &conf->certfile,
552                           "CertificateKeyFile", CONF_STR, &conf->certkeyfile,
553                           "CertificateKeyPassword", CONF_STR, &conf->certkeypwd,
554                           "CacheExpiry", CONF_LINT, &expiry,
555                           "CRLCheck", CONF_BLN, &conf->crlcheck,
556                           "PolicyOID", CONF_MSTR, &conf->policyoids,
557                           NULL
558             )) {
559         debug(DBG_ERR, "conftls_cb: configuration error in block %s", val);
560         goto errexit;
561     }
562     if (!conf->certfile || !conf->certkeyfile) {
563         debug(DBG_ERR, "conftls_cb: TLSCertificateFile and TLSCertificateKeyFile must be specified in block %s", val);
564         goto errexit;
565     }
566     if (!conf->cacertfile && !conf->cacertpath) {
567         debug(DBG_ERR, "conftls_cb: CA Certificate file or path need to be specified in block %s", val);
568         goto errexit;
569     }
570     if (expiry != LONG_MIN) {
571         if (expiry < 0) {
572             debug(DBG_ERR, "error in block %s, value of option CacheExpiry is %ld, may not be negative", val, expiry);
573             goto errexit;
574         }
575         conf->cacheexpiry = expiry;
576     }
577
578     conf->name = stringcopy(val, 0);
579     if (!conf->name) {
580         debug(DBG_ERR, "conftls_cb: malloc failed");
581         goto errexit;
582     }
583
584     if (!tlsconfs)
585         tlsconfs = hash_create();
586     if (!hash_insert(tlsconfs, val, strlen(val), conf)) {
587         debug(DBG_ERR, "conftls_cb: malloc failed");
588         goto errexit;
589     }
590     if (!tlsgetctx(RAD_TLS, conf))
591         debug(DBG_ERR, "conftls_cb: error creating ctx for TLS block %s", val);
592     debug(DBG_DBG, "conftls_cb: added TLS block %s", val);
593     return 1;
594
595 errexit:
596     free(conf->cacertfile);
597     free(conf->cacertpath);
598     free(conf->certfile);
599     free(conf->certkeyfile);
600     free(conf->certkeypwd);
601     freegconfmstr(conf->policyoids);
602     free(conf);
603     return 0;
604 }
605
606 int addmatchcertattr(struct clsrvconf *conf) {
607     char *v;
608     regex_t **r;
609
610     if (!strncasecmp(conf->matchcertattr, "CN:/", 4)) {
611         r = &conf->certcnregex;
612         v = conf->matchcertattr + 4;
613     } else if (!strncasecmp(conf->matchcertattr, "SubjectAltName:URI:/", 20)) {
614         r = &conf->certuriregex;
615         v = conf->matchcertattr + 20;
616     } else
617         return 0;
618     if (!*v)
619         return 0;
620     /* regexp, remove optional trailing / if present */
621     if (v[strlen(v) - 1] == '/')
622         v[strlen(v) - 1] = '\0';
623     if (!*v)
624         return 0;
625
626     *r = malloc(sizeof(regex_t));
627     if (!*r) {
628         debug(DBG_ERR, "malloc failed");
629         return 0;
630     }
631     if (regcomp(*r, v, REG_EXTENDED | REG_ICASE | REG_NOSUB)) {
632         free(*r);
633         *r = NULL;
634         debug(DBG_ERR, "failed to compile regular expression %s", v);
635         return 0;
636     }
637     return 1;
638 }
639 #else
640 /* Just to makes file non-empty, should rather avoid compiling this file when not needed */
641 static void tlsdummy() {
642 }
643 #endif
644
645 /* Local Variables: */
646 /* c-file-style: "stroustrup" */
647 /* End: */