Commit | Line | Data |
---|---|---|
9d1c15ef JH |
1 | /************************************************* |
2 | * Exim - an Internet mail transport agent * | |
3 | *************************************************/ | |
4 | ||
f9ba5e22 | 5 | /* Copyright (c) Jeremy Harris 2014 - 2018 */ |
9d1c15ef JH |
6 | |
7 | /* This file provides TLS/SSL support for Exim using the GnuTLS library, | |
8 | one of the available supported implementations. This file is #included into | |
9 | tls.c when USE_GNUTLS has been set. | |
10 | */ | |
11 | ||
12 | #include <gnutls/gnutls.h> | |
13 | /* needed for cert checks in verification and DN extraction: */ | |
14 | #include <gnutls/x509.h> | |
15 | /* needed to disable PKCS11 autoload unless requested */ | |
16 | #if GNUTLS_VERSION_NUMBER >= 0x020c00 | |
17 | # include <gnutls/pkcs11.h> | |
18 | #endif | |
19 | ||
20 | ||
21 | /***************************************************** | |
22 | * Export/import a certificate, binary/printable | |
b2827658 JH |
23 | ****************************************************** |
24 | Return: boolean success | |
25 | */ | |
26 | ||
2944124c | 27 | BOOL |
9d1c15ef JH |
28 | tls_export_cert(uschar * buf, size_t buflen, void * cert) |
29 | { | |
30 | size_t sz = buflen; | |
f3ebb786 | 31 | rmark reset_point = store_mark(); |
2944124c | 32 | BOOL fail; |
55414b25 | 33 | const uschar * cp; |
9d1c15ef | 34 | |
c03fae8a JH |
35 | if ((fail = gnutls_x509_crt_export((gnutls_x509_crt_t)cert, |
36 | GNUTLS_X509_FMT_PEM, buf, &sz))) | |
37 | { | |
38 | log_write(0, LOG_MAIN, "TLS error in certificate export: %s", | |
39 | gnutls_strerror(fail)); | |
b2827658 | 40 | return FALSE; |
c03fae8a | 41 | } |
9d1c15ef JH |
42 | if ((cp = string_printing(buf)) != buf) |
43 | { | |
44 | Ustrncpy(buf, cp, buflen); | |
45 | if (buf[buflen-1]) | |
46 | fail = 1; | |
47 | } | |
48 | store_reset(reset_point); | |
2944124c | 49 | return !fail; |
9d1c15ef JH |
50 | } |
51 | ||
b2827658 JH |
52 | /* On error, NULL out the destination */ |
53 | BOOL | |
9d1c15ef JH |
54 | tls_import_cert(const uschar * buf, void ** cert) |
55 | { | |
f3ebb786 | 56 | rmark reset_point = store_mark(); |
9d1c15ef | 57 | gnutls_datum_t datum; |
152e7604 | 58 | gnutls_x509_crt_t crt = *(gnutls_x509_crt_t *)cert; |
b2827658 | 59 | int rc; |
9d1c15ef | 60 | |
152e7604 JH |
61 | if (crt) |
62 | gnutls_x509_crt_deinit(crt); | |
63 | else | |
64 | gnutls_global_init(); | |
65 | ||
9d1c15ef JH |
66 | gnutls_x509_crt_init(&crt); |
67 | ||
68 | datum.data = string_unprinting(US buf); | |
69 | datum.size = Ustrlen(datum.data); | |
b2827658 | 70 | if ((rc = gnutls_x509_crt_import(crt, &datum, GNUTLS_X509_FMT_PEM))) |
c03fae8a JH |
71 | { |
72 | log_write(0, LOG_MAIN, "TLS error in certificate import: %s", | |
b2827658 JH |
73 | gnutls_strerror(rc)); |
74 | crt = NULL; | |
c03fae8a | 75 | } |
b2827658 | 76 | *cert = (void *)crt; |
9d1c15ef | 77 | store_reset(reset_point); |
b2827658 | 78 | return rc != 0; |
9d1c15ef JH |
79 | } |
80 | ||
81 | void | |
790fbb71 | 82 | tls_free_cert(void ** cert) |
9d1c15ef | 83 | { |
790fbb71 JH |
84 | gnutls_x509_crt_t crt = *(gnutls_x509_crt_t *)cert; |
85 | if (crt) | |
86 | { | |
87 | gnutls_x509_crt_deinit(crt); | |
88 | gnutls_global_deinit(); | |
89 | *cert = NULL; | |
90 | } | |
9d1c15ef JH |
91 | } |
92 | ||
93 | /***************************************************** | |
94 | * Certificate field extraction routines | |
95 | *****************************************************/ | |
c03fae8a JH |
96 | |
97 | /* First, some internal service functions */ | |
98 | ||
8a6eec04 JH |
99 | static uschar * |
100 | g_err(const char * tag, const char * from, int gnutls_err) | |
101 | { | |
812a6045 JH |
102 | expand_string_message = string_sprintf("%s: %s fail: %s\n", |
103 | from, tag, gnutls_strerror(gnutls_err)); | |
8a6eec04 JH |
104 | return NULL; |
105 | } | |
106 | ||
107 | ||
9d1c15ef | 108 | static uschar * |
25ba2544 | 109 | time_copy(time_t t, uschar * mod) |
9d1c15ef | 110 | { |
25ba2544 | 111 | uschar * cp; |
e9477a08 | 112 | size_t len = 32; |
25ba2544 JH |
113 | |
114 | if (mod && Ustrcmp(mod, "int") == 0) | |
115 | return string_sprintf("%u", (unsigned)t); | |
116 | ||
f3ebb786 | 117 | cp = store_get(len, FALSE); |
8768d548 | 118 | if (f.timestamps_utc) |
e9477a08 | 119 | { |
45500060 | 120 | uschar * tz = to_tz(US"GMT0"); |
e9477a08 JH |
121 | len = strftime(CS cp, len, "%b %e %T %Y %Z", gmtime(&t)); |
122 | restore_tz(tz); | |
123 | } | |
124 | else | |
125 | len = strftime(CS cp, len, "%b %e %T %Y %Z", localtime(&t)); | |
9d1c15ef JH |
126 | return len > 0 ? cp : NULL; |
127 | } | |
128 | ||
c03fae8a | 129 | |
9d1c15ef | 130 | /**/ |
c03fae8a JH |
131 | /* Now the extractors, called from expand.c |
132 | Arguments: | |
133 | cert The certificate | |
134 | mod Optional modifiers for the operator | |
135 | ||
136 | Return: | |
137 | Allocated string with extracted value | |
138 | */ | |
9d1c15ef JH |
139 | |
140 | uschar * | |
9e4dddbd | 141 | tls_cert_issuer(void * cert, uschar * mod) |
9d1c15ef | 142 | { |
812a6045 JH |
143 | uschar * cp = NULL; |
144 | int ret; | |
145 | size_t siz = 0; | |
146 | ||
76075bb5 | 147 | if ((ret = gnutls_x509_crt_get_issuer_dn(cert, CS cp, &siz)) |
812a6045 JH |
148 | != GNUTLS_E_SHORT_MEMORY_BUFFER) |
149 | return g_err("gi0", __FUNCTION__, ret); | |
150 | ||
f3ebb786 | 151 | cp = store_get(siz, TRUE); |
76075bb5 | 152 | if ((ret = gnutls_x509_crt_get_issuer_dn(cert, CS cp, &siz)) < 0) |
812a6045 JH |
153 | return g_err("gi1", __FUNCTION__, ret); |
154 | ||
155 | return mod ? tls_field_from_dn(cp, mod) : cp; | |
9d1c15ef JH |
156 | } |
157 | ||
158 | uschar * | |
9e4dddbd | 159 | tls_cert_not_after(void * cert, uschar * mod) |
9d1c15ef JH |
160 | { |
161 | return time_copy( | |
25ba2544 JH |
162 | gnutls_x509_crt_get_expiration_time((gnutls_x509_crt_t)cert), |
163 | mod); | |
9d1c15ef JH |
164 | } |
165 | ||
166 | uschar * | |
9e4dddbd | 167 | tls_cert_not_before(void * cert, uschar * mod) |
9d1c15ef JH |
168 | { |
169 | return time_copy( | |
25ba2544 JH |
170 | gnutls_x509_crt_get_activation_time((gnutls_x509_crt_t)cert), |
171 | mod); | |
9d1c15ef JH |
172 | } |
173 | ||
174 | uschar * | |
9e4dddbd | 175 | tls_cert_serial_number(void * cert, uschar * mod) |
9d1c15ef JH |
176 | { |
177 | uschar bin[50], txt[150]; | |
d7978c0f | 178 | uschar * sp = bin; |
9d1c15ef | 179 | size_t sz = sizeof(bin); |
c03fae8a JH |
180 | int ret; |
181 | ||
182 | if ((ret = gnutls_x509_crt_get_serial((gnutls_x509_crt_t)cert, | |
183 | bin, &sz))) | |
184 | return g_err("gs0", __FUNCTION__, ret); | |
9d1c15ef | 185 | |
d7978c0f | 186 | for(uschar * dp = txt; sz; sz--) |
5976eb99 | 187 | dp += sprintf(CS dp, "%.2x", *sp++); |
9d1c15ef JH |
188 | for(sp = txt; sp[0]=='0' && sp[1]; ) sp++; /* leading zeroes */ |
189 | return string_copy(sp); | |
190 | } | |
191 | ||
192 | uschar * | |
9e4dddbd | 193 | tls_cert_signature(void * cert, uschar * mod) |
9d1c15ef | 194 | { |
69cbeaec | 195 | uschar * cp1 = NULL; |
9d1c15ef JH |
196 | uschar * cp2; |
197 | uschar * cp3; | |
198 | size_t len = 0; | |
199 | int ret; | |
200 | ||
76075bb5 | 201 | if ((ret = gnutls_x509_crt_get_signature((gnutls_x509_crt_t)cert, CS cp1, &len)) |
812a6045 JH |
202 | != GNUTLS_E_SHORT_MEMORY_BUFFER) |
203 | return g_err("gs0", __FUNCTION__, ret); | |
9d1c15ef | 204 | |
f3ebb786 | 205 | cp1 = store_get(len*4+1, TRUE); |
76075bb5 | 206 | if (gnutls_x509_crt_get_signature((gnutls_x509_crt_t)cert, CS cp1, &len) != 0) |
812a6045 | 207 | return g_err("gs1", __FUNCTION__, ret); |
9d1c15ef | 208 | |
5976eb99 JH |
209 | for(cp3 = cp2 = cp1+len; cp1 < cp2; cp1++) |
210 | cp3 += sprintf(CS cp3, "%.2x ", *cp1); | |
9d1c15ef JH |
211 | cp3[-1]= '\0'; |
212 | ||
213 | return cp2; | |
214 | } | |
215 | ||
216 | uschar * | |
9e4dddbd | 217 | tls_cert_signature_algorithm(void * cert, uschar * mod) |
9d1c15ef JH |
218 | { |
219 | gnutls_sign_algorithm_t algo = | |
220 | gnutls_x509_crt_get_signature_algorithm((gnutls_x509_crt_t)cert); | |
76075bb5 | 221 | return algo < 0 ? NULL : string_copy(US gnutls_sign_get_name(algo)); |
9d1c15ef JH |
222 | } |
223 | ||
224 | uschar * | |
9e4dddbd | 225 | tls_cert_subject(void * cert, uschar * mod) |
9d1c15ef | 226 | { |
8a6eec04 JH |
227 | uschar * cp = NULL; |
228 | int ret; | |
229 | size_t siz = 0; | |
230 | ||
76075bb5 | 231 | if ((ret = gnutls_x509_crt_get_dn(cert, CS cp, &siz)) |
812a6045 JH |
232 | != GNUTLS_E_SHORT_MEMORY_BUFFER) |
233 | return g_err("gs0", __FUNCTION__, ret); | |
8a6eec04 | 234 | |
f3ebb786 | 235 | cp = store_get(siz, TRUE); |
76075bb5 | 236 | if ((ret = gnutls_x509_crt_get_dn(cert, CS cp, &siz)) < 0) |
812a6045 | 237 | return g_err("gs1", __FUNCTION__, ret); |
8a6eec04 | 238 | |
812a6045 | 239 | return mod ? tls_field_from_dn(cp, mod) : cp; |
9d1c15ef JH |
240 | } |
241 | ||
242 | uschar * | |
9e4dddbd | 243 | tls_cert_version(void * cert, uschar * mod) |
9d1c15ef JH |
244 | { |
245 | return string_sprintf("%d", gnutls_x509_crt_get_version(cert)); | |
246 | } | |
247 | ||
248 | uschar * | |
249 | tls_cert_ext_by_oid(void * cert, uschar * oid, int idx) | |
250 | { | |
251 | uschar * cp1 = NULL; | |
252 | uschar * cp2; | |
253 | uschar * cp3; | |
254 | size_t siz = 0; | |
255 | unsigned int crit; | |
256 | int ret; | |
257 | ||
258 | ret = gnutls_x509_crt_get_extension_by_oid ((gnutls_x509_crt_t)cert, | |
48224640 | 259 | CS oid, idx, CS cp1, &siz, &crit); |
9d1c15ef | 260 | if (ret != GNUTLS_E_SHORT_MEMORY_BUFFER) |
8a6eec04 | 261 | return g_err("ge0", __FUNCTION__, ret); |
9d1c15ef | 262 | |
f3ebb786 | 263 | cp1 = store_get(siz*4 + 1, TRUE); |
9d1c15ef JH |
264 | |
265 | ret = gnutls_x509_crt_get_extension_by_oid ((gnutls_x509_crt_t)cert, | |
48224640 | 266 | CS oid, idx, CS cp1, &siz, &crit); |
9d1c15ef | 267 | if (ret < 0) |
8a6eec04 | 268 | return g_err("ge1", __FUNCTION__, ret); |
9d1c15ef JH |
269 | |
270 | /* binary data, DER encoded */ | |
271 | ||
272 | /* just dump for now */ | |
5976eb99 JH |
273 | for(cp3 = cp2 = cp1+siz; cp1 < cp2; cp1++) |
274 | cp3 += sprintf(CS cp3, "%.2x ", *cp1); | |
9d1c15ef JH |
275 | cp3[-1]= '\0'; |
276 | ||
277 | return cp2; | |
278 | } | |
279 | ||
280 | uschar * | |
9e4dddbd | 281 | tls_cert_subject_altname(void * cert, uschar * mod) |
9d1c15ef | 282 | { |
acec9514 | 283 | gstring * list = NULL; |
9e4dddbd | 284 | size_t siz; |
9d1c15ef | 285 | int ret; |
9e4dddbd JH |
286 | uschar sep = '\n'; |
287 | uschar * tag = US""; | |
288 | uschar * ele; | |
289 | int match = -1; | |
9d1c15ef | 290 | |
9e4dddbd | 291 | while (mod) |
9d1c15ef | 292 | { |
9e4dddbd JH |
293 | if (*mod == '>' && *++mod) sep = *mod++; |
294 | else if (Ustrcmp(mod, "dns")==0) { match = GNUTLS_SAN_DNSNAME; mod += 3; } | |
295 | else if (Ustrcmp(mod, "uri")==0) { match = GNUTLS_SAN_URI; mod += 3; } | |
296 | else if (Ustrcmp(mod, "mail")==0) { match = GNUTLS_SAN_RFC822NAME; mod += 4; } | |
297 | else continue; | |
298 | ||
299 | if (*mod++ != ',') | |
9d1c15ef | 300 | break; |
9d1c15ef JH |
301 | } |
302 | ||
d7978c0f | 303 | for (int index = 0;; index++) |
9d1c15ef | 304 | { |
9e4dddbd JH |
305 | siz = 0; |
306 | switch(ret = gnutls_x509_crt_get_subject_alt_name( | |
e51c7be2 | 307 | (gnutls_x509_crt_t)cert, index, NULL, &siz, NULL)) |
9e4dddbd JH |
308 | { |
309 | case GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE: | |
acec9514 | 310 | return string_from_gstring(list); /* no more elements; normal exit */ |
9e4dddbd JH |
311 | |
312 | case GNUTLS_E_SHORT_MEMORY_BUFFER: | |
313 | break; | |
314 | ||
315 | default: | |
8a6eec04 | 316 | return g_err("gs0", __FUNCTION__, ret); |
9e4dddbd JH |
317 | } |
318 | ||
f3ebb786 | 319 | ele = store_get(siz+1, TRUE); |
9e4dddbd JH |
320 | if ((ret = gnutls_x509_crt_get_subject_alt_name( |
321 | (gnutls_x509_crt_t)cert, index, ele, &siz, NULL)) < 0) | |
8a6eec04 | 322 | return g_err("gs1", __FUNCTION__, ret); |
9e4dddbd JH |
323 | ele[siz] = '\0'; |
324 | ||
e51c7be2 JH |
325 | if ( match != -1 && match != ret /* wrong type of SAN */ |
326 | || Ustrlen(ele) != siz) /* contains a NUL */ | |
9e4dddbd JH |
327 | continue; |
328 | switch (ret) | |
329 | { | |
330 | case GNUTLS_SAN_DNSNAME: tag = US"DNS"; break; | |
94431adb | 331 | case GNUTLS_SAN_URI: tag = US"URI"; break; |
9e4dddbd JH |
332 | case GNUTLS_SAN_RFC822NAME: tag = US"MAIL"; break; |
333 | default: continue; /* ignore unrecognised types */ | |
334 | } | |
acec9514 | 335 | list = string_append_listele(list, sep, |
9e4dddbd | 336 | match == -1 ? string_sprintf("%s=%s", tag, ele) : ele); |
9d1c15ef | 337 | } |
9e4dddbd | 338 | /*NOTREACHED*/ |
9d1c15ef JH |
339 | } |
340 | ||
341 | uschar * | |
9e4dddbd | 342 | tls_cert_ocsp_uri(void * cert, uschar * mod) |
9d1c15ef JH |
343 | { |
344 | #if GNUTLS_VERSION_NUMBER >= 0x030000 | |
345 | gnutls_datum_t uri; | |
9e4dddbd JH |
346 | int ret; |
347 | uschar sep = '\n'; | |
acec9514 | 348 | gstring * list = NULL; |
9e4dddbd JH |
349 | |
350 | if (mod) | |
351 | if (*mod == '>' && *++mod) sep = *mod++; | |
9d1c15ef | 352 | |
d7978c0f | 353 | for (int index = 0;; index++) |
9e4dddbd JH |
354 | { |
355 | ret = gnutls_x509_crt_get_authority_info_access((gnutls_x509_crt_t)cert, | |
356 | index, GNUTLS_IA_OCSP_URI, &uri, NULL); | |
9d1c15ef | 357 | |
9e4dddbd | 358 | if (ret == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) |
acec9514 | 359 | return string_from_gstring(list); |
9e4dddbd | 360 | if (ret < 0) |
8a6eec04 | 361 | return g_err("gai", __FUNCTION__, ret); |
9d1c15ef | 362 | |
acec9514 | 363 | list = string_append_listele_n(list, sep, uri.data, uri.size); |
9e4dddbd JH |
364 | } |
365 | /*NOTREACHED*/ | |
9d1c15ef JH |
366 | |
367 | #else | |
368 | ||
94431adb | 369 | expand_string_message = |
9d1c15ef JH |
370 | string_sprintf("%s: OCSP support with GnuTLS requires version 3.0.0\n", |
371 | __FUNCTION__); | |
372 | return NULL; | |
373 | ||
374 | #endif | |
375 | } | |
376 | ||
377 | uschar * | |
9e4dddbd | 378 | tls_cert_crl_uri(void * cert, uschar * mod) |
9d1c15ef JH |
379 | { |
380 | int ret; | |
9e4dddbd | 381 | uschar sep = '\n'; |
acec9514 | 382 | gstring * list = NULL; |
9e4dddbd JH |
383 | uschar * ele; |
384 | ||
385 | if (mod) | |
386 | if (*mod == '>' && *++mod) sep = *mod++; | |
9d1c15ef | 387 | |
d7978c0f | 388 | for (int index = 0;; index++) |
9d1c15ef | 389 | { |
d7978c0f | 390 | size_t siz = 0; |
9e4dddbd JH |
391 | switch(ret = gnutls_x509_crt_get_crl_dist_points( |
392 | (gnutls_x509_crt_t)cert, index, NULL, &siz, NULL, NULL)) | |
393 | { | |
394 | case GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE: | |
acec9514 | 395 | return string_from_gstring(list); |
9e4dddbd JH |
396 | case GNUTLS_E_SHORT_MEMORY_BUFFER: |
397 | break; | |
398 | default: | |
8a6eec04 | 399 | return g_err("gc0", __FUNCTION__, ret); |
9e4dddbd JH |
400 | } |
401 | ||
f3ebb786 | 402 | ele = store_get(siz, TRUE); |
9e4dddbd | 403 | if ((ret = gnutls_x509_crt_get_crl_dist_points( |
8a6eec04 JH |
404 | (gnutls_x509_crt_t)cert, index, ele, &siz, NULL, NULL)) < 0) |
405 | return g_err("gc1", __FUNCTION__, ret); | |
406 | ||
acec9514 | 407 | list = string_append_listele_n(list, sep, ele, siz); |
9d1c15ef | 408 | } |
9e4dddbd | 409 | /*NOTREACHED*/ |
9d1c15ef JH |
410 | } |
411 | ||
412 | ||
6a8a60e0 JH |
413 | /***************************************************** |
414 | * Certificate operator routines | |
415 | *****************************************************/ | |
59b87190 JH |
416 | uschar * |
417 | tls_cert_der_b64(void * cert) | |
418 | { | |
419 | size_t len = 0; | |
420 | uschar * cp = NULL; | |
421 | int fail; | |
422 | ||
423 | if ( (fail = gnutls_x509_crt_export((gnutls_x509_crt_t)cert, | |
424 | GNUTLS_X509_FMT_DER, cp, &len)) != GNUTLS_E_SHORT_MEMORY_BUFFER | |
f3ebb786 | 425 | || !(cp = store_get((int)len, TRUE), TRUE) /* tainted */ |
59b87190 JH |
426 | || (fail = gnutls_x509_crt_export((gnutls_x509_crt_t)cert, |
427 | GNUTLS_X509_FMT_DER, cp, &len)) | |
428 | ) | |
429 | { | |
430 | log_write(0, LOG_MAIN, "TLS error in certificate export: %s", | |
431 | gnutls_strerror(fail)); | |
432 | return NULL; | |
433 | } | |
1f20760b | 434 | return b64encode(CUS cp, (int)len); |
59b87190 JH |
435 | } |
436 | ||
437 | ||
6a8a60e0 JH |
438 | static uschar * |
439 | fingerprint(gnutls_x509_crt_t cert, gnutls_digest_algorithm_t algo) | |
440 | { | |
441 | int ret; | |
442 | size_t siz = 0; | |
443 | uschar * cp; | |
444 | uschar * cp2; | |
6a8a60e0 JH |
445 | |
446 | if ((ret = gnutls_x509_crt_get_fingerprint(cert, algo, NULL, &siz)) | |
447 | != GNUTLS_E_SHORT_MEMORY_BUFFER) | |
8a6eec04 JH |
448 | return g_err("gf0", __FUNCTION__, ret); |
449 | ||
f3ebb786 | 450 | cp = store_get(siz*3+1, TRUE); |
6a8a60e0 | 451 | if ((ret = gnutls_x509_crt_get_fingerprint(cert, algo, cp, &siz)) < 0) |
8a6eec04 JH |
452 | return g_err("gf1", __FUNCTION__, ret); |
453 | ||
d7978c0f | 454 | for (uschar * cp3 = cp2 = cp+siz; cp < cp2; cp++) |
5976eb99 | 455 | cp3 += sprintf(CS cp3, "%02X", *cp); |
6a8a60e0 JH |
456 | return cp2; |
457 | } | |
458 | ||
459 | ||
460 | uschar * | |
461 | tls_cert_fprt_md5(void * cert) | |
462 | { | |
463 | return fingerprint((gnutls_x509_crt_t)cert, GNUTLS_DIG_MD5); | |
464 | } | |
465 | ||
466 | uschar * | |
467 | tls_cert_fprt_sha1(void * cert) | |
468 | { | |
469 | return fingerprint((gnutls_x509_crt_t)cert, GNUTLS_DIG_SHA1); | |
470 | } | |
471 | ||
9ef9101c JH |
472 | uschar * |
473 | tls_cert_fprt_sha256(void * cert) | |
474 | { | |
475 | return fingerprint((gnutls_x509_crt_t)cert, GNUTLS_DIG_SHA256); | |
476 | } | |
477 | ||
6a8a60e0 | 478 | |
9d1c15ef JH |
479 | /* vi: aw ai sw=2 |
480 | */ | |
481 | /* End of tlscert-gnu.c */ |