/* * OpenVPN-GUI -- A Windows GUI for OpenVPN. * * Copyright (C) 2022 Selva Nair * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program (see the file COPYING included with this * distribution); if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifdef HAVE_CONFIG_H #include #endif #include #include "pkcs11.h" #include "options.h" #include "main.h" #include "manage.h" #include "openvpn.h" #include "misc.h" #include "openvpn-gui-res.h" #include "localization.h" #include #include #include #include extern options_t o; /* state of list array */ #define STATE_GET_COUNT 1 #define STATE_GET_ENTRY 2 #define STATE_FILLED 4 #define STATE_SELECTED 8 struct cert_info { wchar_t *commonname; wchar_t *issuer; wchar_t *notAfter; const CERT_CONTEXT *ctx; }; struct pkcs11_entry { char *id; /* pkcs11-id string value as received from daemon */ struct cert_info cert; /* decoded certificate structure */ }; static void certificate_free(struct cert_info *cert) { if (cert) { free(cert->commonname); free(cert->issuer); free(cert->notAfter); CertFreeCertificateContext(cert->ctx); } } static bool pkcs11_list_alloc(struct pkcs11_list *l) { if (l->count > 0 && !l->pe) { l->pe = calloc(l->count, sizeof(struct pkcs11_entry)); } return (l->pe != NULL); } static void pkcs11_entry_free(struct pkcs11_entry *pe) { if (!pe) { return; } free(pe->id); certificate_free(&pe->cert); } /* Free any allocated memory and clear the list */ void pkcs11_list_clear(struct pkcs11_list *l) { if (l->pe) { for (UINT i = 0; i < l->count; i++) { pkcs11_entry_free(&l->pe[i]); } free(l->pe); } CLEAR(*l); } /* Get the "friendly name" (usually the commonname) from a certificate * context. If issuer = true, the issuer name is parsed, else the * subject. A newly allocated wide char string is returned. */ static wchar_t * extract_name_entry(const CERT_CONTEXT *ctx, bool issuer) { DWORD size = CertGetNameStringW(ctx, CERT_NAME_FRIENDLY_DISPLAY_TYPE, issuer ? CERT_NAME_ISSUER_FLAG : 0, NULL, NULL, 0); wchar_t *name = malloc(size*sizeof(wchar_t)); if (name) { size = CertGetNameStringW(ctx, CERT_NAME_FRIENDLY_DISPLAY_TYPE, issuer ? CERT_NAME_ISSUER_FLAG : 0, NULL, name, size); } return name; } /* Decode a base64 encoded certificate blob and fill in * the cert structure with commonname, issuer and validity. * Returns false on error. */ static bool decode_certificate(struct cert_info *cert, const char *b64) { unsigned char *der = NULL; bool ret = false; int len = Base64Decode(b64, (char **) &der); if (len < 0) { goto out; } const CERT_CONTEXT *ctx = CertCreateCertificateContext(X509_ASN_ENCODING, der, (DWORD) len); if (!ctx) { goto out; } cert->commonname = extract_name_entry(ctx, 0); cert->issuer = extract_name_entry(ctx, CERT_NAME_ISSUER_FLAG); cert->notAfter = LocalizedFileTime(&ctx->pCertInfo->NotAfter); cert->ctx = ctx; ret = true; out: free(der); return ret; } /* Parse pkcs11-id message "'n', ID:'', BLOB:''" * and fill in data in pkcs11 enrty. * Returns index of the item on success, -1 on error. * On success, caller must free the entry after use. */ static UINT pkcs11_entry_parse(const char *data, struct pkcs11_list *l) { char *token = NULL; UINT index = (UINT) -1; const char *quotes = " '"; struct pkcs11_entry *pe = NULL; char *p = strdup(data); if (!p) { goto out; } token = strtok(p, ","); /* parse index */ if (token) { StrTrimA(token, quotes); UINT i = strtoul(token, NULL, 10); if (i >= l->count) /* invalid entry number */ { goto out; } index = i; } pe = &l->pe[index]; while ((token = strtok(NULL, ",")) != NULL) { char *tmp; if ((tmp = strstr(token, "ID:")) != NULL) { tmp += 3; StrTrimA(tmp, quotes); pe->id = strdup(tmp); } else if ((tmp = strstr(token, "BLOB:")) != NULL) { tmp += 5; StrTrimA(tmp, quotes); if (!decode_certificate(&pe->cert, tmp)) { pkcs11_entry_free(pe); index = (UINT) -1; goto out; } } } out: free(p); return index; } /* send pkcs11-id to management-interface */ static void pkcs11_id_send(connection_t *c, const char *id) { const char *format = "needstr 'pkcs11-id-request' '%s'"; size_t len = strlen(format) + (id ? strlen(id) : 0) + 1; char *cmd = malloc(len); if (cmd) { snprintf(cmd, len, format, id ? id : ""); cmd[len-1] = '\0'; ManagementCommand(c, cmd, NULL, regular); } else { WriteStatusLog(c, L"GUI> ", L"Out of memory in pkcs11_id_send", false); ManagementCommand(c, "needstr 'pkcs11-id-request' ''", NULL, regular); } free(cmd); } static INT_PTR CALLBACK QueryPkcs11DialogProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam); /* Handle Need 'pkcs11-id-request' from management */ void OnPkcs11(connection_t *c, UNUSED char *msg) { struct pkcs11_list *l = &c->pkcs11_list; pkcs11_list_clear(l); l->selected = (UINT) -1; /* set selection to an invalid index */ /* prompt user to select a certificate */ if (IDOK == LocalizedDialogBoxParam(ID_DLG_PKCS11_QUERY, QueryPkcs11DialogProc, (LPARAM)c) && l->state & STATE_SELECTED && l->selected < l->count) { pkcs11_id_send(c, l->pe[l->selected].id); } } /* Callback when pkcs11-id-count is received */ static void pkcs11_count_recv(connection_t *c, char *msg) { struct pkcs11_list *l = &c->pkcs11_list; if (msg && strbegins(msg, ">PKCS11ID-COUNT:")) { l->count = strtoul(msg+16, NULL, 10); } else { WriteStatusLog(c, L"GUI> ", L"Invalid pkcs11-id-count ignored", false); l->state &= ~STATE_GET_COUNT; } if (l->count == 0) { l->state |= STATE_FILLED; } } /* * Callback for receiving pkcs11 entry from daemon. * Expect msg = >PKCS11ID-ENTRY:'index', ID:'', BLOB:'' */ static void pkcs11_entry_recv(connection_t *c, char *msg) { struct pkcs11_list *l = &c->pkcs11_list; UINT index = (UINT) -1; if (msg && strbegins(msg, ">PKCS11ID-ENTRY:")) { index = pkcs11_entry_parse(msg+16, l); } if (index == (UINT) -1) { WriteStatusLog(c, L"GUI> ", L"Invalid pkcs11 entry ignored.", false); return; } else if (index + 1 == l->count) /* done */ { l->state |= STATE_FILLED; } } /* * Helper to populate the pkcs11 list by querying the daemon * * The requests are queued and completed asynchronously. * We return immediately if already in progress or * not yet ready to populate. This is called by * pkcs11_listview_fill until state & STATE_FILLED evaluates to 1. * * To recreate the list afresh, call pkcs11_list_clear() first. */ static void pkcs11_list_update(connection_t *c) { struct pkcs11_list *l = &c->pkcs11_list; if ((l->state & STATE_GET_COUNT) == 0) { ManagementCommand(c, "pkcs11-id-count", pkcs11_count_recv, regular); l->state |= STATE_GET_COUNT; } else if (l->count > 0 && (l->state & STATE_GET_ENTRY) == 0) { if (!l->pe && !pkcs11_list_alloc(l)) { WriteStatusLog(c, L"GUI> ", L"Out of memory for pkcs11 entry list", false); l->count = 0; l->state |= STATE_FILLED; return; } /* required space = strlen("pkcs11-id-get ") + 10 + 1 = 25 */ char cmd[25]; for (UINT i = 0; i < l->count; i++) { _snprintf_0(cmd, "pkcs11-id-get %u", i); ManagementCommand(c, cmd, pkcs11_entry_recv, regular); } l->state |= STATE_GET_ENTRY; } } static void listview_set_column_width(HWND lv) { for (int i = 0; i < 3; i++) { /* MSDN docs on this is not clear, but using AUTOSIZE_USEHEADER * instead of AUTOSIZE ensures the column is wide enough for * both header and content strings -- must be set again if/when * content or header strings are modified. */ ListView_SetColumnWidth(lv, i, LVSCW_AUTOSIZE_USEHEADER); } } /* * Position widgets in pkcs11 list window using current dpi. * Takes client area width and height in screen pixels as input. */ static void pkcs11_listview_resize(HWND hwnd, UINT w, UINT h) { HWND lv = GetDlgItem(hwnd, ID_LVW_PKCS11); MoveWindow(lv, DPI_SCALE(20), DPI_SCALE(25), w - DPI_SCALE(40), h - DPI_SCALE(120), TRUE); MoveWindow(GetDlgItem(hwnd, ID_TXT_PKCS11), DPI_SCALE(20), DPI_SCALE(5), w-DPI_SCALE(30), DPI_SCALE(15), TRUE); MoveWindow(GetDlgItem(hwnd, ID_TXT_WARNING), DPI_SCALE(20), h - DPI_SCALE(80), w-DPI_SCALE(20), DPI_SCALE(30), TRUE); MoveWindow(GetDlgItem(hwnd, IDOK), DPI_SCALE(20), h - DPI_SCALE(30), DPI_SCALE(60), DPI_SCALE(23), TRUE); MoveWindow(GetDlgItem(hwnd, IDCANCEL), DPI_SCALE(90), h - DPI_SCALE(30), DPI_SCALE(60), DPI_SCALE(23), TRUE); MoveWindow(GetDlgItem(hwnd, IDRETRY), DPI_SCALE(200), h - DPI_SCALE(30), DPI_SCALE(60), DPI_SCALE(23), TRUE); listview_set_column_width(lv); } /* initialize the listview widget for displaying pkcs11 entries */ static HWND pkcs11_listview_init(HWND parent) { HWND lv; RECT rc; lv = GetDlgItem(parent, ID_LVW_PKCS11); if (!lv) { return NULL; } SendMessage(lv, LVM_SETEXTENDEDLISTVIEWSTYLE, 0, LVS_EX_FULLROWSELECT); /* Use bold font for header */ HFONT hf = (HFONT) SendMessage(lv, WM_GETFONT, 0, 0); if (hf) { LOGFONT lf; GetObject(hf, sizeof(LOGFONT), &lf); lf.lfWeight = FW_BOLD; HFONT hfb = CreateFontIndirect(&lf); if (hfb) { SendMessage(ListView_GetHeader(lv), WM_SETFONT, (WPARAM)hfb, 1); SetProp(parent, L"header_font", (HANDLE)hfb); } } /* Add column headings */ int hdrs[] = {IDS_CERT_DISPLAYNAME, IDS_CERT_ISSUER, IDS_CERT_NOTAFTER}; LVCOLUMNW lvc; lvc.mask = LVCF_TEXT | LVCF_SUBITEM; for (int i = 0; i < 3; i++) { lvc.iSubItem = i; lvc.pszText = LoadLocalizedString(hdrs[i]); ListView_InsertColumn(lv, i, &lvc); } GetClientRect(parent, &rc); pkcs11_listview_resize(parent, rc.right-rc.left, rc.bottom-rc.top); EnableWindow(lv, FALSE); /* disable until filled in */ EnableWindow(GetDlgItem(parent, IDOK), FALSE); EnableWindow(GetDlgItem(parent, IDRETRY), FALSE); return lv; } /* Populate the pkcs11 list and listview widget. Unless the state * of the list evaluates to STATE_FILLED, a callback to this * is requeued. Meant to be used as a Timer callback. */ static void CALLBACK pkcs11_listview_fill(HWND hwnd, UINT UNUSED msg, UINT_PTR id, DWORD UNUSED now) { connection_t *c = (connection_t *) GetProp(hwnd, cfgProp); struct pkcs11_list *l = &c->pkcs11_list; HWND lv = GetDlgItem(hwnd, ID_LVW_PKCS11); LVITEMW lvi = {0}; lvi.mask = LVIF_TEXT|LVIF_PARAM; if ((l->state & STATE_FILLED) == 0) { /* request list update and set a timer to call this routine again */ pkcs11_list_update(c); SetTimer(hwnd, id, 100, pkcs11_listview_fill); } else { int pos; for (UINT i = 0; i < l->count; i++) { lvi.iItem = i; lvi.iSubItem = 0; lvi.pszText = l->pe[i].cert.commonname; lvi.lParam = (LPARAM) i; pos = ListView_InsertItem(lv, &lvi); ListView_SetItemText(lv, pos, 1, l->pe[i].cert.issuer); ListView_SetItemText(lv, pos, 2, l->pe[i].cert.notAfter); } if (l->count == 0) { /* no certificates -- show a message and let user retry */ SetDlgItemTextW(hwnd, ID_TXT_WARNING, LoadLocalizedString(IDS_ERR_NO_PKCS11)); EnableWindow(GetDlgItem(hwnd, IDRETRY), TRUE); } else { EnableWindow(lv, TRUE); SetFocus(lv); EnableWindow(GetDlgItem(hwnd, IDOK), TRUE); EnableWindow(GetDlgItem(hwnd, IDRETRY), TRUE); SendMessage(GetDlgItem(hwnd, IDOK), BM_SETSTYLE, BS_DEFPUSHBUTTON, TRUE); SendMessage(GetDlgItem(hwnd, IDCANCEL), BM_SETSTYLE, BS_PUSHBUTTON, TRUE); SendMessage(GetDlgItem(hwnd, IDRETRY), BM_SETSTYLE, BS_PUSHBUTTON, TRUE); } /* if there is only one item, select it by default */ if (l->count == 1) { ListView_SetItemState(lv, pos, LVIS_SELECTED, LVIS_SELECTED); } listview_set_column_width(lv); KillTimer(hwnd, id); } } /* Reset the listview, clear the list and initiate a fresh scan * without closing the dialog. */ static void pkcs11_listview_reset(HWND parent) { connection_t *c = (connection_t *) GetProp(parent, cfgProp); struct pkcs11_list *l = &c->pkcs11_list; HWND lv = GetDlgItem(parent, ID_LVW_PKCS11); /* ensure the list is not being built and the widget exists */ if ((l->state & STATE_FILLED) == 0 || !lv) { return; } /* clear the list and listview */ pkcs11_list_clear(l); EnableWindow(lv, FALSE); EnableWindow(GetDlgItem(parent, IDOK), FALSE); EnableWindow(GetDlgItem(parent, IDRETRY), FALSE); ListView_DeleteAllItems(lv); SetDlgItemTextW(parent, ID_TXT_WARNING, L""); /* initiate a rebuild of the list */ SetTimer(parent, 0, 100, pkcs11_listview_fill); } void display_certificate(HWND parent, connection_t *c, UINT i) { struct pkcs11_list *l = &c->pkcs11_list; if (i < l->count) { /* Currently cryptui.lib is missing in mingw for i686 * Remove this and corresponding check in configure.ac * when that changes. */ #if defined(HAVE_LIBCRYPTUI) || defined (_MSC_VER) CryptUIDlgViewContext(CERT_STORE_CERTIFICATE_CONTEXT, l->pe[i].cert.ctx, parent, L"Certificate", 0, NULL); #else (void) i; (void) parent; WriteStatusLog(c, L"GUI> ", L"Certificate display not supported in this build", false); #endif } } /* Dialog proc for querying pkcs11 */ static INT_PTR CALLBACK QueryPkcs11DialogProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam) { connection_t *c; switch (msg) { case WM_INITDIALOG: c = (connection_t *) lParam; SetProp(hwndDlg, cfgProp, (HANDLE)lParam); SetStatusWinIcon(hwndDlg, ID_ICO_APP); /* init the listview and schedule a call to listview_fill */ if (pkcs11_listview_init(hwndDlg)) { SetTimer(hwndDlg, 0, 50, pkcs11_listview_fill); } else { WriteStatusLog(c, L"GUI> ", L"Error initializing pkcs11 selection dialog.", false); EndDialog(hwndDlg, wParam); } return TRUE; case WM_COMMAND: c = (connection_t *) GetProp(hwndDlg, cfgProp); if (LOWORD(wParam) == IDOK) { HWND lv = GetDlgItem(hwndDlg, ID_LVW_PKCS11); int id = (int) ListView_GetNextItem(lv, -1, LVNI_ALL|LVNI_SELECTED); LVITEM lvi = {.iItem = id, .mask = LVIF_PARAM}; if (id >= 0 && ListView_GetItem(lv, &lvi)) { c->pkcs11_list.selected = (UINT) lvi.lParam; c->pkcs11_list.state |= STATE_SELECTED; } else if (c->pkcs11_list.count > 0) { /* No selection -- show an error message */ SetDlgItemTextW(hwndDlg, ID_TXT_WARNING, LoadLocalizedString(IDS_ERR_SELECT_PKCS11)); return TRUE; } EndDialog(hwndDlg, wParam); return TRUE; } else if (LOWORD(wParam) == IDCANCEL) { StopOpenVPN(c); EndDialog(hwndDlg, wParam); return TRUE; } else if (LOWORD(wParam) == IDRETRY) { pkcs11_listview_reset(hwndDlg); return TRUE; } break; case WM_CTLCOLORSTATIC: if (GetDlgCtrlID((HWND) lParam) == ID_TXT_WARNING) { HBRUSH br = (HBRUSH) DefWindowProc(hwndDlg, msg, wParam, lParam); COLORREF clr = o.clr_warning; SetTextColor((HDC) wParam, clr); return (INT_PTR) br; } break; case WM_SIZE: pkcs11_listview_resize(hwndDlg, LOWORD(lParam), HIWORD(lParam)); InvalidateRect(hwndDlg, NULL, TRUE); return FALSE; case WM_NOTIFY: c = (connection_t *) GetProp(hwndDlg, cfgProp); if (((NMHDR *)lParam)->idFrom == ID_LVW_PKCS11) { NMITEMACTIVATE *ln = (NMITEMACTIVATE *) lParam; if (ln->iItem >= 0 && ln->uNewState & LVNI_SELECTED) { /* remove the no-selection warning */ SetDlgItemTextW(hwndDlg, ID_TXT_WARNING, L""); } if (ln->hdr.code == NM_DBLCLK && ln->iItem >= 0) { display_certificate(hwndDlg, c, (UINT) ln->iItem); } } break; case WM_CLOSE: c = (connection_t *) GetProp(hwndDlg, cfgProp); StopOpenVPN(c); EndDialog(hwndDlg, wParam); return TRUE; case WM_NCDESTROY: RemoveProp(hwndDlg, cfgProp); HFONT hf = (HFONT) GetProp(hwndDlg, L"header_font"); if (hf) { DeleteObject(hf); } RemoveProp(hwndDlg, cfgProp); break; } return FALSE; }