From 4f2c1e32cfe0ebcb628c5a55a52eef283aa39446 Mon Sep 17 00:00:00 2001 From: Eduardo Chappa Date: Wed, 5 Oct 2016 01:10:52 -0600 Subject: [PATCH] * When Alpine is compiled with password file and SMIME support the password file is encrypted using a private key/public certificate pair. If one such pair cannot be found, one will be created. --- alpine/imap.c | 64 +++++++++++++---- alpine/keymenu.c | 2 +- alpine/smime.c | 40 ++++++++++- pith/pine.hlp | 97 +++++++++++++++++++------ pith/readfile.c | 10 ++- pith/smime.c | 124 +++++++++++++++++++------------- pith/smime.h | 6 +- pith/smkeys.c | 211 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ pith/smkeys.h | 16 ++++- 9 files changed, 479 insertions(+), 91 deletions(-) diff --git a/alpine/imap.c b/alpine/imap.c index d5c97ee..45cc860 100644 --- a/alpine/imap.c +++ b/alpine/imap.c @@ -2302,7 +2302,7 @@ read_passfile(pinerc, l) #else /* PASSFILE */ char tmp[MAILTMPLEN], *ui[5]; - int i, j, n; + int i, j, n, rv = 0; #ifdef SMIME char tmp2[MAILTMPLEN]; char *text = NULL, *text2 = NULL; @@ -2333,18 +2333,48 @@ read_passfile(pinerc, l) * do not init smime here, because the .pinerc might not have been * read and we do not really know where the keys and certificates really * are. + * Remark: setupwdcert will produce a null ps_global->pwdcert only when + * it is called for the first time and there are certificates at all, + * or when it is called after the first time and the user refuses to + * create a self-signed certificate. In this situation we will just + * let the user live in an insecure world, but no more passwords will + * be saved in the password file, and only those found there will be used. */ - if(ps_global->pwdcert == NULL) - setup_pwdcert(&ps_global->pwdcert); tmp2[0] = '\0'; fgets(tmp2, sizeof(tmp2), fp); fclose(fp); if(strcmp(tmp2, "-----BEGIN PKCS7-----\n")){ - if(encrypt_file((char *)tmp, NULL, (PERSONAL_CERT *)ps_global->pwdcert)) - encrypted++; + /* there is an already existing password file, that is not encrypted + * and there is no key to encrypt it yet, go again through setup_pwdcert + * and encrypt it now. + */ + if(tmp2[0]){ /* not empty, UNencrypted password file */ + if(ps_global->pwdcert == NULL) + rv = setup_pwdcert(&ps_global->pwdcert); + if(rv == 0 && ps_global->pwdcert == NULL) + ps_global->pwdcert = (void *) ALPINE_self_signed_certificate(NULL, 0, ps_global->pwdcertdir, MASTERNAME); + if(ps_global->pwdcert == NULL){ + q_status_message(SM_ORDER, 3, 3, + " Failed to create private key. Using UNencrypted Password file. "); + save_password = 0; + } + else{ + if(rv == 1){ + q_status_message(SM_ORDER, 3, 3, + " Failed to unlock private key. Using UNencrypted Password file. "); + save_password = 0; /* do not save more passwords */ + } + } + if(ps_global->pwdcert != NULL + && encrypt_file((char *)tmp, NULL, (PERSONAL_CERT *)ps_global->pwdcert)) + encrypted++; + } } - else + else { + if(ps_global->pwdcert == NULL) + rv = setup_pwdcert(&ps_global->pwdcert); encrypted++; + } /* * if password file is encrypted we attemtp to decrypt. We ask the @@ -2568,7 +2598,7 @@ write_passfile(pinerc, l) } #ifdef SMIME - strncpy(tmp2, tmp, sizeof(tmp2)-1); + strncpy(tmp2, tmp, sizeof(tmp2)); tmp2[sizeof(tmp2)-1] = '\0'; #endif /* SMIME */ @@ -2603,13 +2633,21 @@ write_passfile(pinerc, l) fclose(fp); #ifdef SMIME if(text != NULL){ - if(encrypt_file((char *)tmp2, text, ps_global->pwdcert) == 0){ - if((fp = our_fopen(tmp2, "wb")) != NULL){ - fputs(text, fp); - fclose(fp); - } + if(ps_global->pwdcert == NULL){ + q_status_message(SM_ORDER, 3, 3, "Attempting to encrypt password file"); + i = setup_pwdcert(&ps_global->pwdcert); + if(i == 0 && ps_global->pwdcert == NULL) + ps_global->pwdcert = (void *) ALPINE_self_signed_certificate(NULL, 0, ps_global->pwdcertdir, MASTERNAME); + } + if(ps_global->pwdcert == NULL){ /* we tried but failed */ + if(i == -1) + q_status_message1(SM_ORDER, 3, 3, "Error: no directory %s to save certificates", ps_global->pwdcertdir); + else + q_status_message(SM_ORDER, 3, 3, "Refusing to write non-encrypted password file"); } - fs_give((void **)&text); + else if(encrypt_file((char *)tmp2, text, ps_global->pwdcert) == 0) + q_status_message(SM_ORDER, 3, 3, "Failed to encrypt password file"); + fs_give((void **)&text); /* do not save this text */ } #endif /* SMIME */ #endif /* PASSFILE */ diff --git a/alpine/keymenu.c b/alpine/keymenu.c index 7c8d167..1d5c7f6 100644 --- a/alpine/keymenu.c +++ b/alpine/keymenu.c @@ -2670,7 +2670,7 @@ struct key config_smime_add_new_key[] = NULL_MENU, EXIT_SETUP_MENU, {"I", N_("Import Key"), {MC_IMPORT,3,{'i', ctrl('M'), ctrl('J')}}, KS_NONE}, - NULL_MENU, + {"C", N_("Create Key"), {MC_ADD,1,{'c'}}, KS_NONE}, NULL_MENU, NULL_MENU, NULL_MENU, diff --git a/alpine/smime.c b/alpine/smime.c index 2eb5858..06693aa 100644 --- a/alpine/smime.c +++ b/alpine/smime.c @@ -1269,6 +1269,41 @@ manage_certs_tool(struct pine *ps, int cmd, CONF_S **cl, unsigned flags) WhichCerts ctype = (*cl)->d.s.ctype; switch(cmd){ + case MC_ADD: /* create a self signed certificate and import it */ + if(ctype == Password){ + PERSONAL_CERT *pc; + char pathdir[MAXPATH+1], filename[MAXPATH+1]; + struct stat sbuf; + int st; + smime_path(DF_SMIMETMPDIR, pathdir, sizeof(pathdir)); + if(((st = our_stat(pathdir, &sbuf)) == 0 + && (sbuf.st_mode & S_IFMT) == S_IFDIR) + || (st != 0 + && can_access(pathdir, ACCESS_EXISTS) != 0 + && our_mkpath(pathdir, 0700) == 0)){ + pc = ALPINE_self_signed_certificate(NULL, 0, pathdir, MASTERNAME); + snprintf(filename, sizeof(filename), "%s/%s.key", + pathdir, MASTERNAME); + filename[sizeof(filename)-1] = '\0'; + rv = import_certificate(ctype, pc, filename); + if(our_stat(pathdir, &sbuf) == 0){ + if(unlink(filename) < 0) + q_status_message1(SM_ORDER, 0, 2, + _("Could not remove private key %s.key"), MASTERNAME); + filename[strlen(filename)-4] = '\0'; + strcat(filename, ".crt"); + if(unlink(filename) < 0) + q_status_message1(SM_ORDER, 0, 2, + _("Could not remove public certifica %s.crt"), MASTERNAME); + if(rmdir(pathdir) < 0) + q_status_message1(SM_ORDER, 0, 2, + _("Could not remove temporary directory %s"), pathdir); + } + } + rv = 10; /* forces redraw */ + } + break; + case MC_CHOICE: if(PATHCERTDIR(ctype) == NULL) return 0; @@ -1320,7 +1355,7 @@ manage_certs_tool(struct pine *ps, int cmd, CONF_S **cl, unsigned flags) break; } case MC_IMPORT: - rv = import_certificate(ctype); + rv = import_certificate(ctype, NULL, NULL); if(rv < 0){ switch(rv){ default: @@ -1384,6 +1419,7 @@ void manage_password_file_certificates(struct pine *ps) if(ctmp == NULL){ ps->mangled_screen = 1; // smime_reinit(); + q_status_message(SM_ORDER, 1, 3, _("Failed to initialize password management screen (no key)")); return; } @@ -1774,7 +1810,7 @@ smime_helper_tool(struct pine *ps, int cmd, CONF_S **cl, unsigned flags) break; case MC_IMPORT: - rv = import_certificate((*cl)->d.s.ctype); + rv = import_certificate((*cl)->d.s.ctype, NULL, NULL); break; default: diff --git a/pith/pine.hlp b/pith/pine.hlp index ea684a7..0a8e2ef 100644 --- a/pith/pine.hlp +++ b/pith/pine.hlp @@ -140,7 +140,7 @@ with help text for the config screen and the composer that didn't have any reasonable place to be called from. Dummy change to get revision in pine.hlp ============= h_revision ================= -Alpine Commit 172 2016-09-29 09:24:36 +Alpine Commit 173 2016-10-05 01:10:48 ============= h_news ================= @@ -188,6 +188,11 @@ Additions include:
  • Unix-Alpine: Connect securely to a LDAP server on a secure port. Based on a contribution by Wang Kang. +
  • When Alpine is compiled with password file and SMIME support + the password file is encrypted using a private key/public + certificate pair. If one such pair cannot be found, one will be + created. Learn more. +
  • Alpine builds with any version bigger or equal to 1.0.0c, including version 1.1.0, as well as LibreSSL. @@ -1100,7 +1105,8 @@ or instead you can find the Apache License, version 2.0 at the web URL: Index
    1. Explanation -
    2. Example +
    3. Example of Use of Existing Key and Certificate +
    4. Example of Creating Master Password

    Unix Alpine Only. @@ -1109,28 +1115,38 @@ Index
    then you can use a special file to save your passwords, and avoid typing them every time you open a connection to a remote server. -

    If your version of Alpine was built with SMIME support, and you have a -public certificate/private key pair, then Alpine will use such pair to -encrypt your password file. If you have more than one key/certificate -pair, Alpine will pick the first pair that it finds that works. You can also -select a pair, and the way to do this is explained below. +

    If, in addition, your version of Alpine was built with SMIME support, then your +password file will be encrypted with a strong key. There are two ways in +which this can happen: Alpine will either use a matching private key and +public certificate pair that you already own, or it will create one for +you, just for purposes of encrypting this file. We describe both processes +below. + +

    Initially, Alpine will scan your public and private directories for a +certificate/private key pair that works. Alpine will pick the first pair +that it finds that matches.

    Once a pair has been chosen, it will be copied to the directory ~/.alpine-smime/.pwd, and from then on, Alpine will use the pair found in that directory. The first time this process is done, this directory will -be created, a key/certificate pair will be copied to it, and this pair -will be used in the future to encrypt and decrypt your password file. You -can create this directory and copy any key/certificate pair there. You -can add a self-signed certificate there, if you like, and you can let -this certificate expire. This will not affect the encryption and decryption +be created, a key/certificate pair will be copied to it, from then on +this pair will be used to encrypt and decrypt your password file. + +

    If you want to use a specific key and certificate pair to encrypt +your password file, you can create the directory ~/.alpine-smime/.pwd +manually, and then create your preferred key/certificate pair there. +Alpine will use this key regardless of if it has expired, or if it is +self-signed. These issues do not affect the encryption or decryption of the password file.

    If you prefer not to use the directory ~/.alpine-smime/.pwd to save your key/certificate pair, you can specify a different one with the -pwdcertdir command line option in Alpine. If the directory specified by -this option is not found or there is no valid key/certificate pair there, -Alpine will fail to encrypt and decrypt your password file. In other words, -Alpine will not initialize this directory for you. +this option is not found Alpine will fail to encrypt and decrypt your +password file. However if it exists, Alpine will search for a +key/certificate pair in that +directory, and if it does not find one, it will create one and save it +in that directory.

    Alpine does not care about the names of the key and certificates in this directory, but the private key must have ".key" extension @@ -1138,7 +1154,15 @@ and your public certificate must have the ".crt" extension. The name of the private key will be used in the prompt when you are asked to unlock your key to decrypt your password. -

    An example follows +

    If Alpine cannot find a suitable private key and public certificate +pair to encrypt your password, it will create one. You will be asked to +create a "Master Password" to protect such key. At this moment +there are no restrictions on passwords, other than they have to be at +least 8 characters long, but future versions of Alpine will include +functionality to restrict master passwords, as set up by the administrator +of the system in the pine.conf.fixed file. + +

    Example of Use of Existing Key and Certificate

    Assume you have a private key called peter@address.com.key in your, ~/.alpine-smime/private directory, and a public certificate called @@ -1178,10 +1202,43 @@ Enter password of key <private_key> to unlock password file:

    Observe that you do not need to use an existing key/certificate pair, and that you can create a new private key/public certificate pair to -encrypt and decrypt your password. However, once one is used, Alpine does -not provide a mechanism to switch the encryption and decryption files to -another key/certificate pair. This will be implemented in a future -release of Alpine. +encrypt and decrypt your password file. Alpine provides a mechanism to +change the encryption key for this file in the S/MIME configuration +screen. + +

    Example of Creating Master Password + +

    If Alpine cannot find a suitable private key and public certificate pair +to encrypt your password file, it will create one. When doing so, it will +start the process with the following warning: + +

    +Creating a Master Password for your Password file.
    +
    + +

    Then Alpine will ask you to enter your Master Password: + +

    +Create master password (attempt 1 of 3):
    +
    + +

    Once you enter this password, and it validates according to system policy, +you will be asked to confirm this password. + +

    +Confirm master password (attempt 1 of 3):
    +
    + +

    If you input the same password, then Alpine will set that as your +Master Password, and you will use this password to unlock your key in the +future. + +

    If you would like to switch your Master Password in the future, you can +do so by creating a new public key and public certificate pair. You can do +so in the S/MIME configuration screen, in the "Manage Key and +Certificate for Password File" section, simply enter your current +password to unlock your current key and then press "C" to create +a new key.

    <End of help> diff --git a/pith/readfile.c b/pith/readfile.c index cae03fc..e918ec9 100644 --- a/pith/readfile.c +++ b/pith/readfile.c @@ -78,6 +78,7 @@ our_copy(char *to_file, char *from_file) { STORE_S *in_cert, *out_cert; unsigned char c; + long int size = 0; in_cert = so_get(FileStar, from_file, READ_ACCESS | READ_FROM_LOCALE); if (in_cert == NULL) @@ -89,9 +90,14 @@ our_copy(char *to_file, char *from_file) return -1; } - while(so_readc(&c, in_cert) > 0) - so_writec(c, out_cert); + so_seek(out_cert, 0L, 0); + so_truncate(out_cert, 0); + while(so_readc(&c, in_cert) > 0){ + so_writec(c, out_cert); +// size++; + } +// so_truncate(out_cert, size); so_give(&in_cert); so_give(&out_cert); diff --git a/pith/smime.c b/pith/smime.c index ba98506..9ee8334 100644 --- a/pith/smime.c +++ b/pith/smime.c @@ -48,6 +48,7 @@ static char rcsid[] = "$Id: smime.c 1176 2008-09-29 21:16:42Z hubert@u.washingto #include #include +#include /* internal prototypes */ static void forget_private_keys(void); @@ -85,8 +86,8 @@ int smime_validate_extra_test(char *mimetext, unsigned long mimelen, char int (*pith_opt_smime_get_passphrase)(void); int (*pith_smime_import_certificate)(char *, char *, char *, size_t); -int (*pith_smime_enter_password)(char *prompt, char *, size_t); -int (*pith_smime_confirm_save)(char *email); +int (*pith_smime_enter_password)(char *, char *, size_t); +int (*pith_smime_confirm_save)(char *); static X509_STORE *s_cert_store; @@ -200,10 +201,16 @@ load_key_and_cert(char *pathkeydir, char *pathcertdir, char **keyfile, * If setup is successful, setup ps_global->pwdcert. * If any of this fails, ps_global->pwdcert will be null. * Ok, that should do it. + * + * return values: 0 - everything is normal + * 1 - User could not unlock key + * 2 - User cancelled to create self signed certificate + * -1 - a not normal value. */ -void +int setup_pwdcert(void **pwdcert) { + int rv; int we_inited = 0; int setup_dir = 0; /* make it non zero if we know which dir to use */ struct stat sbuf; @@ -213,10 +220,10 @@ setup_pwdcert(void **pwdcert) EVP_PKEY *pkey = NULL; X509 *pcert = NULL; PERSONAL_CERT *pc, *pc2 = NULL; - static int was_here = 0; + static int was_here = 0, setup_certdir = 0; if(pwdcert == NULL || was_here == 1) - return; + return -1; was_here++; if(ps_global->pwdcertdir){ @@ -238,17 +245,18 @@ setup_pwdcert(void **pwdcert) if(setup_dir == 0){ was_here = 0; - return; + return -1; } if(load_key_and_cert(pathdir, pathdir, &keyfile, &certfile, &pkey, &pcert) < 0){ was_here = 0; - return; + return 1; } - - if(ps_global->pwdcertdir == NULL) /* save the result of pwdcertdir */ + if(ps_global->pwdcertdir == NULL){ /* save the result of pwdcertdir */ + setup_certdir = 1; ps_global->pwdcertdir = cpystr(pathdir); + } if(certfile && keyfile){ pc = (PERSONAL_CERT *) fs_get(sizeof(PERSONAL_CERT)); @@ -259,16 +267,16 @@ setup_pwdcert(void **pwdcert) pc->cname = certfile; *pwdcert = (void *) pc; was_here = 0; - return; + return 0; } /* if the user gave a pwdcertdir and there is nothing there, do not * continue. Let the user initialize on their own this directory. */ - if(ps_global->pwdcertdir != NULL){ + if(setup_certdir){ /* if we are here, pwdcertdir failed */ was_here = 0; - return; - } + return -1; + } /* look to see if there are any certificates lying around, first * we try to load ps_global->smime to see if that has information @@ -379,7 +387,7 @@ setup_pwdcert(void **pwdcert) if(setup_dir){ *pwdcert = (void *) pc2; was_here = 0; - return; + return 0; } else if(pc2 != NULL) free_personal_certs(&pc2); @@ -432,16 +440,13 @@ setup_pwdcert(void **pwdcert) *pwdcert = (void *) pc; fs_give((void **)&certfile); was_here = 0; - return; + return 0; } -/* TODO: create self signed certificate - q_status_message(SM_ORDER, 2, 2, - _("No key/certificate pair found for password file encryption support")); -*/ was_here = 0; if(we_inited) smime_deinit(); + return 0; } #endif /* PASSFILE */ @@ -603,26 +608,38 @@ load_pkey_with_prompt(char *fpath, char *text, char *prompt, int *ret) /* This is a tool for conf_screen, The return value must be zero when * nothing changed, so if there is a failure in the import return 0 - * and return 1 when we succeeded + * and return 1 when we succeeded.\ + * We call this function in two ways: + * either fname is null or not. If they fname is null, so is p_cert. + * if p_cert is not null, it is the PERSONAL_CERT structure of fname if this + * is available, otherwise we will fill it up here. */ int -import_certificate(WhichCerts ctype) +import_certificate(WhichCerts ctype, PERSONAL_CERT *p_cert, char *fname) { int r = 1, rc; char filename[MAXPATH+1], full_filename[MAXPATH+1], buf[MAXPATH+1]; char *what; - if(pith_smime_import_certificate == NULL){ + if(pith_smime_import_certificate == NULL + || pith_smime_enter_password == NULL){ q_status_message(SM_ORDER, 0, 2, _("import of certificates not implemented yet!")); return 0; } - what = ctype == Public || ctype == CACert ? "certificate" : "key"; - r = (*pith_smime_import_certificate)(filename, full_filename, what, sizeof(filename) - 20); - - if(r < 0) - return 0; + if(fname == NULL){ + what = ctype == Public || ctype == CACert ? "certificate" : "key"; + r = (*pith_smime_import_certificate)(filename, full_filename, what, sizeof(filename) - 20); + + if(r < 0) + return 0; + } else { + char *s; + strncpy(full_filename, fname, sizeof(full_filename)); + if((s = strrchr(full_filename, '/')) != '\0') + strncpy(filename, s+1, sizeof(filename)); + } /* we are trying to import a new key for the password file. First we ask for the * private key. Once this is loaded, we make a reasonable attempt to find the @@ -637,7 +654,7 @@ import_certificate(WhichCerts ctype) char full_name_key[MAXPATH+1], full_name_cert[MAXPATH+1]; char *use_this_file; char prompt[500]; - EVP_PKEY *key = NULL; + EVP_PKEY *key = p_cert ? p_cert->key : NULL; rc = 1; /* assume success :) */ if(strlen(filename) > 4){ @@ -654,11 +671,12 @@ import_certificate(WhichCerts ctype) return 0; } - snprintf(prompt, sizeof(prompt), _("Enter passphrase for <%s>: "), filename); + snprintf(prompt, sizeof(prompt), _("Enter passphrase to unlock new key <%s>: "), filename); prompt[sizeof(prompt)-1] = '\0'; - if((key = load_pkey_with_prompt(full_filename, NULL, prompt, NULL)) != NULL){ + if(key != NULL + || (key = load_pkey_with_prompt(full_filename, NULL, prompt, NULL)) != NULL){ BIO *ins = NULL; - X509 *cert = NULL; + X509 *cert = p_cert ? p_cert->cert : NULL, *cert2; strncpy(full_name_key, full_filename, sizeof(full_filename)); full_name_key[sizeof(full_name_key)-1] = '\0'; @@ -687,15 +705,20 @@ import_certificate(WhichCerts ctype) strncat(PublicCertPath, EXTCERT(Public), 4); PublicCertPath[sizeof(PublicCertPath)-1] = '\0'; } - - /* attempt #1 to guess public cert name, use .crt extension */ - if((ins = BIO_new_file(full_name_cert, "r")) != NULL){ - if((cert = PEM_read_bio_X509(ins, NULL, NULL, NULL)) != NULL){ + /* attempt #1, use provided certificate, + * assumption is that full_name_cert is the file that this + * certificate derives from (which is obtained by substitution + * of .key extension in key by .crt extension) + */ + if(cert != NULL) /* attempt #1 */ use_this_file = &full_name_cert[0]; - } + else if((ins = BIO_new_file(full_name_cert, "r")) != NULL){ + /* attempt #2 to guess public cert name, use .crt extension */ + if((cert = PEM_read_bio_X509(ins, NULL, NULL, NULL)) != NULL){ + use_this_file = &full_name_cert[0]; + } } - else{ - /* attempt #2 to guess public cert name: user the original key */ + else{ /* attempt #3 to guess public cert name: use the original key */ if((ins = BIO_new_file(full_name_key, "r")) != NULL){ if((cert = PEM_read_bio_X509(ins, NULL, NULL, NULL)) != NULL){ use_this_file = &full_name_key[0]; @@ -703,7 +726,7 @@ import_certificate(WhichCerts ctype) } else { int done = 0; - /* attempt #3, ask the user */ + /* attempt #4, ask the user */ do { r = (*pith_smime_import_certificate)(filename, use_this_file, "certificate", sizeof(filename) - 20); if(r < 0){ @@ -751,7 +774,7 @@ import_certificate(WhichCerts ctype) char tmp2[MAILTMPLEN]; int encrypted = 0; char *text; - PERSONAL_CERT *pwdcert, *pc; + PERSONAL_CERT *pwdcert, *pc = p_cert; pwdcert = (PERSONAL_CERT *) ps_global->pwdcert; if(pwdcert == NULL) @@ -770,15 +793,17 @@ import_certificate(WhichCerts ctype) if(encrypted){ text = decrypt_file((char *)tmp, NULL, pwdcert); if(text != NULL){ - pc = fs_get(sizeof(PERSONAL_CERT)); - memset((void *)pc, 0, sizeof(PERSONAL_CERT)); - filename[strlen(filename)-strlen(EXTCERT(Private))] = '\0'; - pc->name = cpystr(filename); - snprintf(buf, sizeof(buf), "%s%s", filename, EXTCERT(Public)); - buf[sizeof(buf)-1] = '\0'; - pc->cname = cpystr(buf); - pc->key = key; - pc->cert = cert; + if(pc == NULL){ + pc = fs_get(sizeof(PERSONAL_CERT)); + memset((void *)pc, 0, sizeof(PERSONAL_CERT)); + filename[strlen(filename)-strlen(EXTCERT(Private))] = '\0'; + pc->name = cpystr(filename); + snprintf(buf, sizeof(buf), "%s%s", filename, EXTCERT(Public)); + buf[sizeof(buf)-1] = '\0'; + pc->cname = cpystr(buf); + pc->key = key; + pc->cert = cert; + } if(encrypt_file((char *)tmp, text, pc)){ /* we did it! */ build_path(buf, PATHCERTDIR(ctype), pwdcert->name, sizeof(buf)); @@ -2449,6 +2474,7 @@ bio_from_store(STORE_S *store) * replace the text of (char *) fp by the encrypted version of (char *) text. * certpath is the FULL path to the file containing the certificate used for * encryption. + * return value: 0 - failed to encrypt; 1 - success! */ int encrypt_file(char *fp, char *text, PERSONAL_CERT *pc) diff --git a/pith/smime.h b/pith/smime.h index 01e4cdc..ca92fbc 100644 --- a/pith/smime.h +++ b/pith/smime.h @@ -24,12 +24,14 @@ #include "../pith/filttype.h" #include "../pith/smkeys.h" +#include #include #include #ifdef PASSFILE #define DF_PASSWORD_DIR ".alpine-smime/.pwd" #endif +#define DF_SMIMETMPDIR ".alpine-smime/smtmp" #define OUR_PKCS7_ENCLOSURE_SUBTYPE "x-pkcs7-enclosure" @@ -68,14 +70,14 @@ int copy_privatecert_dir_to_container(void); int copy_privatecert_container_to_dir(void); int copy_cacert_dir_to_container(void); int copy_cacert_container_to_dir(void); -int import_certificate(WhichCerts); +int import_certificate(WhichCerts, PERSONAL_CERT *, char *); int copy_dir_to_container(WhichCerts which, char *contents); #ifdef APPLEKEYCHAIN int copy_publiccert_container_to_keychain(void); int copy_publiccert_keychain_to_container(void); #endif /* APPLEKEYCHAIN */ #ifdef PASSFILE -void setup_pwdcert(void **pwdcert); +int setup_pwdcert(void **pwdcert); #endif /* PASSFILE */ void mark_cert_deleted(WhichCerts ctype, int num, unsigned state); unsigned get_cert_deleted(WhichCerts ctype, int num); diff --git a/pith/smkeys.c b/pith/smkeys.c index 8666d53..09b2a0e 100644 --- a/pith/smkeys.c +++ b/pith/smkeys.c @@ -48,7 +48,218 @@ static char rcsid[] = "$Id: smkeys.c 1266 2009-07-14 18:39:12Z hubert@u.washingt static char *emailstrclean(char *string); static int mem_add_extra_cacerts(char *contents, X509_LOOKUP *lookup); int compare_certs_by_name(const void *data1, const void *data2); +int password_policy_check(char *); +int (*pith_smime_enter_password)(char *, char *, size_t); + +/* test if password passes a predetermined policy. + * return value: 0 - does not pass; 1 - it passes + */ +int +password_policy_check(char *password) +{ + int rv = 1; + char *error; + char tmp[1024]; + + if(password == NULL || password[0] == '\0'){ + error = _("Password cannot be blank"); + rv = 0; + } else if(strlen(password) < 8){ + error = _("Password is too short"); + rv = 0; + } + if(rv == 0){ + snprintf(tmp, sizeof(tmp), "%s%s", error, _(". Enter password again")); + tmp[sizeof(tmp) - 1] = '\0'; + q_status_message(SM_ORDER, 3, 3, tmp); + } + return rv; +} + + +int +create_master_password(char *pass, size_t passlen, int first_time) +{ +#define MAXTRIAL 3 + int rv, trial; + char prompt[MAILTMPLEN]; + char passbackup[MAILTMPLEN]; + + if(first_time) + q_status_message(SM_ORDER, 3, 3, + _(" Creating a Master Password for your Password file ")); + else + q_status_message(SM_ORDER, 3, 3, + _(" Retrying to create a Master Password for your Password file ")); + + for(trial = 0; trial < MAXTRIAL; trial++){ + snprintf(prompt, sizeof(prompt), + _("Create master password \(attempt %d of %d): "), trial+1, MAXTRIAL); + prompt[sizeof(prompt)- 1] = '\0'; + pass[0] = '\0'; + do { + rv = (pith_smime_enter_password)(prompt, pass, passlen); + if(password_policy_check(pass) == 0) + pass[0] = '\0'; + } while ((rv !=0 && rv !=1 && rv > 0) || pass[0] == '\0'); + + snprintf(prompt, sizeof(prompt), + _("Confirm master password \(attempt %d of %d): "), trial+1, MAXTRIAL); + prompt[sizeof(prompt)- 1] = '\0'; + passbackup[0] = '\0'; + do { + rv = (pith_smime_enter_password)(prompt, passbackup, sizeof(passbackup)); + } while ((rv !=0 && rv !=1 && rv > 0) || passbackup[0] == '\0'); + if(!strcmp(pass, passbackup)) + break; + if(trial + 1 < MAXTRIAL) + q_status_message(SM_ORDER, 2, 2, _("Passwords do not match, try again.")); + else{ + q_status_message(SM_ORDER, 2, 2, _("Passwords do not match, too many failures.")); + pass[0] = '\0'; + } + } + return (trial < MAXTRIAL) ? 1 : 0; +} + +/* + * Create a self signed certificate with root name _fname_, in directory + * _pathdir_. If _version_ is 3, we use the _template_ file as configuration + * file for openssl. At this moment, we only call this function with template = NULL + * and version = 0, but a sensible call is + * ALPINE_self_signed_certificate("/etc/ssl/openssl.cnf", 2, pathdir, fname, first_time); + * or so. + * _pathdir_ is the directory to save the file, + * _fname_ is the root of the name to use. Append ".key" and ".crt" to this name + * _first_time_ is an indicator to tell us if this is the first time we call this function + */ +PERSONAL_CERT * +ALPINE_self_signed_certificate(char *template, int version, char *pathdir, char *fname) +{ + BIGNUM *b = NULL; + X509_NAME *name = NULL; + X509_REQ *req = NULL; + EVP_PKEY_CTX *pkctx; + BIO *out = NULL; + char tmp[MAXPATH+1], password[1024]; + char *keyfile = NULL, *certfile = NULL; + char *extensions = NULL; + FILE *fp; + long errline = -1L; + PERSONAL_CERT *pc = NULL; + EVP_PKEY *pkey = NULL; + X509 *pcert = NULL; + CONF *req_conf = NULL; + static int first_time = 1; + + if(pathdir == NULL) + return NULL; + + if(template){ + if((out = BIO_new_file(template, "r")) == NULL){ + q_status_message(SM_ORDER, 2, 2, _("Problem reading configuration file")); + return pc; + } + + if((req_conf = NCONF_new(NULL)) != NULL + && NCONF_load_bio(req_conf, out, &errline) > 0){ + if((extensions = NCONF_get_string(req_conf, "req", "x509_extensions")) != NULL){ + X509V3_CTX ctx; + X509V3_set_ctx_test(&ctx); + X509V3_set_nconf(&ctx, req_conf); + if (!X509V3_EXT_add_nconf(req_conf, &ctx, extensions, NULL)) { + q_status_message(SM_ORDER, 2, 2, _("Problem loading openssl configuration")); + NCONF_free(req_conf); + return pc; + } + } + } + BIO_free(out); + out = NULL; + } + + if(create_master_password(password, sizeof(password), first_time) + && (pkctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL)) != NULL + && EVP_PKEY_keygen_init(pkctx) > 0 + && EVP_PKEY_CTX_set_rsa_keygen_bits(pkctx, 2048) > 0 /* RSA:2048 */ + && EVP_PKEY_keygen(pkctx, &pkey) > 0){ + snprintf(tmp, sizeof(tmp), "%s.key", fname); + tmp[sizeof(tmp)-1] = '\0'; + keyfile = cpystr(tmp); + build_path(tmp, pathdir, keyfile, sizeof(tmp)); + keyfile[strlen(keyfile)-4] = '\0'; /* keyfile does not have .key extension */ + if((fp = fopen(tmp, "w")) != NULL + && (out = BIO_new_fp(fp, BIO_CLOSE | BIO_FP_TEXT)) != NULL + && PEM_write_bio_PrivateKey(out, pkey, EVP_des_ede3_cbc(), + NULL, 0, NULL, password)){ + BIO_free(out); + out = NULL; + } + memset((void *)password, 0, sizeof(password)); + if((req = X509_REQ_new()) != NULL + && X509_REQ_set_version(req, 0L)){ + name = X509_REQ_get_subject_name(req); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, "Password File Certificate and Key Pair", -1, -1, 0); + if(X509_REQ_set_pubkey(req, pkey) + && (pcert = X509_new()) != NULL){ + if(X509_set_version(pcert, version) + && (b = BN_new()) != NULL + && BN_set_word(b, 65537) + && BN_pseudo_rand(b, 64, 0, 0) + && X509_get_serialNumber(pcert) + && BN_to_ASN1_INTEGER(b, X509_get_serialNumber(pcert)) /* set serial */ + && X509_set_issuer_name(pcert, X509_REQ_get_subject_name(req)) + && X509_set_subject_name(pcert, X509_REQ_get_subject_name(req))){ + X509V3_CTX ext_ctx; + EVP_PKEY *tmppkey; + + X509_gmtime_adj(X509_getm_notBefore(pcert), 0); + X509_time_adj_ex(X509_getm_notAfter(pcert), 1095, 0, NULL); + + if((tmppkey = X509_REQ_get0_pubkey(req)) != NULL + && X509_set_pubkey(pcert, tmppkey)){ + if(extensions != NULL && version == 2){ + X509V3_set_ctx(&ext_ctx, pcert, pcert, NULL, NULL, 0); + if(req_conf){ /* only if template is not null */ + X509V3_set_nconf(&ext_ctx, req_conf); + X509V3_EXT_add_nconf(req_conf, &ext_ctx, extensions, pcert); + } + } + EVP_PKEY_free(tmppkey); + X509_sign(pcert, pkey, NULL); + } + BN_free(b); + } + } + } + + snprintf(tmp, sizeof(tmp), "%s.crt", fname); + tmp[sizeof(tmp)-1] = '\0'; + certfile = cpystr(tmp); + build_path(tmp, pathdir, certfile, sizeof(tmp)); + if((fp = fopen(tmp, "w")) != NULL + &&(out = BIO_new_fp(fp, BIO_FP_TEXT)) != NULL){ + EVP_PKEY *tpubkey = X509_REQ_get0_pubkey(req); + PEM_write_bio_X509(out, pcert); + BIO_flush(out); + BIO_free(out); + out = NULL; + } + if(req_conf) + NCONF_free(req_conf); + } + if(keyfile && certfile && pkey && pcert){ + pc = (PERSONAL_CERT *) fs_get(sizeof(PERSONAL_CERT)); + memset((void *)pc, 0, sizeof(PERSONAL_CERT)); + pc->name = keyfile; + pc->key = pkey; + pc->cert = pcert; + pc->cname = certfile; + } + first_time = 0; + return pc; +} CertList * smime_X509_to_cert_info(X509 *x, char *name) diff --git a/pith/smkeys.h b/pith/smkeys.h index 0d3570b..d4e4c58 100644 --- a/pith/smkeys.h +++ b/pith/smkeys.h @@ -30,6 +30,8 @@ #include #include #include +#include +#include #ifndef OPENSSL_1_1_0 #define X509_get0_notBefore(x) ((x) && (x)->cert_info \ @@ -38,11 +40,21 @@ #define X509_get0_notAfter(x) ((x) && (x)->cert_info \ ? (x)->cert_info->validity->notAfter \ : NULL) +#define X509_getm_notBefore(x) ((x) && (x)->cert_info \ + ? (x)->cert_info->validity->notBefore \ + : NULL) +#define X509_getm_notAfter(x) ((x) && (x)->cert_info \ + ? (x)->cert_info->validity->notAfter \ + : NULL) +#define X509_REQ_get0_pubkey(x) (X509_REQ_get_pubkey((x))) +#else +#include +#include #endif /* OPENSSL_1_1_0 */ #define EMAILADDRLEADER "emailAddress=" #define CACERTSTORELEADER "cacert=" - +#define MASTERNAME "MasterPassword" typedef struct personal_cert { X509 *cert; @@ -78,7 +90,7 @@ void resort_certificates(CertList **data, WhichCerts ctype); int setup_certs_backup_by_type(WhichCerts ctype); char *smime_get_cn(X509 *); CertList *smime_X509_to_cert_info(X509 *, char *); - +PERSONAL_CERT *ALPINE_self_signed_certificate(char *, int, char *, char *); #endif /* PITH_SMKEYS_INCLUDED */ #endif /* SMIME */ -- 2.11.4.GIT