2003-10-31 12:27:14 +00:00
|
|
|
/*
|
|
|
|
* PKCS15 emulation layer for OpenPGP card.
|
2003-10-31 16:01:35 +00:00
|
|
|
* To see how this works, run p15dump on your OpenPGP card.
|
2003-10-31 12:27:14 +00:00
|
|
|
*
|
|
|
|
* Copyright (C) 2003, Olaf Kirch <okir@suse.de>
|
|
|
|
*
|
|
|
|
* This library is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
|
|
* License as published by the Free Software Foundation; either
|
|
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This library 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
|
|
|
|
* Lesser General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
|
|
* License along with this library; if not, write to the Free Software
|
|
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
*/
|
|
|
|
|
2015-04-22 21:55:33 +00:00
|
|
|
#if HAVE_CONFIG_H
|
2010-03-04 08:14:36 +00:00
|
|
|
#include "config.h"
|
2015-04-22 21:55:33 +00:00
|
|
|
#endif
|
2010-03-04 08:14:36 +00:00
|
|
|
|
2003-10-31 12:27:14 +00:00
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <assert.h>
|
2010-03-04 08:14:36 +00:00
|
|
|
|
|
|
|
#include "common/compat_strlcpy.h"
|
|
|
|
#include "internal.h"
|
|
|
|
#include "pkcs15.h"
|
|
|
|
#include "log.h"
|
2003-10-31 12:27:14 +00:00
|
|
|
|
2013-02-26 10:37:16 +00:00
|
|
|
static int sc_pkcs15emu_openpgp_add_data(sc_pkcs15_card_t *);
|
2004-12-15 17:34:15 +00:00
|
|
|
|
2011-08-10 13:45:40 +00:00
|
|
|
|
2012-05-06 15:59:34 +00:00
|
|
|
#define PGP_USER_PIN_FLAGS (SC_PKCS15_PIN_FLAG_CASE_SENSITIVE \
|
|
|
|
| SC_PKCS15_PIN_FLAG_INITIALIZED \
|
|
|
|
| SC_PKCS15_PIN_FLAG_LOCAL)
|
|
|
|
#define PGP_ADMIN_PIN_FLAGS (PGP_USER_PIN_FLAGS \
|
|
|
|
| SC_PKCS15_PIN_FLAG_UNBLOCK_DISABLED \
|
|
|
|
| SC_PKCS15_PIN_FLAG_SO_PIN)
|
|
|
|
|
2013-02-26 10:37:16 +00:00
|
|
|
#define PGP_NUM_PRIVDO 4
|
|
|
|
|
2012-05-06 15:59:34 +00:00
|
|
|
typedef struct _pgp_pin_cfg {
|
|
|
|
const char *label;
|
|
|
|
int reference;
|
|
|
|
unsigned int flags;
|
|
|
|
int min_length;
|
|
|
|
int do_index;
|
|
|
|
} pgp_pin_cfg_t;
|
|
|
|
|
|
|
|
/* OpenPGP cards v1:
|
|
|
|
* "Signature PIN2 & "Encryption PIN" are two different PINs - not sync'ed by hardware
|
|
|
|
*/
|
|
|
|
static const pgp_pin_cfg_t pin_cfg_v1[3] = {
|
2012-06-02 06:13:55 +00:00
|
|
|
{ "Encryption PIN", 0x02, PGP_USER_PIN_FLAGS, 6, 1 }, // used for PSO:DEC, INT-AUT, {GET,PUT} DATA
|
2016-08-11 16:36:57 +00:00
|
|
|
{ "Signature PIN", 0x01, PGP_USER_PIN_FLAGS, 6, 0 }, // used for PSO:CDS
|
2012-06-02 06:13:55 +00:00
|
|
|
{ "Admin PIN", 0x03, PGP_ADMIN_PIN_FLAGS, 8, 2 }
|
2012-05-06 15:59:34 +00:00
|
|
|
};
|
|
|
|
/* OpenPGP cards v2:
|
2012-06-02 06:13:55 +00:00
|
|
|
* "User PIN (sig)" & "User PIN" are the same PIN, but use different references depending on action
|
2012-05-06 15:59:34 +00:00
|
|
|
*/
|
|
|
|
static const pgp_pin_cfg_t pin_cfg_v2[3] = {
|
2012-06-02 06:13:55 +00:00
|
|
|
{ "User PIN", 0x02, PGP_USER_PIN_FLAGS, 6, 0 }, // used for PSO:DEC, INT-AUT, {GET,PUT} DATA
|
2016-08-11 16:36:57 +00:00
|
|
|
{ "User PIN (sig)", 0x01, PGP_USER_PIN_FLAGS, 6, 0 }, // used for PSO:CDS
|
2012-06-02 06:13:55 +00:00
|
|
|
{ "Admin PIN", 0x03, PGP_ADMIN_PIN_FLAGS, 8, 2 }
|
2012-05-06 15:59:34 +00:00
|
|
|
};
|
|
|
|
|
2020-03-19 01:21:06 +00:00
|
|
|
static struct sc_object_id curve25519_oid = {{1, 3, 6, 1, 4, 1, 3029, 1, 5, 1, -1}};
|
2012-05-06 15:59:34 +00:00
|
|
|
|
2012-05-06 16:35:47 +00:00
|
|
|
#define PGP_SIG_PRKEY_USAGE (SC_PKCS15_PRKEY_USAGE_SIGN \
|
|
|
|
| SC_PKCS15_PRKEY_USAGE_SIGNRECOVER \
|
|
|
|
| SC_PKCS15_PRKEY_USAGE_NONREPUDIATION)
|
|
|
|
#define PGP_ENC_PRKEY_USAGE (SC_PKCS15_PRKEY_USAGE_DECRYPT \
|
|
|
|
| SC_PKCS15_PRKEY_USAGE_UNWRAP)
|
|
|
|
#define PGP_AUTH_PRKEY_USAGE (SC_PKCS15_PRKEY_USAGE_NONREPUDIATION)
|
|
|
|
|
|
|
|
#define PGP_SIG_PUBKEY_USAGE (SC_PKCS15_PRKEY_USAGE_VERIFY \
|
|
|
|
| SC_PKCS15_PRKEY_USAGE_VERIFYRECOVER)
|
|
|
|
#define PGP_ENC_PUBKEY_USAGE (SC_PKCS15_PRKEY_USAGE_ENCRYPT \
|
|
|
|
| SC_PKCS15_PRKEY_USAGE_WRAP)
|
|
|
|
#define PGP_AUTH_PUBKEY_USAGE (SC_PKCS15_PRKEY_USAGE_VERIFY)
|
|
|
|
|
|
|
|
typedef struct _pgp_key_cfg {
|
|
|
|
const char *label;
|
|
|
|
const char *pubkey_path;
|
|
|
|
int prkey_pin;
|
|
|
|
int prkey_usage;
|
|
|
|
int pubkey_usage;
|
|
|
|
} pgp_key_cfg_t;
|
|
|
|
|
|
|
|
static const pgp_key_cfg_t key_cfg[3] = {
|
|
|
|
{ "Signature key", "B601", 1, PGP_SIG_PRKEY_USAGE, PGP_SIG_PUBKEY_USAGE },
|
|
|
|
{ "Encryption key", "B801", 2, PGP_ENC_PRKEY_USAGE, PGP_ENC_PUBKEY_USAGE },
|
2012-07-17 13:24:42 +00:00
|
|
|
{ "Authentication key", "A401", 2, PGP_AUTH_PRKEY_USAGE | PGP_ENC_PRKEY_USAGE, PGP_AUTH_PUBKEY_USAGE | PGP_ENC_PUBKEY_USAGE }
|
2012-05-06 16:35:47 +00:00
|
|
|
};
|
|
|
|
|
2003-10-31 16:01:35 +00:00
|
|
|
|
2012-05-06 19:29:06 +00:00
|
|
|
typedef struct _pgp_manuf_map {
|
2013-08-13 13:30:37 +00:00
|
|
|
unsigned short id;
|
2012-05-06 19:29:06 +00:00
|
|
|
const char *name;
|
|
|
|
} pgp_manuf_map_t;
|
|
|
|
|
|
|
|
static const pgp_manuf_map_t manuf_map[] = {
|
2020-02-08 07:58:26 +00:00
|
|
|
{ 0x0001, "PPC Card Systems" },
|
|
|
|
{ 0x0002, "Prism" },
|
|
|
|
{ 0x0003, "OpenFortress" },
|
|
|
|
{ 0x0004, "Wewid AB" },
|
|
|
|
{ 0x0005, "ZeitControl" },
|
|
|
|
{ 0x0006, "Yubico" },
|
|
|
|
{ 0x0007, "OpenKMS" },
|
|
|
|
{ 0x0008, "LogoEmail" },
|
|
|
|
{ 0x0009, "Fidesmo" },
|
|
|
|
{ 0x000A, "Dangerous Things" },
|
|
|
|
{ 0x000B, "Feitian Technologies" },
|
|
|
|
{ 0x002A, "Magrathea" },
|
|
|
|
{ 0x0042, "GnuPG e.V." },
|
|
|
|
{ 0x1337, "Warsaw Hackerspace" },
|
|
|
|
{ 0x2342, "warpzone" },
|
|
|
|
{ 0x4354, "Confidential Technologies" },
|
|
|
|
{ 0x5443, "TIF-IT e.V." },
|
|
|
|
{ 0x63AF, "Trustica" },
|
2020-03-07 09:25:07 +00:00
|
|
|
{ 0xBA53, "c-base e.V." },
|
2020-02-08 07:58:26 +00:00
|
|
|
{ 0xBD0E, "Paranoidlabs" },
|
|
|
|
{ 0xF517, "FSIJ" },
|
2020-03-07 09:25:07 +00:00
|
|
|
{ 0xF5EC, "F-Secure" },
|
2020-02-08 07:58:26 +00:00
|
|
|
{ 0x0000, "test card" },
|
|
|
|
{ 0xffff, "test card" },
|
2012-05-06 19:29:06 +00:00
|
|
|
{ 0, NULL }
|
|
|
|
};
|
|
|
|
|
2003-10-31 12:27:14 +00:00
|
|
|
/*
|
|
|
|
* This function pretty much follows what find_tlv in the GNUpg
|
|
|
|
* code does.
|
|
|
|
*/
|
|
|
|
static int
|
2003-10-31 16:01:35 +00:00
|
|
|
read_file(sc_card_t *card, const char *path_name, void *buf, size_t len)
|
2003-10-31 12:27:14 +00:00
|
|
|
{
|
2003-10-31 16:01:35 +00:00
|
|
|
sc_path_t path;
|
|
|
|
sc_file_t *file;
|
2003-10-31 12:27:14 +00:00
|
|
|
int r;
|
|
|
|
|
2003-10-31 16:01:35 +00:00
|
|
|
sc_format_path(path_name, &path);
|
|
|
|
if ((r = sc_select_file(card, &path, &file)) < 0)
|
|
|
|
return r;
|
2003-10-31 12:27:14 +00:00
|
|
|
|
2003-10-31 16:01:35 +00:00
|
|
|
if (file->size < len)
|
|
|
|
len = file->size;
|
2018-04-04 08:02:30 +00:00
|
|
|
|
|
|
|
sc_file_free(file);
|
|
|
|
|
2003-12-18 21:37:34 +00:00
|
|
|
return sc_read_binary(card, 0, (u8 *) buf, len, 0);
|
2003-10-31 12:27:14 +00:00
|
|
|
}
|
|
|
|
|
2004-12-15 17:34:15 +00:00
|
|
|
static int
|
2003-10-31 12:27:14 +00:00
|
|
|
sc_pkcs15emu_openpgp_init(sc_pkcs15_card_t *p15card)
|
|
|
|
{
|
|
|
|
sc_card_t *card = p15card->card;
|
|
|
|
sc_context_t *ctx = card->ctx;
|
|
|
|
char string[256];
|
2012-05-27 09:29:24 +00:00
|
|
|
u8 c4data[10];
|
2019-11-11 09:29:46 +00:00
|
|
|
u8 c5data[100];
|
2003-10-31 12:27:14 +00:00
|
|
|
int r, i;
|
2018-01-05 10:08:14 +00:00
|
|
|
const pgp_pin_cfg_t *pin_cfg = (card->type == SC_CARD_TYPE_OPENPGP_V1)
|
|
|
|
? pin_cfg_v1 : pin_cfg_v2;
|
2012-06-14 01:37:22 +00:00
|
|
|
sc_path_t path;
|
2018-04-04 08:02:30 +00:00
|
|
|
sc_file_t *file = NULL;
|
2012-06-08 16:58:20 +00:00
|
|
|
|
2020-02-24 17:23:23 +00:00
|
|
|
LOG_FUNC_CALLED(ctx);
|
|
|
|
|
2011-08-10 13:45:40 +00:00
|
|
|
set_string(&p15card->tokeninfo->label, "OpenPGP card");
|
2010-10-05 15:44:58 +00:00
|
|
|
set_string(&p15card->tokeninfo->manufacturer_id, "OpenPGP project");
|
2003-10-31 12:27:14 +00:00
|
|
|
|
2012-05-06 19:29:06 +00:00
|
|
|
/* card->serialnr = 2 byte manufacturer_id + 4 byte serial_number */
|
|
|
|
if (card->serialnr.len > 0) {
|
2013-08-13 13:30:37 +00:00
|
|
|
unsigned short manuf_id = bebytes2ushort(card->serialnr.value);
|
2012-05-06 19:29:06 +00:00
|
|
|
int j;
|
|
|
|
|
2019-03-03 20:26:20 +00:00
|
|
|
sc_bin_to_hex(card->serialnr.value, card->serialnr.len, string, sizeof(string), 0);
|
2012-05-06 19:29:06 +00:00
|
|
|
set_string(&p15card->tokeninfo->serial_number, string);
|
|
|
|
|
|
|
|
for (j = 0; manuf_map[j].name != NULL; j++) {
|
|
|
|
if (manuf_id == manuf_map[j].id) {
|
|
|
|
set_string(&p15card->tokeninfo->manufacturer_id, manuf_map[j].name);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2003-10-31 12:27:14 +00:00
|
|
|
|
2010-10-05 15:44:58 +00:00
|
|
|
p15card->tokeninfo->flags = SC_PKCS15_TOKEN_PRN_GENERATION | SC_PKCS15_TOKEN_EID_COMPLIANT;
|
2003-10-31 12:27:14 +00:00
|
|
|
|
|
|
|
/* Extract preferred language */
|
2012-05-06 19:07:01 +00:00
|
|
|
r = read_file(card, "0065:5f2d", string, sizeof(string)-1);
|
2003-10-31 16:01:35 +00:00
|
|
|
if (r < 0)
|
2003-10-31 12:27:14 +00:00
|
|
|
goto failed;
|
2003-10-31 16:01:35 +00:00
|
|
|
string[r] = '\0';
|
2010-10-05 15:44:58 +00:00
|
|
|
set_string(&p15card->tokeninfo->preferred_language, string);
|
2003-10-31 12:27:14 +00:00
|
|
|
|
2012-05-06 15:59:34 +00:00
|
|
|
/* Get CHV status bytes from DO 006E/0073/00C4:
|
2011-07-14 14:21:34 +00:00
|
|
|
* 00: 1 == user consent for signature PIN
|
2012-05-06 15:59:34 +00:00
|
|
|
* (i.e. PIN still valid for next PSO:CDS command)
|
2003-10-31 12:27:14 +00:00
|
|
|
* 01-03: max length of pins 1-3
|
|
|
|
* 04-07: tries left for pins 1-3
|
|
|
|
*/
|
2020-02-24 17:23:23 +00:00
|
|
|
sc_log(ctx, "Reading PW status bytes");
|
2012-05-27 09:29:24 +00:00
|
|
|
if ((r = read_file(card, "006E:0073:00C4", c4data, sizeof(c4data))) < 0)
|
2003-10-31 12:27:14 +00:00
|
|
|
goto failed;
|
|
|
|
if (r != 7) {
|
2018-11-22 08:31:29 +00:00
|
|
|
sc_log(ctx,
|
2011-07-14 14:21:34 +00:00
|
|
|
"CHV status bytes have unexpected length (expected 7, got %d)\n", r);
|
2003-10-31 12:27:14 +00:00
|
|
|
return SC_ERROR_OBJECT_NOT_VALID;
|
|
|
|
}
|
|
|
|
|
2011-08-10 13:45:40 +00:00
|
|
|
/* Add PIN codes */
|
2003-10-31 12:27:14 +00:00
|
|
|
for (i = 0; i < 3; i++) {
|
2012-05-06 16:13:28 +00:00
|
|
|
sc_pkcs15_auth_info_t pin_info;
|
|
|
|
sc_pkcs15_object_t pin_obj;
|
2005-02-06 21:34:59 +00:00
|
|
|
|
|
|
|
memset(&pin_info, 0, sizeof(pin_info));
|
|
|
|
memset(&pin_obj, 0, sizeof(pin_obj));
|
2003-10-31 12:27:14 +00:00
|
|
|
|
2011-06-05 15:46:25 +00:00
|
|
|
pin_info.auth_type = SC_PKCS15_PIN_AUTH_TYPE_PIN;
|
2012-05-06 15:59:34 +00:00
|
|
|
pin_info.auth_id.len = 1;
|
2016-08-11 16:36:57 +00:00
|
|
|
pin_info.auth_id.value[0] = pin_cfg[i].reference;
|
2012-05-06 15:59:34 +00:00
|
|
|
pin_info.attrs.pin.reference = pin_cfg[i].reference;
|
|
|
|
pin_info.attrs.pin.flags = pin_cfg[i].flags;
|
2012-05-20 19:04:57 +00:00
|
|
|
pin_info.attrs.pin.type = SC_PKCS15_PIN_TYPE_UTF8;
|
2012-05-06 15:59:34 +00:00
|
|
|
pin_info.attrs.pin.min_length = pin_cfg[i].min_length;
|
2012-05-27 09:29:24 +00:00
|
|
|
pin_info.attrs.pin.stored_length = c4data[1 + pin_cfg[i].do_index];
|
|
|
|
pin_info.attrs.pin.max_length = c4data[1 + pin_cfg[i].do_index];
|
2011-06-05 15:46:25 +00:00
|
|
|
pin_info.attrs.pin.pad_char = '\0';
|
2012-05-27 09:29:24 +00:00
|
|
|
pin_info.tries_left = c4data[4 + pin_cfg[i].do_index];
|
2016-08-11 16:26:01 +00:00
|
|
|
pin_info.logged_in = SC_PIN_STATE_UNKNOWN;
|
2012-05-06 15:59:34 +00:00
|
|
|
|
2005-02-06 21:34:59 +00:00
|
|
|
sc_format_path("3F00", &pin_info.path);
|
2012-05-06 15:59:34 +00:00
|
|
|
|
|
|
|
strlcpy(pin_obj.label, pin_cfg[i].label, sizeof(pin_obj.label));
|
2005-02-06 21:34:59 +00:00
|
|
|
pin_obj.flags = SC_PKCS15_CO_FLAG_MODIFIABLE | SC_PKCS15_CO_FLAG_PRIVATE;
|
2016-08-13 22:55:13 +00:00
|
|
|
if (i < 2) {
|
|
|
|
pin_obj.auth_id.len = 1;
|
|
|
|
pin_obj.auth_id.value[0] = 3;
|
|
|
|
}
|
2005-02-06 21:34:59 +00:00
|
|
|
|
|
|
|
r = sc_pkcs15emu_add_pin_obj(p15card, &pin_obj, &pin_info);
|
|
|
|
if (r < 0)
|
|
|
|
return SC_ERROR_INTERNAL;
|
2003-10-31 16:01:35 +00:00
|
|
|
}
|
|
|
|
|
2012-05-27 09:29:24 +00:00
|
|
|
/* Get private key finger prints from DO 006E/0073/00C5:
|
|
|
|
* 00-19: finger print for SIG key
|
|
|
|
* 20-39: finger print for ENC key
|
|
|
|
* 40-59: finger print for AUT key
|
|
|
|
*/
|
2020-02-24 17:23:23 +00:00
|
|
|
sc_log(ctx, "Reading Fingerprints");
|
2012-05-27 09:29:24 +00:00
|
|
|
if ((r = read_file(card, "006E:0073:00C5", c5data, sizeof(c5data))) < 0)
|
|
|
|
goto failed;
|
2019-11-11 09:29:46 +00:00
|
|
|
if (r < 60) {
|
2020-03-19 01:21:06 +00:00
|
|
|
sc_log(ctx,
|
2012-05-27 09:29:24 +00:00
|
|
|
"finger print bytes have unexpected length (expected 60, got %d)\n", r);
|
|
|
|
return SC_ERROR_OBJECT_NOT_VALID;
|
|
|
|
}
|
|
|
|
|
2020-02-24 17:23:23 +00:00
|
|
|
sc_log(ctx, "Adding private keys");
|
2020-08-29 08:34:51 +00:00
|
|
|
/* XXX: check if "halfkeys" can be stored with gpg2. If not, add key pairs in one loop */
|
2003-10-31 16:01:35 +00:00
|
|
|
for (i = 0; i < 3; i++) {
|
2012-05-06 16:13:28 +00:00
|
|
|
sc_pkcs15_prkey_info_t prkey_info;
|
|
|
|
sc_pkcs15_object_t prkey_obj;
|
2019-01-30 21:00:36 +00:00
|
|
|
u8 cxdata[12];
|
2020-02-24 17:17:45 +00:00
|
|
|
int cxdata_len = sizeof(cxdata);
|
2012-05-27 09:29:24 +00:00
|
|
|
char path_template[] = "006E:0073:00Cx";
|
|
|
|
int j;
|
2012-05-06 16:35:47 +00:00
|
|
|
|
2005-02-06 21:34:59 +00:00
|
|
|
memset(&prkey_info, 0, sizeof(prkey_info));
|
|
|
|
memset(&prkey_obj, 0, sizeof(prkey_obj));
|
2019-01-30 21:00:36 +00:00
|
|
|
memset(&cxdata, 0, sizeof(cxdata));
|
2005-02-06 21:34:59 +00:00
|
|
|
|
2012-05-06 19:07:01 +00:00
|
|
|
path_template[13] = '1' + i; /* The needed tags are C1 C2 and C3 */
|
2020-02-24 17:17:45 +00:00
|
|
|
if ((cxdata_len = read_file(card, path_template, cxdata, sizeof(cxdata))) < 1)
|
2011-07-14 14:21:34 +00:00
|
|
|
goto failed;
|
|
|
|
|
2012-05-27 09:29:24 +00:00
|
|
|
/* check validity using finger prints */
|
|
|
|
for (j = 19; j >= 0; j--) {
|
|
|
|
if (c5data[20 * i + j] != '\0')
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* only add valid keys, i.e. those with a legal algorithm identifier & finger print */
|
|
|
|
if (j >= 0 && cxdata[0] != 0) {
|
2020-02-24 17:17:45 +00:00
|
|
|
struct sc_object_id oid;
|
2020-03-19 01:21:06 +00:00
|
|
|
struct sc_algorithm_info * algorithm_info; /* no need to free */
|
2020-02-24 17:17:45 +00:00
|
|
|
|
2020-03-19 01:21:06 +00:00
|
|
|
algorithm_info = NULL;
|
2012-05-06 17:52:58 +00:00
|
|
|
prkey_info.id.len = 1;
|
|
|
|
prkey_info.id.value[0] = i + 1;
|
|
|
|
prkey_info.usage = key_cfg[i].prkey_usage;
|
|
|
|
prkey_info.native = 1;
|
|
|
|
prkey_info.key_reference = i;
|
|
|
|
|
|
|
|
strlcpy(prkey_obj.label, key_cfg[i].label, sizeof(prkey_obj.label));
|
|
|
|
prkey_obj.flags = SC_PKCS15_CO_FLAG_PRIVATE | SC_PKCS15_CO_FLAG_MODIFIABLE;
|
|
|
|
prkey_obj.auth_id.len = 1;
|
|
|
|
prkey_obj.auth_id.value[0] = key_cfg[i].prkey_pin;
|
|
|
|
|
2020-03-19 01:21:06 +00:00
|
|
|
/* need to get size from algorithms using oid */
|
2020-03-27 10:11:08 +00:00
|
|
|
if (cxdata[0] == SC_OPENPGP_KEYALGO_ECDH ||
|
|
|
|
cxdata[0] == SC_OPENPGP_KEYALGO_ECDSA ||
|
|
|
|
cxdata[0] == SC_OPENPGP_KEYALGO_EDDSA) {
|
2021-04-07 17:29:07 +00:00
|
|
|
/* Last byte could be Import-Format of private key, let's ignore it,
|
|
|
|
* as it is not part of OID */
|
|
|
|
if (cxdata[cxdata_len-1] == SC_OPENPGP_KEYFORMAT_EC_STD ||
|
|
|
|
cxdata[cxdata_len-1] == SC_OPENPGP_KEYFORMAT_EC_STDPUB)
|
|
|
|
cxdata_len--;
|
2020-03-27 10:11:08 +00:00
|
|
|
r = sc_asn1_decode_object_id(&cxdata[1], cxdata_len-1, &oid);
|
|
|
|
if (r != SC_SUCCESS) {
|
|
|
|
sc_log(ctx, "Failed to parse OID for elliptic curve algorithm");
|
|
|
|
}
|
|
|
|
}
|
2020-03-19 01:21:06 +00:00
|
|
|
|
|
|
|
switch (cxdata[0]) {
|
|
|
|
case SC_OPENPGP_KEYALGO_ECDH:
|
2020-03-27 10:13:30 +00:00
|
|
|
if (sc_compare_oid(&oid, &curve25519_oid)) {
|
|
|
|
if ((algorithm_info = sc_card_find_xeddsa_alg(card, 0, &oid)))
|
|
|
|
prkey_info.field_length = algorithm_info->key_length;
|
|
|
|
else {
|
|
|
|
sc_log(ctx, "algorithm not found");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
/* Fall through */
|
|
|
|
case SC_OPENPGP_KEYALGO_ECDSA:
|
2020-03-19 01:21:06 +00:00
|
|
|
if((algorithm_info = sc_card_find_ec_alg(card, 0, &oid)))
|
|
|
|
prkey_info.field_length = algorithm_info->key_length;
|
|
|
|
else {
|
2020-03-27 10:12:33 +00:00
|
|
|
sc_log(ctx, "algorithm not found");
|
|
|
|
continue;
|
2020-03-19 01:21:06 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case SC_OPENPGP_KEYALGO_EDDSA:
|
|
|
|
if ((algorithm_info = sc_card_find_eddsa_alg(card, 0, &oid)))
|
|
|
|
prkey_info.field_length = algorithm_info->key_length;
|
|
|
|
else {
|
2020-03-27 10:12:33 +00:00
|
|
|
sc_log(ctx, "algorithm not found");
|
|
|
|
continue;
|
2020-03-19 01:21:06 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2020-02-24 17:17:45 +00:00
|
|
|
switch (cxdata[0]) {
|
|
|
|
case SC_OPENPGP_KEYALGO_EDDSA:
|
2020-03-27 10:13:30 +00:00
|
|
|
/* Filter out invalid usage: EdDSA does not support anything but sign */
|
2020-02-21 09:32:28 +00:00
|
|
|
prkey_info.usage &= PGP_SIG_PRKEY_USAGE;
|
2020-02-24 17:17:45 +00:00
|
|
|
r = sc_pkcs15emu_add_eddsa_prkey(p15card, &prkey_obj, &prkey_info);
|
|
|
|
break;
|
2020-03-27 10:13:30 +00:00
|
|
|
|
2020-02-24 17:17:45 +00:00
|
|
|
case SC_OPENPGP_KEYALGO_ECDH:
|
|
|
|
/* This can result in either ECDSA key or EC_MONTGOMERY
|
|
|
|
* so we need to check OID */
|
2020-03-19 01:21:06 +00:00
|
|
|
if (sc_compare_oid(&oid, &curve25519_oid)) {
|
2020-02-21 09:32:28 +00:00
|
|
|
/* This can do only DERIVE */
|
|
|
|
prkey_info.usage = SC_PKCS15_PRKEY_USAGE_DERIVE;
|
2020-02-24 17:17:45 +00:00
|
|
|
r = sc_pkcs15emu_add_xeddsa_prkey(p15card, &prkey_obj, &prkey_info);
|
|
|
|
break;
|
|
|
|
}
|
2020-02-21 09:32:28 +00:00
|
|
|
prkey_info.usage |= SC_PKCS15_PRKEY_USAGE_DERIVE;
|
|
|
|
prkey_info.usage &= ~PGP_ENC_PRKEY_USAGE;
|
2019-01-30 21:00:36 +00:00
|
|
|
r = sc_pkcs15emu_add_ec_prkey(p15card, &prkey_obj, &prkey_info);
|
2020-02-24 17:17:45 +00:00
|
|
|
break;
|
2020-03-27 10:13:30 +00:00
|
|
|
|
2020-03-19 01:21:06 +00:00
|
|
|
case SC_OPENPGP_KEYALGO_ECDSA:
|
|
|
|
prkey_info.usage = SC_PKCS15_PRKEY_USAGE_SIGN;
|
|
|
|
r = sc_pkcs15emu_add_ec_prkey(p15card, &prkey_obj, &prkey_info);
|
|
|
|
break;
|
2020-03-27 10:13:30 +00:00
|
|
|
|
2020-02-24 17:17:45 +00:00
|
|
|
case SC_OPENPGP_KEYALGO_RSA:
|
2020-03-19 01:21:06 +00:00
|
|
|
if (cxdata_len >= 3) {
|
2020-02-24 17:17:45 +00:00
|
|
|
prkey_info.modulus_length = bebytes2ushort(cxdata + 1);
|
|
|
|
r = sc_pkcs15emu_add_rsa_prkey(p15card, &prkey_obj, &prkey_info);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
/* Fallthrough */
|
|
|
|
default:
|
|
|
|
sc_log(ctx, "Invalid algorithm identifier %x (length = %d)",
|
|
|
|
cxdata[0], r);
|
2019-01-30 21:00:36 +00:00
|
|
|
}
|
|
|
|
|
2012-05-06 17:52:58 +00:00
|
|
|
if (r < 0)
|
|
|
|
return SC_ERROR_INTERNAL;
|
|
|
|
}
|
2003-10-31 16:01:35 +00:00
|
|
|
}
|
2020-02-24 17:23:23 +00:00
|
|
|
|
|
|
|
sc_log(ctx, "Adding public keys");
|
2011-08-10 13:45:40 +00:00
|
|
|
/* Add public keys */
|
2003-10-31 16:01:35 +00:00
|
|
|
for (i = 0; i < 3; i++) {
|
2012-05-06 16:13:28 +00:00
|
|
|
sc_pkcs15_pubkey_info_t pubkey_info;
|
|
|
|
sc_pkcs15_object_t pubkey_obj;
|
2020-03-27 10:13:30 +00:00
|
|
|
u8 cxdata[12];
|
2020-02-24 17:17:45 +00:00
|
|
|
int cxdata_len = sizeof(cxdata);
|
2012-05-27 09:29:24 +00:00
|
|
|
char path_template[] = "006E:0073:00Cx";
|
|
|
|
int j;
|
2005-02-06 21:34:59 +00:00
|
|
|
|
|
|
|
memset(&pubkey_info, 0, sizeof(pubkey_info));
|
|
|
|
memset(&pubkey_obj, 0, sizeof(pubkey_obj));
|
2019-01-30 21:00:36 +00:00
|
|
|
memset(&cxdata, 0, sizeof(cxdata));
|
2005-02-06 21:34:59 +00:00
|
|
|
|
2012-05-06 19:07:01 +00:00
|
|
|
path_template[13] = '1' + i; /* The needed tags are C1 C2 and C3 */
|
2020-02-24 17:17:45 +00:00
|
|
|
if ((cxdata_len = read_file(card, path_template, cxdata, sizeof(cxdata))) < 1)
|
2011-07-14 14:21:34 +00:00
|
|
|
goto failed;
|
|
|
|
|
2012-05-27 09:29:24 +00:00
|
|
|
/* check validity using finger prints */
|
|
|
|
for (j = 19; j >= 0; j--) {
|
|
|
|
if (c5data[20 * i + j] != '\0')
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* only add valid keys, i.e. those with a legal algorithm identifier & finger print */
|
|
|
|
if (j >= 0 && cxdata[0] != 0) {
|
2020-02-24 17:17:45 +00:00
|
|
|
struct sc_object_id oid;
|
2020-03-19 01:21:06 +00:00
|
|
|
struct sc_algorithm_info * algorithm_info; /* no need to free */
|
2020-02-24 17:17:45 +00:00
|
|
|
|
2020-03-19 01:21:06 +00:00
|
|
|
algorithm_info = NULL;
|
2012-05-06 17:52:58 +00:00
|
|
|
pubkey_info.id.len = 1;
|
|
|
|
pubkey_info.id.value[0] = i + 1;
|
|
|
|
pubkey_info.usage = key_cfg[i].pubkey_usage;
|
|
|
|
sc_format_path(key_cfg[i].pubkey_path, &pubkey_info.path);
|
2005-02-06 21:34:59 +00:00
|
|
|
|
2012-05-06 17:52:58 +00:00
|
|
|
strlcpy(pubkey_obj.label, key_cfg[i].label, sizeof(pubkey_obj.label));
|
|
|
|
pubkey_obj.flags = SC_PKCS15_CO_FLAG_MODIFIABLE;
|
2005-02-06 21:34:59 +00:00
|
|
|
|
2020-03-27 10:11:08 +00:00
|
|
|
if (cxdata[0] == SC_OPENPGP_KEYALGO_ECDH ||
|
|
|
|
cxdata[0] == SC_OPENPGP_KEYALGO_ECDSA ||
|
|
|
|
cxdata[0] == SC_OPENPGP_KEYALGO_EDDSA) {
|
2021-04-07 17:29:07 +00:00
|
|
|
/* Last byte could be Import-Format of private key, let's ignore it,
|
|
|
|
* as it is not part of OID */
|
|
|
|
if (cxdata[cxdata_len-1] == SC_OPENPGP_KEYFORMAT_EC_STD ||
|
|
|
|
cxdata[cxdata_len-1] == SC_OPENPGP_KEYFORMAT_EC_STDPUB)
|
|
|
|
cxdata_len--;
|
2020-03-27 10:11:08 +00:00
|
|
|
r = sc_asn1_decode_object_id(&cxdata[1], cxdata_len-1, &oid);
|
|
|
|
if (r != SC_SUCCESS) {
|
|
|
|
sc_log(ctx, "Failed to parse OID for elliptic curve algorithm");
|
|
|
|
}
|
|
|
|
}
|
2020-03-19 01:21:06 +00:00
|
|
|
|
|
|
|
switch (cxdata[0]) {
|
|
|
|
case SC_OPENPGP_KEYALGO_ECDH:
|
2020-03-27 10:13:30 +00:00
|
|
|
if (sc_compare_oid(&oid, &curve25519_oid)) {
|
|
|
|
if ((algorithm_info = sc_card_find_xeddsa_alg(card, 0, &oid)))
|
|
|
|
pubkey_info.field_length = algorithm_info->key_length;
|
|
|
|
else {
|
|
|
|
sc_log(ctx, "algorithm not found");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
/* Fall through */
|
|
|
|
case SC_OPENPGP_KEYALGO_ECDSA:
|
2020-03-19 01:21:06 +00:00
|
|
|
if((algorithm_info = sc_card_find_ec_alg(card, 0, &oid)))
|
|
|
|
pubkey_info.field_length = algorithm_info->key_length;
|
|
|
|
else {
|
2020-03-27 10:12:33 +00:00
|
|
|
sc_log(ctx, "algorithm not found");
|
|
|
|
continue;
|
2020-03-19 01:21:06 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case SC_OPENPGP_KEYALGO_EDDSA:
|
|
|
|
if ((algorithm_info = sc_card_find_eddsa_alg(card, 0, &oid)))
|
|
|
|
pubkey_info.field_length = algorithm_info->key_length;
|
|
|
|
else {
|
2020-03-27 10:12:33 +00:00
|
|
|
sc_log(ctx, "algorithm not found");
|
|
|
|
continue;
|
2020-03-19 01:21:06 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2020-02-24 17:17:45 +00:00
|
|
|
switch (cxdata[0]) {
|
|
|
|
case SC_OPENPGP_KEYALGO_EDDSA:
|
|
|
|
/* assuming Ed25519 as it is the only supported now */
|
2020-02-21 09:32:28 +00:00
|
|
|
/* Filter out invalid usage: ED does not support anything but sign */
|
|
|
|
pubkey_info.usage &= PGP_SIG_PUBKEY_USAGE;
|
2020-02-24 17:17:45 +00:00
|
|
|
r = sc_pkcs15emu_add_eddsa_pubkey(p15card, &pubkey_obj, &pubkey_info);
|
|
|
|
break;
|
|
|
|
case SC_OPENPGP_KEYALGO_ECDH:
|
|
|
|
/* This can result in either ECDSA key or EC_MONTGOMERY
|
|
|
|
* so we need to check OID */
|
2020-03-19 01:21:06 +00:00
|
|
|
if (sc_compare_oid(&oid, &curve25519_oid)) {
|
2020-02-21 09:32:28 +00:00
|
|
|
/* XXX What can this key do? */
|
2020-03-19 01:21:06 +00:00
|
|
|
pubkey_info.usage = SC_PKCS15_PRKEY_USAGE_DERIVE;
|
2020-02-24 17:17:45 +00:00
|
|
|
r = sc_pkcs15emu_add_xeddsa_pubkey(p15card, &pubkey_obj, &pubkey_info);
|
|
|
|
break;
|
|
|
|
}
|
2020-03-19 01:21:06 +00:00
|
|
|
pubkey_info.usage = SC_PKCS15_PRKEY_USAGE_DERIVE;
|
|
|
|
r = sc_pkcs15emu_add_ec_pubkey(p15card, &pubkey_obj, &pubkey_info);
|
|
|
|
break;
|
2020-02-24 17:17:45 +00:00
|
|
|
case SC_OPENPGP_KEYALGO_ECDSA:
|
2020-03-19 01:21:06 +00:00
|
|
|
pubkey_info.usage = PGP_SIG_PUBKEY_USAGE;
|
2019-01-30 21:00:36 +00:00
|
|
|
r = sc_pkcs15emu_add_ec_pubkey(p15card, &pubkey_obj, &pubkey_info);
|
2020-02-24 17:17:45 +00:00
|
|
|
break;
|
|
|
|
case SC_OPENPGP_KEYALGO_RSA:
|
2020-03-27 10:13:30 +00:00
|
|
|
if (cxdata_len >= 3) {
|
|
|
|
pubkey_info.modulus_length = bebytes2ushort(cxdata + 1);
|
|
|
|
r = sc_pkcs15emu_add_rsa_pubkey(p15card, &pubkey_obj, &pubkey_info);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
/* Fall through */
|
2020-02-24 17:17:45 +00:00
|
|
|
default:
|
|
|
|
sc_log(ctx, "Invalid algorithm identifier %x (length = %d)",
|
|
|
|
cxdata[0], r);
|
2019-01-30 21:00:36 +00:00
|
|
|
}
|
|
|
|
|
2012-05-06 17:52:58 +00:00
|
|
|
if (r < 0)
|
|
|
|
return SC_ERROR_INTERNAL;
|
|
|
|
}
|
2003-10-31 12:27:14 +00:00
|
|
|
}
|
|
|
|
|
2012-06-15 12:56:46 +00:00
|
|
|
/* Check if certificate DO 7F21 holds data */
|
2012-06-14 01:37:22 +00:00
|
|
|
sc_format_path("7F21", &path);
|
|
|
|
r = sc_select_file(card, &path, &file);
|
|
|
|
if (r < 0)
|
|
|
|
goto failed;
|
|
|
|
|
2012-06-15 12:56:46 +00:00
|
|
|
/* If DO 7F21 holds data, we declare a cert object for pkcs15 */
|
|
|
|
if (file->size > 0) {
|
|
|
|
struct sc_pkcs15_cert_info cert_info;
|
|
|
|
struct sc_pkcs15_object cert_obj;
|
2012-06-14 01:37:22 +00:00
|
|
|
|
2012-06-15 12:56:46 +00:00
|
|
|
memset(&cert_info, 0, sizeof(cert_info));
|
|
|
|
memset(&cert_obj, 0, sizeof(cert_obj));
|
2012-06-02 05:14:38 +00:00
|
|
|
|
2012-06-15 12:56:46 +00:00
|
|
|
/* Certificate ID. We use the same ID as the authentication key */
|
|
|
|
cert_info.id.value[0] = 3;
|
|
|
|
cert_info.id.len = 1;
|
|
|
|
/* Authority, flag is zero */
|
2012-06-04 02:54:00 +00:00
|
|
|
/* The path following which PKCS15 will find the content of the object */
|
2012-06-15 12:56:46 +00:00
|
|
|
sc_format_path("3F007F21", &cert_info.path);
|
2012-06-04 02:54:00 +00:00
|
|
|
/* Object label */
|
2012-06-15 12:56:46 +00:00
|
|
|
strlcpy(cert_obj.label, "Cardholder certificate", sizeof(cert_obj.label));
|
|
|
|
|
|
|
|
r = sc_pkcs15emu_add_x509_cert(p15card, &cert_obj, &cert_info);
|
|
|
|
if (r < 0)
|
2012-06-04 02:54:40 +00:00
|
|
|
goto failed;
|
2012-06-15 12:56:46 +00:00
|
|
|
}
|
2012-06-02 05:14:38 +00:00
|
|
|
|
2018-04-15 07:37:43 +00:00
|
|
|
/* Add PKCS#15 DATA objects from other OpenPGP card DOs. The return
|
|
|
|
* value is ignored, so this will not cause initialization to fail.
|
|
|
|
*/
|
2017-10-31 09:12:12 +00:00
|
|
|
sc_pkcs15emu_openpgp_add_data(p15card);
|
2013-02-26 10:37:16 +00:00
|
|
|
|
2017-10-31 09:12:12 +00:00
|
|
|
failed:
|
|
|
|
if (r < 0) {
|
2020-03-19 01:21:06 +00:00
|
|
|
sc_log(card->ctx,
|
2017-10-31 09:12:12 +00:00
|
|
|
"Failed to initialize OpenPGP emulation: %s\n",
|
|
|
|
sc_strerror(r));
|
|
|
|
}
|
2018-04-04 08:02:30 +00:00
|
|
|
sc_file_free(file);
|
2003-10-31 12:27:14 +00:00
|
|
|
|
2020-02-24 17:23:23 +00:00
|
|
|
LOG_FUNC_RETURN(ctx, r);
|
2003-10-31 12:27:14 +00:00
|
|
|
}
|
2004-10-08 21:29:55 +00:00
|
|
|
|
2013-02-26 10:37:16 +00:00
|
|
|
static int
|
|
|
|
sc_pkcs15emu_openpgp_add_data(sc_pkcs15_card_t *p15card)
|
|
|
|
{
|
|
|
|
sc_context_t *ctx = p15card->card->ctx;
|
|
|
|
int i, r;
|
|
|
|
|
|
|
|
LOG_FUNC_CALLED(ctx);
|
2018-04-15 07:37:43 +00:00
|
|
|
/* Optional private use DOs 0101 to 0104 */
|
2013-02-26 10:37:16 +00:00
|
|
|
for (i = 1; i <= PGP_NUM_PRIVDO; i++) {
|
|
|
|
sc_pkcs15_data_info_t dat_info;
|
|
|
|
sc_pkcs15_object_t dat_obj;
|
|
|
|
char name[8];
|
|
|
|
char path[9];
|
2013-03-27 04:38:42 +00:00
|
|
|
u8 content[254];
|
2013-02-26 10:37:16 +00:00
|
|
|
memset(&dat_info, 0, sizeof(dat_info));
|
|
|
|
memset(&dat_obj, 0, sizeof(dat_obj));
|
|
|
|
|
2013-04-16 05:04:46 +00:00
|
|
|
snprintf(name, 8, "PrivDO%d", i);
|
|
|
|
snprintf(path, 9, "3F00010%d", i);
|
2013-02-26 10:37:16 +00:00
|
|
|
|
2018-04-15 07:37:43 +00:00
|
|
|
/* Check if the DO can be read and is not empty. Otherwise we
|
|
|
|
* won't expose a PKCS#15 DATA object.
|
2013-03-27 04:38:42 +00:00
|
|
|
*/
|
|
|
|
r = read_file(p15card->card, path, content, sizeof(content));
|
|
|
|
if (r <= 0 ) {
|
2013-03-27 04:39:33 +00:00
|
|
|
sc_log(ctx, "Cannot read DO 010%d or there is no data in it", i);
|
2013-03-27 04:38:42 +00:00
|
|
|
/* Skip */
|
|
|
|
continue;
|
|
|
|
}
|
2013-02-26 10:37:16 +00:00
|
|
|
sc_format_path(path, &dat_info.path);
|
|
|
|
strlcpy(dat_obj.label, name, sizeof(dat_obj.label));
|
|
|
|
strlcpy(dat_info.app_label, name, sizeof(dat_info.app_label));
|
|
|
|
|
2013-03-27 04:38:42 +00:00
|
|
|
/* Add DATA object to slot protected by PIN2 (PW1 with Ref 0x82) */
|
|
|
|
dat_obj.flags = SC_PKCS15_CO_FLAG_PRIVATE | SC_PKCS15_CO_FLAG_MODIFIABLE;
|
|
|
|
dat_obj.auth_id.len = 1;
|
|
|
|
if (i == 1 || i == 3)
|
|
|
|
dat_obj.auth_id.value[0] = 2;
|
|
|
|
else
|
|
|
|
dat_obj.auth_id.value[0] = 3;
|
|
|
|
|
2013-02-26 10:37:16 +00:00
|
|
|
sc_log(ctx, "Add %s data object", name);
|
|
|
|
r = sc_pkcs15emu_add_data_object(p15card, &dat_obj, &dat_info);
|
2018-04-15 07:37:43 +00:00
|
|
|
LOG_TEST_RET(ctx, r, "Could not add data object to framework");
|
2013-02-26 10:37:16 +00:00
|
|
|
}
|
2018-04-15 07:37:43 +00:00
|
|
|
LOG_FUNC_RETURN(ctx, SC_SUCCESS);
|
2013-02-26 10:37:16 +00:00
|
|
|
}
|
|
|
|
|
2004-10-08 21:29:55 +00:00
|
|
|
static int openpgp_detect_card(sc_pkcs15_card_t *p15card)
|
|
|
|
{
|
2018-01-05 10:08:14 +00:00
|
|
|
if (p15card->card->type == SC_CARD_TYPE_OPENPGP_BASE
|
|
|
|
|| p15card->card->type == SC_CARD_TYPE_OPENPGP_V1
|
|
|
|
|| p15card->card->type == SC_CARD_TYPE_OPENPGP_V2
|
|
|
|
|| p15card->card->type == SC_CARD_TYPE_OPENPGP_GNUK
|
|
|
|
|| p15card->card->type == SC_CARD_TYPE_OPENPGP_V3)
|
2011-01-09 10:17:16 +00:00
|
|
|
return SC_SUCCESS;
|
|
|
|
else
|
|
|
|
return SC_ERROR_WRONG_CARD;
|
2004-10-08 21:29:55 +00:00
|
|
|
}
|
|
|
|
|
2019-03-26 14:14:25 +00:00
|
|
|
int sc_pkcs15emu_openpgp_init_ex(sc_pkcs15_card_t *p15card, struct sc_aid *aid)
|
2004-10-08 21:29:55 +00:00
|
|
|
{
|
2019-03-26 14:14:25 +00:00
|
|
|
if (openpgp_detect_card(p15card))
|
|
|
|
return SC_ERROR_WRONG_CARD;
|
|
|
|
return sc_pkcs15emu_openpgp_init(p15card);
|
2004-10-08 21:29:55 +00:00
|
|
|
}
|