diff options
Diffstat (limited to 'pith/smkeys.c')
-rw-r--r-- | pith/smkeys.c | 925 |
1 files changed, 925 insertions, 0 deletions
diff --git a/pith/smkeys.c b/pith/smkeys.c new file mode 100644 index 00000000..5a827070 --- /dev/null +++ b/pith/smkeys.c @@ -0,0 +1,925 @@ +#if !defined(lint) && !defined(DOS) +static char rcsid[] = "$Id: smkeys.c 1266 2009-07-14 18:39:12Z hubert@u.washington.edu $"; +#endif + +/* + * ======================================================================== + * Copyright 2008 University of Washington + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * ======================================================================== + */ + +/* + * This is based on a contribution from Jonathan Paisley, see smime.c + */ + + +#include "../pith/headers.h" + +#ifdef SMIME + +#include "../pith/status.h" +#include "../pith/conf.h" +#include "../pith/remote.h" +#include "../pith/tempfile.h" +#include "../pith/busy.h" +#include "../pith/osdep/lstcmpnt.h" +#include "smkeys.h" + +#ifdef APPLEKEYCHAIN +#include <Security/SecKeychain.h> +#include <Security/SecKeychainItem.h> +#include <Security/SecKeychainSearch.h> +#include <Security/SecCertificate.h> +#endif /* APPLEKEYCHAIN */ + + +/* internal prototypes */ +static char *emailstrclean(char *string); +static int add_certs_in_dir(X509_LOOKUP *lookup, char *path); +static int certlist_to_file(char *filename, CertList *certlist); +static int mem_add_extra_cacerts(char *contents, X509_LOOKUP *lookup); + + +/* + * Remove leading whitespace, trailing whitespace and convert + * to lowercase. Also remove slash characters + * + * Args: s, -- The string to clean + * + * Result: the cleaned string + */ +static char * +emailstrclean(char *string) +{ + char *s = string, *sc = NULL, *p = NULL; + + for(; *s; s++){ /* single pass */ + if(!isspace((unsigned char) (*s))){ + p = NULL; /* not start of blanks */ + if(!sc) /* first non-blank? */ + sc = string; /* start copying */ + } + else if(!p) /* it's OK if sc == NULL */ + p = sc; /* start of blanks? */ + + if(sc && *s!='/' && *s!='\\') /* if copying, copy */ + *sc++ = isupper((unsigned char) (*s)) + ? (unsigned char) tolower((unsigned char) (*s)) + : (unsigned char) (*s); + } + + if(p) /* if ending blanks */ + *p = '\0'; /* tie off beginning */ + else if(!sc) /* never saw a non-blank */ + *string = '\0'; /* so tie whole thing off */ + + return(string); +} + + +/* + * Add a lookup for each "*.crt" file in the given directory. + */ +static int +add_certs_in_dir(X509_LOOKUP *lookup, char *path) +{ + char buf[MAXPATH]; + struct direct *d; + DIR *dirp; + int ret = 0; + + dirp = opendir(path); + if(dirp){ + + while(!ret && (d=readdir(dirp)) != NULL){ + if(srchrstr(d->d_name, ".crt")){ + build_path(buf, path, d->d_name, sizeof(buf)); + + if(!X509_LOOKUP_load_file(lookup, buf, X509_FILETYPE_PEM)){ + q_status_message1(SM_ORDER, 3, 3, _("Error loading file %s"), buf); + ret = -1; + } + } + + } + + closedir(dirp); + } + + return ret; +} + + +/* + * Get an X509_STORE. This consists of the system + * certs directory and any certificates in the user's + * ~/.alpine-smime/ca directory. + */ +X509_STORE * +get_ca_store(void) +{ + X509_LOOKUP *lookup; + X509_STORE *store = NULL; + + dprint((9, "get_ca_store()")); + + if(!(store=X509_STORE_new())){ + dprint((9, "X509_STORE_new() failed")); + return store; + } + + if(!(lookup=X509_STORE_add_lookup(store, X509_LOOKUP_file()))){ + dprint((9, "X509_STORE_add_lookup() failed")); + X509_STORE_free(store); + return NULL; + } + + if(ps_global->smime && ps_global->smime->catype == Container + && ps_global->smime->cacontent){ + + if(!mem_add_extra_cacerts(ps_global->smime->cacontent, lookup)){ + X509_STORE_free(store); + return NULL; + } + } + else if(ps_global->smime && ps_global->smime->catype == Directory + && ps_global->smime->capath){ + if(add_certs_in_dir(lookup, ps_global->smime->capath) < 0){ + X509_STORE_free(store); + return NULL; + } + } + + if(!(lookup=X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir()))){ + X509_STORE_free(store); + return NULL; + } + +#ifdef SMIME_SSLCERTS + dprint((9, "get_ca_store(): adding cacerts from %s", SMIME_SSLCERTS)); + X509_LOOKUP_add_dir(lookup, SMIME_SSLCERTS, X509_FILETYPE_PEM); +#endif + + return store; +} + + +EVP_PKEY * +load_key(PERSONAL_CERT *pc, char *pass) +{ + BIO *in; + EVP_PKEY *key = NULL; + char buf[MAXPATH], file[MAXPATH]; + + if(!(ps_global->smime && pc && pc->name)) + return key; + + if(ps_global->smime->privatetype == Container){ + char *q; + + if(pc->keytext && (q = strstr(pc->keytext, "-----END")) != NULL){ + while(*q && *q != '\n') + q++; + + if(*q == '\n') + q++; + + if((in = BIO_new_mem_buf(pc->keytext, q-pc->keytext)) != NULL){ + key = PEM_read_bio_PrivateKey(in, NULL, NULL, pass); + BIO_free(in); + } + } + } + else if(ps_global->smime->privatetype == Directory){ + /* filename is path/name.key */ + strncpy(buf, pc->name, sizeof(buf)-5); + buf[sizeof(buf)-5] = '\0'; + strncat(buf, ".key", 5); + build_path(file, ps_global->smime->privatepath, buf, sizeof(file)); + + if(!(in = BIO_new_file(file, "r"))) + return NULL; + + key = PEM_read_bio_PrivateKey(in, NULL, NULL, pass); + BIO_free(in); + } + + return key; +} + + +#ifdef notdef +static char * +get_x509_name_entry(const char *key, X509_NAME *name) +{ + int i, c, n; + char buf[256]; + char *id; + + if(!name) + return NULL; + + c = X509_NAME_entry_count(name); + + for(i=0; i<c; i++){ + X509_NAME_ENTRY *e; + + e = X509_NAME_get_entry(name, i); + if(!e) + continue; + + buf[0] = 0; + id = buf; + + n = OBJ_obj2nid(e->object); + if((n == NID_undef) || ((id=(char*) OBJ_nid2sn(n)) == NULL)){ + i2t_ASN1_OBJECT(buf, sizeof(buf), e->object); + id = buf; + } + + if((strucmp(id, "email")==0) || (strucmp(id, "emailAddress")==0)){ + X509_NAME_get_text_by_OBJ(name, e->object, buf, sizeof(buf)-1); + return cpystr(buf); + } + } + + return NULL; +} + + +char * +get_x509_subject_email(X509 *x) +{ + char* result; + result = get_x509_name_entry("email", X509_get_subject_name(x)); + if( !result ){ + result = get_x509_name_entry("emailAddress", X509_get_subject_name(x)); + } + + return result; +} +#endif /* notdef */ + +#include <openssl/x509v3.h> +/* + * This newer version is from Adrian Vogel. It looks for the email + * address not only in the email address field, but also in an + * X509v3 extension field, Subject Altenative Name. + */ +char * +get_x509_subject_email(X509 *x) +{ + char *result = NULL; + STACK_OF(OPENSSL_STRING) *emails = X509_get1_email(x); + if (sk_OPENSSL_STRING_num(emails) > 0) { + /* take the first one on the stack */ + result = cpystr(sk_OPENSSL_STRING_value(emails, 0)); + } + X509_email_free(emails); + return result; +} + + +/* + * Save the certificate for the given email address in + * ~/.alpine-smime/public. + * + * Should consider the security hazards in making a file with + * the email address that has come from the certificate. + * + * The argument email is destroyed. + */ +void +save_cert_for(char *email, X509 *cert) +{ + if(!ps_global->smime) + return; + + dprint((9, "save_cert_for(%s)", email ? email : "?")); + emailstrclean(email); + + if(ps_global->smime->publictype == Keychain){ +#ifdef APPLEKEYCHAIN + + OSStatus rc; + SecCertificateRef secCertificateRef; + CSSM_DATA certData; + + memset((void *) &certData, 0, sizeof(certData)); + memset((void *) &secCertificateRef, 0, sizeof(secCertificateRef)); + + /* convert OpenSSL X509 cert data to MacOS certData */ + if((certData.Length = i2d_X509(cert, &(certData.Data))) > 0){ + + /* + * Put that certData into a SecCertificateRef. + * Version 3 should work for versions 1-3. + */ + if(!(rc=SecCertificateCreateFromData(&certData, + CSSM_CERT_X_509v3, + CSSM_CERT_ENCODING_DER, + &secCertificateRef))){ + + /* add it to the default keychain */ + if(!(rc=SecCertificateAddToKeychain(secCertificateRef, NULL))){ + /* ok */ + } + else if(rc == errSecDuplicateItem){ + dprint((9, "save_cert_for: certificate for %s already in keychain", email)); + } + else{ + dprint((9, "SecCertificateAddToKeychain failed")); + } + } + else{ + dprint((9, "SecCertificateCreateFromData failed")); + } + } + else{ + dprint((9, "i2d_X509 failed")); + } + +#endif /* APPLEKEYCHAIN */ + } + else if(ps_global->smime->publictype == Container){ + REMDATA_S *rd = NULL; + char path[MAXPATH]; + char *tempfile = NULL; + int err = 0; + + add_to_end_of_certlist(&ps_global->smime->publiccertlist, email, X509_dup(cert)); + + if(!ps_global->smime->publicpath) + return; + + if(IS_REMOTE(ps_global->smime->publicpath)){ + rd = rd_create_remote(RemImap, ps_global->smime->publicpath, REMOTE_SMIME_SUBTYPE, + NULL, "Error: ", + _("Can't access remote smime configuration.")); + if(!rd) + return; + + (void) rd_read_metadata(rd); + + if(rd->access == MaybeRorW){ + if(rd->read_status == 'R') + rd->access = ReadOnly; + else + rd->access = ReadWrite; + } + + if(rd->access != NoExists){ + + rd_check_remvalid(rd, 1L); + + /* + * If the cached info says it is readonly but + * it looks like it's been fixed now, change it to readwrite. + */ + if(rd->read_status == 'R'){ + rd_check_readonly_access(rd); + if(rd->read_status == 'W'){ + rd->access = ReadWrite; + rd->flags |= REM_OUTOFDATE; + } + else + rd->access = ReadOnly; + } + } + + if(rd->flags & REM_OUTOFDATE){ + if(rd_update_local(rd) != 0){ + + dprint((1, "save_cert_for: rd_update_local failed\n")); + rd_close_remdata(&rd); + return; + } + } + else + rd_open_remote(rd); + + if(rd->access != ReadWrite || rd_remote_is_readonly(rd)){ + rd_close_remdata(&rd); + return; + } + + rd->flags |= DO_REMTRIM; + + strncpy(path, rd->lf, sizeof(path)-1); + path[sizeof(path)-1] = '\0'; + } + else{ + strncpy(path, ps_global->smime->publicpath, sizeof(path)-1); + path[sizeof(path)-1] = '\0'; + } + + tempfile = tempfile_in_same_dir(path, "az", NULL); + if(tempfile){ + if(certlist_to_file(tempfile, ps_global->smime->publiccertlist)) + err++; + + if(!err){ + if(rename_file(tempfile, path) < 0){ + q_status_message2(SM_ORDER, 3, 3, + _("Can't rename %s to %s"), tempfile, path); + err++; + } + } + + if(!err && IS_REMOTE(ps_global->smime->publicpath)){ + int e, we_cancel; + char datebuf[200]; + + datebuf[0] = '\0'; + + we_cancel = busy_cue(_("Copying to remote smime container"), NULL, 1); + if((e = rd_update_remote(rd, datebuf)) != 0){ + if(e == -1){ + q_status_message2(SM_ORDER | SM_DING, 3, 5, + _("Error opening temporary smime file %s: %s"), + rd->lf, error_description(errno)); + dprint((1, + "write_remote_smime: error opening temp file %s\n", + rd->lf ? rd->lf : "?")); + } + else{ + q_status_message2(SM_ORDER | SM_DING, 3, 5, + _("Error copying to %s: %s"), + rd->rn, error_description(errno)); + dprint((1, + "write_remote_smime: error copying from %s to %s\n", + rd->lf ? rd->lf : "?", rd->rn ? rd->rn : "?")); + } + + q_status_message(SM_ORDER | SM_DING, 5, 5, + _("Copy of smime cert to remote folder failed, changes NOT saved remotely")); + } + else{ + rd_update_metadata(rd, datebuf); + rd->read_status = 'W'; + } + + rd_close_remdata(&rd); + + if(we_cancel) + cancel_busy_cue(-1); + } + + fs_give((void **) &tempfile); + } + } + else if(ps_global->smime->publictype == Directory){ + char certfilename[MAXPATH]; + BIO *bio_out; + + build_path(certfilename, ps_global->smime->publicpath, + email, sizeof(certfilename)); + strncat(certfilename, ".crt", sizeof(certfilename)-1-strlen(certfilename)); + certfilename[sizeof(certfilename)-1] = 0; + + bio_out = BIO_new_file(certfilename, "w"); + if(bio_out){ + PEM_write_bio_X509(bio_out, cert); + BIO_free(bio_out); + q_status_message1(SM_ORDER, 1, 1, _("Saved certificate for <%s>"), email); + } + else{ + q_status_message1(SM_ORDER, 1, 1, _("Couldn't save certificate for <%s>"), email); + } + } +} + + +/* + * Try to retrieve the certificate for the given email address. + * The caller should free the cert. + */ +X509 * +get_cert_for(char *email) +{ + char *path; + char certfilename[MAXPATH]; + char emailaddr[MAXPATH]; + X509 *cert = NULL; + BIO *in; + + if(!ps_global->smime) + return cert; + + dprint((9, "get_cert_for(%s)", email ? email : "?")); + + strncpy(emailaddr, email, sizeof(emailaddr)-1); + emailaddr[sizeof(emailaddr)-1] = 0; + + /* clean it up (lowercase, space removal) */ + emailstrclean(emailaddr); + + if(ps_global->smime->publictype == Keychain){ +#ifdef APPLEKEYCHAIN + + OSStatus rc; + SecKeychainItemRef itemRef = nil; + SecKeychainAttributeList attrList; + SecKeychainAttribute attrib; + SecKeychainSearchRef searchRef = nil; + CSSM_DATA certData; + + /* low-level form of MacOS data */ + memset((void *) &certData, 0, sizeof(certData)); + + attrList.count = 1; + attrList.attr = &attrib; + + /* kSecAlias means email address for a certificate */ + attrib.tag = kSecAlias; + attrib.data = emailaddr; + attrib.length = strlen(attrib.data); + + /* Find the certificate in the default keychain */ + if(!(rc=SecKeychainSearchCreateFromAttributes(NULL, + kSecCertificateItemClass, + &attrList, + &searchRef))){ + + if(!(rc=SecKeychainSearchCopyNext(searchRef, &itemRef))){ + + /* extract the data portion of the certificate */ + if(!(rc=SecCertificateGetData((SecCertificateRef) itemRef, &certData))){ + + /* + * Convert it from MacOS form to OpenSSL form. + * The input is certData from above and the output + * is the X509 *cert. + */ + if(!d2i_X509(&cert, &(certData.Data), certData.Length)){ + dprint((9, "d2i_X509 failed")); + } + } + else{ + dprint((9, "SecCertificateGetData failed")); + } + } + else if(rc == errSecItemNotFound){ + dprint((9, "get_cert_for: Public cert for %s not found", emailaddr)); + } + else{ + dprint((9, "SecKeychainSearchCopyNext failed")); + } + } + else{ + dprint((9, "SecKeychainSearchCreateFromAttributes failed")); + } + + if(searchRef) + CFRelease(searchRef); + +#endif /* APPLEKEYCHAIN */ + } + else if(ps_global->smime->publictype == Container){ + if(ps_global->smime->publiccertlist){ + CertList *cl; + + for(cl = ps_global->smime->publiccertlist; cl; cl = cl->next){ + if(cl->name && !strucmp(emailaddr, cl->name)) + break; + } + + if(cl) + cert = X509_dup((X509 *) cl->x509_cert); + } + } + else if(ps_global->smime->publictype == Directory){ + path = ps_global->smime->publicpath; + build_path(certfilename, path, emailaddr, sizeof(certfilename)); + strncat(certfilename, ".crt", sizeof(certfilename)-1-strlen(certfilename)); + certfilename[sizeof(certfilename)-1] = 0; + + if((in = BIO_new_file(certfilename, "r"))!=0){ + + cert = PEM_read_bio_X509(in, NULL, NULL, NULL); + + if(cert){ + /* could check email addr in cert matches */ + } + + BIO_free(in); + } + } + + return cert; +} + + +PERSONAL_CERT * +mem_to_personal_certs(char *contents) +{ + PERSONAL_CERT *result = NULL; + char *p, *q, *line, *name, *keytext, *save_p; + X509 *cert = NULL; + + if(contents && *contents){ + for(p = contents; *p != '\0';){ + line = p; + + while(*p && *p != '\n') + p++; + + save_p = NULL; + if(*p == '\n'){ + save_p = p; + *p++ = '\0'; + } + + if(strncmp(EMAILADDRLEADER, line, strlen(EMAILADDRLEADER)) == 0){ + name = line + strlen(EMAILADDRLEADER); + cert = get_cert_for(name); + keytext = p; + + /* advance p past this record */ + if((q = strstr(keytext, "-----END")) != NULL){ + while(*q && *q != '\n') + q++; + + if(*q == '\n') + q++; + + p = q; + } + else{ + p = p + strlen(p); + q_status_message(SM_ORDER | SM_DING, 3, 3, _("Error in privatekey container, missing END")); + } + + if(cert){ + PERSONAL_CERT *pc; + + pc = (PERSONAL_CERT *) fs_get(sizeof(*pc)); + pc->cert = cert; + pc->name = cpystr(name); + pc->keytext = keytext; /* a pointer into contents */ + + pc->key = load_key(pc, ""); + + pc->next = result; + result = pc; + } + } + + if(save_p) + *save_p = '\n'; + } + } + + return result; +} + + +CertList * +mem_to_certlist(char *contents) +{ + CertList *ret = NULL; + char *p, *q, *line, *name, *certtext, *save_p; + X509 *cert = NULL; + BIO *in; + + if(contents && *contents){ + for(p = contents; *p != '\0';){ + line = p; + + while(*p && *p != '\n') + p++; + + save_p = NULL; + if(*p == '\n'){ + save_p = p; + *p++ = '\0'; + } + + if(strncmp(EMAILADDRLEADER, line, strlen(EMAILADDRLEADER)) == 0){ + name = line + strlen(EMAILADDRLEADER); + cert = NULL; + certtext = p; + if(strncmp("-----BEGIN", certtext, strlen("-----BEGIN")) == 0){ + if((q = strstr(certtext, "-----END")) != NULL){ + while(*q && *q != '\n') + q++; + + if(*q == '\n') + q++; + + p = q; + + if((in = BIO_new_mem_buf(certtext, q-certtext)) != 0){ + cert = PEM_read_bio_X509(in, NULL, NULL, NULL); + + BIO_free(in); + } + } + } + else{ + p = p + strlen(p); + q_status_message1(SM_ORDER | SM_DING, 3, 3, _("Error in publiccert container, missing BEGIN, certtext=%s"), certtext); + } + + if(name && cert){ + add_to_end_of_certlist(&ret, name, cert); + } + } + + if(save_p) + *save_p = '\n'; + } + } + + return ret; +} + + +/* + * Add the CACert Container contents into the CACert store. + * + * Returns > 0 for success, 0 for failure + */ +int +mem_add_extra_cacerts(char *contents, X509_LOOKUP *lookup) +{ + char *p, *q, *line, *certtext, *save_p; + BIO *in, *out; + int len, failed = 0; + char *tempfile; + char iobuf[4096]; + + /* + * The most straight-forward way to do this is to write + * the container contents to a temp file and then load the + * contents of the file with X509_LOOKUP_load_file(), like + * is done in add_certs_in_dir(). What we don't know is if + * each file should consist of one cacert or if they can all + * just be jammed together into one file. To be safe, we'll use + * one file per and do each in a separate operation. + */ + + if(contents && *contents){ + for(p = contents; *p != '\0';){ + line = p; + + while(*p && *p != '\n') + p++; + + save_p = NULL; + if(*p == '\n'){ + save_p = p; + *p++ = '\0'; + } + + /* look for separator line */ + if(strncmp(CACERTSTORELEADER, line, strlen(CACERTSTORELEADER)) == 0){ + /* certtext is the content that should go in a file */ + certtext = p; + if(strncmp("-----BEGIN", certtext, strlen("-----BEGIN")) == 0){ + if((q = strstr(certtext, CACERTSTORELEADER)) != NULL){ + p = q; + } + else{ /* end of file */ + q = certtext + strlen(certtext); + p = q; + } + + in = BIO_new_mem_buf(certtext, q-certtext); + if(in){ + tempfile = temp_nam(NULL, "az"); + out = NULL; + if(tempfile) + out = BIO_new_file(tempfile, "w"); + + if(out){ + while((len = BIO_read(in, iobuf, sizeof(iobuf))) > 0) + BIO_write(out, iobuf, len); + + BIO_free(out); + if(!X509_LOOKUP_load_file(lookup, tempfile, X509_FILETYPE_PEM)) + failed++; + + fs_give((void **) &tempfile); + } + + BIO_free(in); + } + } + else{ + p = p + strlen(p); + q_status_message1(SM_ORDER | SM_DING, 3, 3, _("Error in cacert container, missing BEGIN, certtext=%s"), certtext); + } + } + else{ + p = p + strlen(p); + q_status_message1(SM_ORDER | SM_DING, 3, 3, _("Error in cacert container, missing separator, line=%s"), line); + } + + if(save_p) + *save_p = '\n'; + } + } + + return(!failed); +} + + +int +certlist_to_file(char *filename, CertList *certlist) +{ + CertList *cl; + BIO *bio_out = NULL; + int ret = -1; + + if(filename && (bio_out=BIO_new_file(filename, "w")) != NULL){ + ret = 0; + for(cl = certlist; cl; cl = cl->next){ + if(cl->name && cl->name[0] && cl->x509_cert){ + if(!((BIO_puts(bio_out, EMAILADDRLEADER) > 0) + && (BIO_puts(bio_out, cl->name) > 0) + && (BIO_puts(bio_out, "\n") > 0))) + ret = -1; + + if(!PEM_write_bio_X509(bio_out, (X509 *) cl->x509_cert)) + ret = -1; + } + } + + BIO_free(bio_out); + } + + return ret; +} + + +void +add_to_end_of_certlist(CertList **cl, char *name, X509 *cert) +{ + CertList *new, *clp; + + if(!cl) + return; + + new = (CertList *) fs_get(sizeof(*new)); + memset((void *) new, 0, sizeof(*new)); + new->x509_cert = cert; + new->name = name ? cpystr(name) : NULL; + + if(!*cl){ + *cl = new; + } + else{ + for(clp = (*cl); clp->next; clp = clp->next) + ; + + clp->next = new; + } +} + + +void +free_certlist(CertList **cl) +{ + if(cl && *cl){ + free_certlist(&(*cl)->next); + if((*cl)->name) + fs_give((void **) &(*cl)->name); + + if((*cl)->x509_cert) + X509_free((X509 *) (*cl)->x509_cert); + + fs_give((void **) cl); + } +} + + +void +free_personal_certs(PERSONAL_CERT **pc) +{ + if(pc && *pc){ + free_personal_certs(&(*pc)->next); + if((*pc)->name) + fs_give((void **) &(*pc)->name); + + if((*pc)->name) + fs_give((void **) &(*pc)->name); + + if((*pc)->cert) + X509_free((*pc)->cert); + + if((*pc)->key) + EVP_PKEY_free((*pc)->key); + + fs_give((void **) pc); + } +} + +#endif /* SMIME */ |