2007-05-03 [wwp] 2.9.1cvs41
[claws.git] / src / common / ssl_certificate.c
1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2007 Colin Leroy <colin@colino.net> 
4  * and the Claws Mail team
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19  */
20
21 #ifdef HAVE_CONFIG_H
22 #  include "config.h"
23 #endif
24
25 #if USE_OPENSSL
26
27 #include <openssl/ssl.h>
28 #include <glib.h>
29 #include <glib/gi18n.h>
30
31 #include "ssl_certificate.h"
32 #include "utils.h"
33 #include "log.h"
34 #include "socket.h"
35 #include "hooks.h"
36
37 static GHashTable *warned_expired = NULL;
38
39 gboolean prefs_common_unsafe_ssl_certs(void);
40
41 static gchar *get_certificate_path(const gchar *host, const gchar *port, const gchar *fp)
42 {
43         if (fp != NULL && prefs_common_unsafe_ssl_certs())
44                 return g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, 
45                           "certs", G_DIR_SEPARATOR_S,
46                           host, ".", port, ".", fp, ".cert", NULL);
47         else 
48                 return g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, 
49                           "certs", G_DIR_SEPARATOR_S,
50                           host, ".", port, ".cert", NULL);
51 }
52
53 static SSLCertificate *ssl_certificate_new_lookup(X509 *x509_cert, gchar *host, gushort port, gboolean lookup);
54
55 /* from Courier */
56 time_t asn1toTime(ASN1_TIME *asn1Time)
57 {
58         struct tm tm;
59         int offset;
60
61         if (asn1Time == NULL || asn1Time->length < 13)
62                 return 0;
63
64         memset(&tm, 0, sizeof(tm));
65
66 #define N2(n)   ((asn1Time->data[n]-'0')*10 + asn1Time->data[(n)+1]-'0')
67
68 #define CPY(f,n) (tm.f=N2(n))
69
70         CPY(tm_year,0);
71
72         if(tm.tm_year < 50)
73                 tm.tm_year += 100; /* Sux */
74
75         CPY(tm_mon, 2);
76         --tm.tm_mon;
77         CPY(tm_mday, 4);
78         CPY(tm_hour, 6);
79         CPY(tm_min, 8);
80         CPY(tm_sec, 10);
81
82         offset=0;
83
84         if (asn1Time->data[12] != 'Z')
85         {
86                 if (asn1Time->length < 17)
87                         return 0;
88
89                 offset=N2(13)*3600+N2(15)*60;
90
91                 if (asn1Time->data[12] == '-')
92                         offset= -offset;
93         }
94
95 #undef N2
96 #undef CPY
97
98         return mktime(&tm)-offset;
99 }
100
101 static char * get_fqdn(char *host)
102 {
103         struct hostent *hp;
104
105         if (host == NULL || strlen(host) == 0)
106                 return g_strdup("");
107
108         hp = my_gethostbyname(host);
109         if (hp == NULL)
110                 return g_strdup(host); /*caller should free*/
111         else 
112                 return g_strdup(hp->h_name);
113 }
114
115 char * readable_fingerprint(unsigned char *src, int len) 
116 {
117         int i=0;
118         char * ret;
119         
120         if (src == NULL)
121                 return NULL;
122         ret = g_strdup("");
123         while (i < len) {
124                 char *tmp2;
125                 if(i>0)
126                         tmp2 = g_strdup_printf("%s:%02X", ret, src[i]);
127                 else
128                         tmp2 = g_strdup_printf("%02X", src[i]);
129                 g_free(ret);
130                 ret = g_strdup(tmp2);
131                 g_free(tmp2);
132                 i++;
133         }
134         return ret;
135 }
136
137 static SSLCertificate *ssl_certificate_new_lookup(X509 *x509_cert, gchar *host, gushort port, gboolean lookup)
138 {
139         SSLCertificate *cert = g_new0(SSLCertificate, 1);
140         unsigned int n;
141         unsigned char md[EVP_MAX_MD_SIZE];      
142         
143         if (host == NULL || x509_cert == NULL) {
144                 ssl_certificate_destroy(cert);
145                 return NULL;
146         }
147         cert->x509_cert = X509_dup(x509_cert);
148         if (lookup)
149                 cert->host = get_fqdn(host);
150         else
151                 cert->host = g_strdup(host);
152         cert->port = port;
153         
154         /* fingerprint */
155         X509_digest(cert->x509_cert, EVP_md5(), md, &n);
156         cert->fingerprint = readable_fingerprint(md, (int)n);
157
158         return cert;
159 }
160
161 static void ssl_certificate_save (SSLCertificate *cert)
162 {
163         gchar *file, *port;
164         FILE *fp;
165
166         file = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, 
167                           "certs", G_DIR_SEPARATOR_S, NULL);
168         
169         if (!is_dir_exist(file))
170                 make_dir_hier(file);
171         g_free(file);
172
173         port = g_strdup_printf("%d", cert->port);
174         file = get_certificate_path(cert->host, port, cert->fingerprint);
175
176         g_free(port);
177         fp = g_fopen(file, "wb");
178         if (fp == NULL) {
179                 g_free(file);
180                 debug_print("Can't save certificate !\n");
181                 return;
182         }
183         i2d_X509_fp(fp, cert->x509_cert);
184         g_free(file);
185         fclose(fp);
186
187 }
188
189 char* ssl_certificate_to_string(SSLCertificate *cert)
190 {
191         char *ret, buf[100];
192         char *issuer_commonname, *issuer_location, *issuer_organization;
193         char *subject_commonname, *subject_location, *subject_organization;
194         char *fingerprint, *sig_status;
195         unsigned int n;
196         unsigned char md[EVP_MAX_MD_SIZE];      
197         
198         /* issuer */    
199         if (X509_NAME_get_text_by_NID(X509_get_issuer_name(cert->x509_cert), 
200                                        NID_commonName, buf, 100) >= 0)
201                 issuer_commonname = g_strdup(buf);
202         else
203                 issuer_commonname = g_strdup(_("<not in certificate>"));
204         if (X509_NAME_get_text_by_NID(X509_get_issuer_name(cert->x509_cert), 
205                                        NID_localityName, buf, 100) >= 0) {
206                 issuer_location = g_strdup(buf);
207                 if (X509_NAME_get_text_by_NID(X509_get_issuer_name(cert->x509_cert), 
208                                        NID_countryName, buf, 100) >= 0)
209                         issuer_location = g_strconcat(issuer_location,", ",buf, NULL);
210         } else if (X509_NAME_get_text_by_NID(X509_get_issuer_name(cert->x509_cert), 
211                                        NID_countryName, buf, 100) >= 0)
212                 issuer_location = g_strdup(buf);
213         else
214                 issuer_location = g_strdup(_("<not in certificate>"));
215
216         if (X509_NAME_get_text_by_NID(X509_get_issuer_name(cert->x509_cert), 
217                                        NID_organizationName, buf, 100) >= 0)
218                 issuer_organization = g_strdup(buf);
219         else 
220                 issuer_organization = g_strdup(_("<not in certificate>"));
221          
222         /* subject */   
223         if (X509_NAME_get_text_by_NID(X509_get_subject_name(cert->x509_cert), 
224                                        NID_commonName, buf, 100) >= 0)
225                 subject_commonname = g_strdup(buf);
226         else
227                 subject_commonname = g_strdup(_("<not in certificate>"));
228         if (X509_NAME_get_text_by_NID(X509_get_subject_name(cert->x509_cert), 
229                                        NID_localityName, buf, 100) >= 0) {
230                 subject_location = g_strdup(buf);
231                 if (X509_NAME_get_text_by_NID(X509_get_subject_name(cert->x509_cert), 
232                                        NID_countryName, buf, 100) >= 0)
233                         subject_location = g_strconcat(subject_location,", ",buf, NULL);
234         } else if (X509_NAME_get_text_by_NID(X509_get_subject_name(cert->x509_cert), 
235                                        NID_countryName, buf, 100) >= 0)
236                 subject_location = g_strdup(buf);
237         else
238                 subject_location = g_strdup(_("<not in certificate>"));
239
240         if (X509_NAME_get_text_by_NID(X509_get_subject_name(cert->x509_cert), 
241                                        NID_organizationName, buf, 100) >= 0)
242                 subject_organization = g_strdup(buf);
243         else 
244                 subject_organization = g_strdup(_("<not in certificate>"));
245          
246         /* fingerprint */
247         X509_digest(cert->x509_cert, EVP_md5(), md, &n);
248         fingerprint = readable_fingerprint(md, (int)n);
249
250         /* signature */
251         sig_status = ssl_certificate_check_signer(cert->x509_cert);
252
253         ret = g_strdup_printf(_("  Owner: %s (%s) in %s\n  Signed by: %s (%s) in %s\n  Fingerprint: %s\n  Signature status: %s"),
254                                 subject_commonname, subject_organization, subject_location, 
255                                 issuer_commonname, issuer_organization, issuer_location, 
256                                 fingerprint,
257                                 (sig_status==NULL ? "correct":sig_status));
258
259         g_free(issuer_commonname);
260         g_free(issuer_location);
261         g_free(issuer_organization);
262         g_free(subject_commonname);
263         g_free(subject_location);
264         g_free(subject_organization);
265         g_free(fingerprint);
266         g_free(sig_status);
267         return ret;
268 }
269         
270 void ssl_certificate_destroy(SSLCertificate *cert) 
271 {
272         if (cert == NULL)
273                 return;
274
275         if (cert->x509_cert)
276                 X509_free(cert->x509_cert);
277         g_free(cert->host);
278         g_free(cert->fingerprint);
279         g_free(cert);
280         cert = NULL;
281 }
282
283 void ssl_certificate_delete_from_disk(SSLCertificate *cert)
284 {
285         gchar *buf;
286         gchar *file;
287         buf = g_strdup_printf("%d", cert->port);
288         file = get_certificate_path(cert->host, buf, cert->fingerprint);
289         g_unlink (file);
290         g_free(file);
291         g_free(buf);
292 }
293
294 SSLCertificate *ssl_certificate_find (gchar *host, gushort port, const gchar *fingerprint)
295 {
296         return ssl_certificate_find_lookup (host, port, fingerprint, TRUE);
297 }
298
299 SSLCertificate *ssl_certificate_find_lookup (gchar *host, gushort port, const gchar *fingerprint, gboolean lookup)
300 {
301         gchar *file = NULL;
302         gchar *buf;
303         gchar *fqdn_host;
304         SSLCertificate *cert = NULL;
305         X509 *tmp_x509;
306         FILE *fp = NULL;
307         gboolean must_rename = FALSE;
308
309         if (lookup)
310                 fqdn_host = get_fqdn(host);
311         else
312                 fqdn_host = g_strdup(host);
313
314         buf = g_strdup_printf("%d", port);
315         
316         if (fingerprint != NULL) {
317                 file = get_certificate_path(fqdn_host, buf, fingerprint);
318                 fp = g_fopen(file, "rb");
319         }
320         if (fp == NULL) {
321                 /* see if we have the old one */
322                 g_free(file);
323                 file = get_certificate_path(fqdn_host, buf, NULL);
324                 fp = g_fopen(file, "rb");
325
326                 if (fp)
327                         must_rename = (fingerprint != NULL);
328         }
329         if (fp == NULL) {
330                 g_free(file);
331                 g_free(fqdn_host);
332                 g_free(buf);
333                 return NULL;
334         }
335         
336         if ((tmp_x509 = d2i_X509_fp(fp, 0)) != NULL) {
337                 cert = ssl_certificate_new_lookup(tmp_x509, fqdn_host, port, lookup);
338                 X509_free(tmp_x509);
339         }
340         fclose(fp);
341         g_free(file);
342         
343         if (must_rename) {
344                 gchar *old = get_certificate_path(fqdn_host, buf, NULL);
345                 gchar *new = get_certificate_path(fqdn_host, buf, fingerprint);
346                 if (strcmp(old, new))
347                         move_file(old, new, TRUE);
348                 g_free(old);
349                 g_free(new);
350         }
351         g_free(buf);
352         g_free(fqdn_host);
353
354         return cert;
355 }
356
357 static gboolean ssl_certificate_compare (SSLCertificate *cert_a, SSLCertificate *cert_b)
358 {
359         if (cert_a == NULL || cert_b == NULL)
360                 return FALSE;
361         else if (!X509_cmp(cert_a->x509_cert, cert_b->x509_cert))
362                 return TRUE;
363         else
364                 return FALSE;
365 }
366
367 char *ssl_certificate_check_signer (X509 *cert) 
368 {
369         X509_STORE_CTX store_ctx;
370         X509_STORE *store;
371         char *err_msg = NULL;
372
373         store = X509_STORE_new();
374         if (store == NULL) {
375                 printf("Can't create X509_STORE\n");
376                 return NULL;
377         }
378         if (!X509_STORE_set_default_paths(store)) {
379                 X509_STORE_free (store);
380                 return g_strdup(_("Couldn't load X509 default paths"));
381         }
382         
383         X509_STORE_CTX_init (&store_ctx, store, cert, NULL);
384
385         if(!X509_verify_cert (&store_ctx)) {
386                 err_msg = g_strdup(X509_verify_cert_error_string(
387                                         X509_STORE_CTX_get_error(&store_ctx)));
388                 debug_print("Can't check signer: %s\n", err_msg);
389                 X509_STORE_CTX_cleanup (&store_ctx);
390                 X509_STORE_free (store);
391                 return err_msg;
392                         
393         }
394         X509_STORE_CTX_cleanup (&store_ctx);
395         X509_STORE_free (store);
396         return NULL;
397 }
398
399 gboolean ssl_certificate_check (X509 *x509_cert, gchar *fqdn, gchar *host, gushort port)
400 {
401         SSLCertificate *current_cert = NULL;
402         SSLCertificate *known_cert;
403         SSLCertHookData cert_hook_data;
404         gchar *fqdn_host = NULL;        
405         gchar *fingerprint;
406         unsigned int n;
407         unsigned char md[EVP_MAX_MD_SIZE];      
408
409         if (fqdn)
410                 fqdn_host = g_strdup(fqdn);
411         else if (host)
412                 fqdn_host = get_fqdn(host);
413         else {
414                 g_warning("no host!\n");
415                 return FALSE;
416         }
417                 
418         current_cert = ssl_certificate_new_lookup(x509_cert, fqdn_host, port, FALSE);
419         
420         if (current_cert == NULL) {
421                 debug_print("Buggy certificate !\n");
422                 g_free(fqdn_host);
423                 return FALSE;
424         }
425
426         /* fingerprint */
427         X509_digest(x509_cert, EVP_md5(), md, &n);
428         fingerprint = readable_fingerprint(md, (int)n);
429
430         known_cert = ssl_certificate_find_lookup (fqdn_host, port, fingerprint, FALSE);
431
432         g_free(fingerprint);
433         g_free(fqdn_host);
434
435         if (known_cert == NULL) {
436                 cert_hook_data.cert = current_cert;
437                 cert_hook_data.old_cert = NULL;
438                 cert_hook_data.expired = FALSE;
439                 cert_hook_data.accept = FALSE;
440                 
441                 hooks_invoke(SSLCERT_ASK_HOOKLIST, &cert_hook_data);
442                 
443                 if (!cert_hook_data.accept) {
444                         ssl_certificate_destroy(current_cert);
445                         return FALSE;
446                 } else {
447                         ssl_certificate_save(current_cert);
448                         ssl_certificate_destroy(current_cert);
449                         return TRUE;
450                 }
451         } else if (!ssl_certificate_compare (current_cert, known_cert)) {
452                 cert_hook_data.cert = current_cert;
453                 cert_hook_data.old_cert = known_cert;
454                 cert_hook_data.expired = FALSE;
455                 cert_hook_data.accept = FALSE;
456                 
457                 hooks_invoke(SSLCERT_ASK_HOOKLIST, &cert_hook_data);
458
459                 if (!cert_hook_data.accept) {
460                         ssl_certificate_destroy(current_cert);
461                         ssl_certificate_destroy(known_cert);
462                         return FALSE;
463                 } else {
464                         ssl_certificate_save(current_cert);
465                         ssl_certificate_destroy(current_cert);
466                         ssl_certificate_destroy(known_cert);
467                         return TRUE;
468                 }
469         } else if (asn1toTime(X509_get_notAfter(current_cert->x509_cert)) < time(NULL)) {
470                 gchar *tmp = g_strdup_printf("%s:%d", current_cert->host, current_cert->port);
471                 
472                 if (warned_expired == NULL)
473                         warned_expired = g_hash_table_new(g_str_hash, g_str_equal);
474                 
475                 if (g_hash_table_lookup(warned_expired, tmp)) {
476                         g_free(tmp);
477                         ssl_certificate_destroy(current_cert);
478                         ssl_certificate_destroy(known_cert);
479                         return TRUE;
480                 }
481                         
482                 cert_hook_data.cert = current_cert;
483                 cert_hook_data.old_cert = NULL;
484                 cert_hook_data.expired = TRUE;
485                 cert_hook_data.accept = FALSE;
486                 
487                 hooks_invoke(SSLCERT_ASK_HOOKLIST, &cert_hook_data);
488
489                 if (!cert_hook_data.accept) {
490                         g_free(tmp);
491                         ssl_certificate_destroy(current_cert);
492                         ssl_certificate_destroy(known_cert);
493                         return FALSE;
494                 } else {
495                         g_hash_table_insert(warned_expired, tmp, GINT_TO_POINTER(1));
496                         ssl_certificate_destroy(current_cert);
497                         ssl_certificate_destroy(known_cert);
498                         return TRUE;
499                 }
500         }
501
502         ssl_certificate_destroy(current_cert);
503         ssl_certificate_destroy(known_cert);
504         return TRUE;
505 }
506
507 #endif /* USE_OPENSSL */