- added partial support for GPK 4000
- made line parsing in opensc-explorer saner - moved change_reference_data and reset_retry_counter to iso7816.c, where they belong - added partial libreadline support to opensc-explorer git-svn-id: https://www.opensc-project.org/svnp/opensc/trunk@206 c6295689-39f2-0310-b995-f0e70906c6a9
This commit is contained in:
parent
bfc15fa7fd
commit
b4063302bf
|
@ -10,10 +10,15 @@ lib_LTLIBRARIES = libopensc.la
|
||||||
libopensc_la_SOURCES = asn1.c base64.c sec.c log.c sc.c card.c iso7816.c \
|
libopensc_la_SOURCES = asn1.c base64.c sec.c log.c sc.c card.c iso7816.c \
|
||||||
pkcs15.c pkcs15-cert.c pkcs15-pin.c \
|
pkcs15.c pkcs15-cert.c pkcs15-pin.c \
|
||||||
pkcs15-prkey.c pkcs15-sec.c pkcs15-cache.c \
|
pkcs15-prkey.c pkcs15-sec.c pkcs15-cache.c \
|
||||||
card-setec.c card-flex.c \
|
card-setec.c card-flex.c card-gpk.c \
|
||||||
card-emv.c card-default.c
|
card-emv.c card-default.c
|
||||||
libopensc_la_LDFLAGS = -version-info 0:5:0
|
libopensc_la_LDFLAGS = -version-info 0:5:0
|
||||||
|
|
||||||
|
if HAVE_SSL
|
||||||
|
libopensc_la_LIBADD = @LIBPCSC@ @LIBCRYPTO@
|
||||||
|
else
|
||||||
libopensc_la_LIBADD = @LIBPCSC@
|
libopensc_la_LIBADD = @LIBPCSC@
|
||||||
|
endif
|
||||||
|
|
||||||
include_HEADERS = opensc.h opensc-pkcs15.h opensc-emv.h
|
include_HEADERS = opensc.h opensc-pkcs15.h opensc-emv.h
|
||||||
noinst_HEADERS = sc-asn1.h sc-log.h sc-internal.h
|
noinst_HEADERS = sc-asn1.h sc-log.h sc-internal.h
|
||||||
|
|
|
@ -0,0 +1,823 @@
|
||||||
|
/*
|
||||||
|
* GPK4000 driver for opensc
|
||||||
|
*
|
||||||
|
* Copyright (C) 2002 Olaf Kirch <okir@lst.de>
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "sc-internal.h"
|
||||||
|
#include "sc-log.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <openssl/des.h>
|
||||||
|
#include <openssl/rand.h>
|
||||||
|
|
||||||
|
#ifdef HAVE_OPENSSL
|
||||||
|
|
||||||
|
/* GPK4000 variants */
|
||||||
|
enum {
|
||||||
|
GPK4000_su256,
|
||||||
|
GPK4000_s,
|
||||||
|
GPK4000_sp,
|
||||||
|
GPK4000_sdo,
|
||||||
|
};
|
||||||
|
|
||||||
|
#define GPK_SEL_MF 0x00
|
||||||
|
#define GPK_SEL_DF 0x01
|
||||||
|
#define GPK_SEL_EF 0x02
|
||||||
|
#define GPK_SEL_AID 0x04
|
||||||
|
#define GPK_FID_MF 0x3F00
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GPK4000 private data
|
||||||
|
*/
|
||||||
|
struct gpk_private_data {
|
||||||
|
int variant;
|
||||||
|
|
||||||
|
/* access control bits of file most recently selected */
|
||||||
|
u_int16_t ac[3];
|
||||||
|
|
||||||
|
/* is non-zero if we should use secure messaging */
|
||||||
|
u_int8_t key_set : 1;
|
||||||
|
u_int8_t key_local : 1,
|
||||||
|
key_sfi : 5;
|
||||||
|
u_int8_t key[16];
|
||||||
|
};
|
||||||
|
#define OPSDATA(card) ((struct gpk_private_data *) ((card)->ops_data))
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ATRs of GPK4000 cards courtesy of libscez
|
||||||
|
*/
|
||||||
|
static struct atrinfo {
|
||||||
|
unsigned char atr[SC_MAX_ATR_SIZE];
|
||||||
|
unsigned int atr_len;
|
||||||
|
int variant;
|
||||||
|
} atrlist[] = {
|
||||||
|
{ "\x3B\x27\x00\x80\x65\xA2\x04\x01\x01\x37", 10, GPK4000_s },
|
||||||
|
{ "\x3B\x27\x00\x80\x65\xA2\x05\x01\x01\x37", 10, GPK4000_sp },
|
||||||
|
{ "\x3B\x27\x00\x80\x65\xA2\x0C\x01\x01\x37", 10, GPK4000_su256 },
|
||||||
|
{ "\x3B\xA7\x00\x40\x14\x80\x65\xA2\x14\x01\x01\x37", 12, GPK4000_sdo },
|
||||||
|
|
||||||
|
{ "", 0, -1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Driver and card ops structures
|
||||||
|
*/
|
||||||
|
static struct sc_card_operations gpk_ops;
|
||||||
|
static const struct sc_card_driver gpk_drv = {
|
||||||
|
NULL,
|
||||||
|
"Gemplus GPK 4000 driver",
|
||||||
|
"gpk",
|
||||||
|
&gpk_ops
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Identify the card variant based on the ATR
|
||||||
|
*/
|
||||||
|
static struct atrinfo *
|
||||||
|
gpk_identify(struct sc_card *card)
|
||||||
|
{
|
||||||
|
struct atrinfo *ai;
|
||||||
|
|
||||||
|
for (ai = atrlist; ai->atr_len; ai++) {
|
||||||
|
if (card->atr_len >= ai->atr_len
|
||||||
|
&& !memcmp(card->atr, ai->atr, ai->atr_len))
|
||||||
|
return ai;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* return 1 iff this driver can handle the card
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
gpk_match(struct sc_card *card)
|
||||||
|
{
|
||||||
|
return gpk_identify(card)? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialize the card struct
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
gpk_init(struct sc_card *card)
|
||||||
|
{
|
||||||
|
struct gpk_private_data *priv;
|
||||||
|
struct atrinfo *ai;
|
||||||
|
|
||||||
|
if (!(ai = gpk_identify(card)))
|
||||||
|
return SC_ERROR_INVALID_CARD;
|
||||||
|
card->ops_data = priv = malloc(sizeof(*priv));
|
||||||
|
if (card->ops_data == NULL)
|
||||||
|
return SC_ERROR_OUT_OF_MEMORY;
|
||||||
|
memset(priv, 0, sizeof(*priv));
|
||||||
|
priv->variant = ai->variant;
|
||||||
|
card->cla = 0;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Card is being closed; discard any private data etc
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
gpk_finish(struct sc_card *card)
|
||||||
|
{
|
||||||
|
if (card->ops_data)
|
||||||
|
free(card->ops_data);
|
||||||
|
card->ops_data = NULL;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Error code handling for the GPK4000.
|
||||||
|
* sc_sw_to_errorcode doesn't seem to handle all of the
|
||||||
|
* status words the GPK is capable of returning
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
gpk_sw_to_errorcode(struct sc_card *card, u_int8_t sw1, u_int8_t sw2)
|
||||||
|
{
|
||||||
|
u_int16_t sw = (sw1 << 8) | sw2;
|
||||||
|
|
||||||
|
if ((sw & 0xFFF0) == 0x63C0) {
|
||||||
|
error(card->ctx, "wrong PIN, %u tries left", sw&0xf);
|
||||||
|
return SC_ERROR_PIN_CODE_INCORRECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sw) {
|
||||||
|
case 0x6400:
|
||||||
|
error(card->ctx, "wrong crypto context");
|
||||||
|
return SC_ERROR_OBJECT_NOT_VALID; /* XXX ??? */
|
||||||
|
case 0x6581:
|
||||||
|
error(card->ctx, "out of space on card or file");
|
||||||
|
return SC_ERROR_OUT_OF_MEMORY;
|
||||||
|
case 0x6981:
|
||||||
|
return SC_ERROR_FILE_NOT_FOUND;
|
||||||
|
case 0x6A80:
|
||||||
|
case 0x6b00:
|
||||||
|
return SC_ERROR_INVALID_ARGUMENTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sc_sw_to_errorcode(card, sw1, sw2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Select a DF/EF
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
match_path(struct sc_card *card, u_int16_t **pathptr, size_t *pathlen,
|
||||||
|
int need_info)
|
||||||
|
{
|
||||||
|
u_int16_t *curptr, *ptr;
|
||||||
|
size_t curlen, len;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
curptr = (u_int16_t *) card->cache.current_path.value;
|
||||||
|
curlen = card->cache.current_path.len;
|
||||||
|
ptr = *pathptr;
|
||||||
|
len = *pathlen;
|
||||||
|
|
||||||
|
if (curlen < 1 || len < 1)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
/* Skip the MF if present */
|
||||||
|
if (ptr[0] != GPK_FID_MF) {
|
||||||
|
curptr++;
|
||||||
|
curlen--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len < curlen)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
if (ptr[i] != curptr[i])
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exact match? */
|
||||||
|
if (len == curlen && need_info)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
*pathptr = ptr + i;
|
||||||
|
*pathlen = len - i;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline unsigned int
|
||||||
|
ac_to_acl(u_int16_t ac)
|
||||||
|
{
|
||||||
|
unsigned int npins, pin;
|
||||||
|
unsigned int res = 0;
|
||||||
|
|
||||||
|
npins = (ac >> 14) & 3;
|
||||||
|
if (npins == 3)
|
||||||
|
return SC_AC_NEVER;
|
||||||
|
pin = ac & 0xFF;
|
||||||
|
while (npins--) {
|
||||||
|
switch (pin & 7) {
|
||||||
|
case 0: res |= SC_AC_CHV1; break;
|
||||||
|
case 1: res |= SC_AC_CHV2; break;
|
||||||
|
default:return SC_AC_NEVER;
|
||||||
|
}
|
||||||
|
pin >>= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check whether secure messaging key is specified */
|
||||||
|
if (ac & 0x1F00)
|
||||||
|
res |= SC_AC_PRO;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert ACLs requested by the application to access condition
|
||||||
|
* bits supported by the GPK. Since these do not map 1:1 there's
|
||||||
|
* some fuzz involved.
|
||||||
|
*/
|
||||||
|
static inline void
|
||||||
|
acl_to_ac(unsigned int acl, u_int8_t *ac)
|
||||||
|
{
|
||||||
|
ac[0] = ac[1] = 0;
|
||||||
|
|
||||||
|
if (acl == SC_AC_NEVER) {
|
||||||
|
ac[0] = 0xC0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* XXX should we set the "local" flag for PINs or not?
|
||||||
|
* OpenSC does not provide for a "lock file" operation
|
||||||
|
* that lets us freeze the ac bits after setting up the file.
|
||||||
|
*/
|
||||||
|
if (acl & SC_AC_CHV2) {
|
||||||
|
ac[0] += 0x40;
|
||||||
|
ac[1] |= 1;
|
||||||
|
}
|
||||||
|
if (acl & SC_AC_CHV1) {
|
||||||
|
ac[0] += 0x40;
|
||||||
|
ac[1] <<= 4;
|
||||||
|
ac[1] |= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* XXX should we set the "local" flag on key files or not?
|
||||||
|
* OpenSC does not provide for a "lock file" operation
|
||||||
|
* that lets us freeze the ac bits after setting up the file.
|
||||||
|
*/
|
||||||
|
if (acl & SC_AC_PRO) {
|
||||||
|
ac[0] |= 0x01;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
gpk_parse_fileinfo(struct sc_card *card,
|
||||||
|
const u_int8_t *buf, size_t buflen,
|
||||||
|
struct sc_file *file)
|
||||||
|
{
|
||||||
|
struct gpk_private_data *priv = OPSDATA(card);
|
||||||
|
const u_int8_t *sp, *end, *next;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
memset(file, 0, sizeof(*file));
|
||||||
|
for (i = 0; i < SC_MAX_AC_OPS; i++)
|
||||||
|
file->acl[i] = SC_AC_UNKNOWN;
|
||||||
|
|
||||||
|
end = buf + buflen;
|
||||||
|
for (sp = buf; sp + 2 < end; sp = next) {
|
||||||
|
next = sp + 2 + sp[1];
|
||||||
|
if (next > end)
|
||||||
|
break;
|
||||||
|
if (sp[0] == 0x84) {
|
||||||
|
/* ignore if name is longer than what it should be */
|
||||||
|
if (sp[1] > sizeof(file->name))
|
||||||
|
continue;
|
||||||
|
memset(file->name, 0, sizeof(file->name));
|
||||||
|
memcpy(file->name, sp+2, sp[1]);
|
||||||
|
} else
|
||||||
|
if (sp[0] == 0x85) {
|
||||||
|
unsigned int ac1, ac2, ac3;
|
||||||
|
|
||||||
|
file->id = (sp[4] << 8) | sp[5];
|
||||||
|
file->size = (sp[8] << 8) | sp[9];
|
||||||
|
file->record_length = sp[7];
|
||||||
|
|
||||||
|
/* Map ACLs */
|
||||||
|
priv->ac[0] = (sp[10] << 8) | sp[11];
|
||||||
|
priv->ac[1] = (sp[12] << 8) | sp[13];
|
||||||
|
priv->ac[2] = (sp[14] << 8) | sp[15]; /* EF only */
|
||||||
|
ac1 = ac_to_acl(priv->ac[0]);
|
||||||
|
ac2 = ac_to_acl(priv->ac[1]);
|
||||||
|
ac3 = ac_to_acl(priv->ac[2]);
|
||||||
|
|
||||||
|
/* Examine file type */
|
||||||
|
switch (sp[6] & 7) {
|
||||||
|
case 0x01: case 0x02: case 0x03: case 0x04:
|
||||||
|
case 0x05: case 0x06: case 0x07:
|
||||||
|
file->type = SC_FILE_TYPE_WORKING_EF;
|
||||||
|
file->ef_structure = sp[6] & 7;
|
||||||
|
file->acl[SC_AC_OP_READ] = ac3;
|
||||||
|
file->acl[SC_AC_OP_WRITE] = ac3;
|
||||||
|
file->acl[SC_AC_OP_UPDATE] = ac1;
|
||||||
|
break;
|
||||||
|
case 0x00: /* 0x38 is DF */
|
||||||
|
file->type = SC_FILE_TYPE_DF;
|
||||||
|
file->acl[SC_AC_OP_SELECT] = SC_AC_NONE;
|
||||||
|
file->acl[SC_AC_OP_LOCK] = ac1;
|
||||||
|
/* Icky: the GPK uses different ACLs
|
||||||
|
* for creating data files and
|
||||||
|
* 'sensitive' i.e. key files */
|
||||||
|
file->acl[SC_AC_OP_CREATE] = ac2;
|
||||||
|
file->acl[SC_AC_OP_DELETE] = SC_AC_NEVER;
|
||||||
|
file->acl[SC_AC_OP_REHABILITATE] = SC_AC_NEVER;
|
||||||
|
file->acl[SC_AC_OP_INVALIDATE] = SC_AC_NEVER;
|
||||||
|
file->acl[SC_AC_OP_LIST_FILES] = SC_AC_NEVER;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file->record_length)
|
||||||
|
file->record_count = file->size / file->record_length;
|
||||||
|
file->magic = SC_FILE_MAGIC;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
gpk_select(struct sc_card *card, u_int8_t kind,
|
||||||
|
const u_int8_t *buf, size_t buflen,
|
||||||
|
struct sc_file *file)
|
||||||
|
{
|
||||||
|
struct gpk_private_data *priv = OPSDATA(card);
|
||||||
|
struct sc_apdu apdu;
|
||||||
|
u_int8_t resbuf[SC_MAX_APDU_BUFFER_SIZE];
|
||||||
|
int r;
|
||||||
|
|
||||||
|
/* If we're about to select a DF, invalidate secure messaging keys */
|
||||||
|
if (kind == GPK_SEL_MF || kind == GPK_SEL_DF) {
|
||||||
|
memset(priv->key, 0, sizeof(priv->key));
|
||||||
|
priv->key_set = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* do the apdu thing */
|
||||||
|
sc_format_apdu(card, &apdu, SC_APDU_CASE_3_SHORT, 0xA4, kind, 0);
|
||||||
|
apdu.data = buf;
|
||||||
|
apdu.datalen = buflen;
|
||||||
|
apdu.lc = apdu.datalen;
|
||||||
|
apdu.resp = resbuf;
|
||||||
|
apdu.resplen = file? sizeof(resbuf) : 0;
|
||||||
|
|
||||||
|
r = sc_transmit_apdu(card, &apdu);
|
||||||
|
SC_TEST_RET(card->ctx, r, "APDU transmit failed");
|
||||||
|
r = gpk_sw_to_errorcode(card, apdu.sw1, apdu.sw2);
|
||||||
|
SC_TEST_RET(card->ctx, r, "Card returned error");
|
||||||
|
|
||||||
|
/* Nothing we can say about it... invalidate
|
||||||
|
* path cache */
|
||||||
|
if (kind == GPK_SEL_AID) {
|
||||||
|
card->cache.current_path.len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == NULL)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return gpk_parse_fileinfo(card, apdu.resp, apdu.resplen, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
gpk_select_id(struct sc_card *card, u_int8_t kind, u_int16_t fid,
|
||||||
|
struct sc_file *file)
|
||||||
|
{
|
||||||
|
struct sc_path *cp = &card->cache.current_path;
|
||||||
|
u_int8_t fbuf[2];
|
||||||
|
int r;
|
||||||
|
|
||||||
|
fbuf[0] = fid >> 8;
|
||||||
|
fbuf[1] = fid & 0xff;
|
||||||
|
r = gpk_select(card, kind, fbuf, 2, file);
|
||||||
|
|
||||||
|
/* Fix up the path cache */
|
||||||
|
if (r == 0) {
|
||||||
|
u_int16_t *path = (u_int16_t *) cp->value;
|
||||||
|
|
||||||
|
if (fid == GPK_FID_MF) {
|
||||||
|
path[0] = fid;
|
||||||
|
cp->len = 1;
|
||||||
|
} else
|
||||||
|
if (cp->len + 1 <= SC_MAX_PATH_SIZE / 2) {
|
||||||
|
path[cp->len++] = fid;
|
||||||
|
} else {
|
||||||
|
cp->len = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cp->len = 0;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
gpk_select_file(struct sc_card *card, const struct sc_path *path,
|
||||||
|
struct sc_file *file)
|
||||||
|
{
|
||||||
|
u_int16_t pathtmp[SC_MAX_PATH_SIZE/2];
|
||||||
|
u_int16_t *pathptr;
|
||||||
|
size_t pathlen, n;
|
||||||
|
int locked = 0, r = 0, use_relative = 0;
|
||||||
|
u_int8_t leaf_type;
|
||||||
|
|
||||||
|
SC_FUNC_CALLED(card->ctx, 3);
|
||||||
|
|
||||||
|
/* Handle the AID case first */
|
||||||
|
if (path->type == SC_PATH_TYPE_DF_NAME) {
|
||||||
|
if (path->len > 16)
|
||||||
|
return SC_ERROR_INVALID_ARGUMENTS;
|
||||||
|
r = gpk_select(card, GPK_SEL_AID,
|
||||||
|
path->value, path->len, file);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Now we know we're dealing with 16bit FIDs, either as
|
||||||
|
* an absolute path name (SC_PATH_TYPE_PATH) or a relative
|
||||||
|
* FID (SC_PATH_TYPE_FILE_ID)
|
||||||
|
*
|
||||||
|
* The API should really tell us whether this is a DF or EF
|
||||||
|
* we're selecting. All we can do is read tea leaves...
|
||||||
|
*/
|
||||||
|
leaf_type = GPK_SEL_EF;
|
||||||
|
|
||||||
|
try_again:
|
||||||
|
if ((path->len & 1) || path->len > sizeof(pathtmp))
|
||||||
|
return SC_ERROR_INVALID_ARGUMENTS;
|
||||||
|
pathptr = pathtmp;
|
||||||
|
for (n = 0; n < path->len; n += 2)
|
||||||
|
pathptr[n>>1] = (path->value[n] << 8)|path->value[n+1];
|
||||||
|
pathlen = path->len >> 1;
|
||||||
|
|
||||||
|
/* See whether we can skip an initial portion of the
|
||||||
|
* (absolute) path */
|
||||||
|
if (path->type == SC_PATH_TYPE_PATH) {
|
||||||
|
use_relative = match_path(card, &pathptr, &pathlen, file != 0);
|
||||||
|
if (pathlen == 0)
|
||||||
|
goto done;
|
||||||
|
} else {
|
||||||
|
/* SC_PATH_TYPE_FILEID */
|
||||||
|
if (pathlen > 1)
|
||||||
|
return SC_ERROR_INVALID_ARGUMENTS;
|
||||||
|
use_relative = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathlen == 1 && pathptr[0] == GPK_FID_MF) {
|
||||||
|
/* Select just the MF */
|
||||||
|
leaf_type = GPK_SEL_MF;
|
||||||
|
} else {
|
||||||
|
if (!locked++) {
|
||||||
|
r = sc_lock(card);
|
||||||
|
SC_TEST_RET(card->ctx, r, "sc_lock() failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Do we need to select the MF first? */
|
||||||
|
if (!use_relative) {
|
||||||
|
r = gpk_select_id(card, GPK_SEL_MF, GPK_FID_MF, NULL);
|
||||||
|
if (r)
|
||||||
|
sc_unlock(card);
|
||||||
|
SC_TEST_RET(card->ctx, r, "Unable to select MF");
|
||||||
|
|
||||||
|
/* Consume the MF FID if it's there */
|
||||||
|
if (pathptr[0] == GPK_FID_MF) {
|
||||||
|
pathptr++;
|
||||||
|
pathlen--;
|
||||||
|
}
|
||||||
|
if (pathlen == 0)
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Next comes a DF, if at all.
|
||||||
|
* This loop can deal with nesting levels > 1 even
|
||||||
|
* though the GPK4000 doesn't support it. */
|
||||||
|
while (pathlen > 1) {
|
||||||
|
r = gpk_select_id(card, GPK_SEL_DF, pathptr[0], NULL);
|
||||||
|
if (r)
|
||||||
|
sc_unlock(card);
|
||||||
|
SC_TEST_RET(card->ctx, r, "Unable to select DF");
|
||||||
|
pathptr++;
|
||||||
|
pathlen--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remaining component will be a DF or EF. How do we find out?
|
||||||
|
* All we can do is try */
|
||||||
|
r = gpk_select_id(card, leaf_type, pathptr[0], file);
|
||||||
|
if (r) {
|
||||||
|
/* Did we guess EF, and were wrong? If so, invalidate
|
||||||
|
* path cache and try again; this time aiming for a DF */
|
||||||
|
if (leaf_type == GPK_SEL_EF) {
|
||||||
|
card->cache.current_path.len = 0;
|
||||||
|
leaf_type = GPK_SEL_DF;
|
||||||
|
goto try_again;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (locked)
|
||||||
|
sc_unlock(card);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Secure messaging
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
gpk_compute_crycks(struct sc_card *card, struct sc_apdu *apdu,
|
||||||
|
u_int8_t *crycks1)
|
||||||
|
{
|
||||||
|
struct gpk_private_data *priv = OPSDATA(card);
|
||||||
|
des_key_schedule k1, k2;
|
||||||
|
u_int8_t in[8], out[8], block[64];
|
||||||
|
unsigned int len = 0, i, j;
|
||||||
|
|
||||||
|
/* Set the key schedule */
|
||||||
|
des_set_key_unchecked((des_cblock *) priv->key, k1);
|
||||||
|
des_set_key_unchecked((des_cblock *) (priv->key+8), k2);
|
||||||
|
|
||||||
|
/* Fill block with 0x00 and then with the data. */
|
||||||
|
memset(block, 0x00, sizeof(block));
|
||||||
|
block[len++] = apdu->cla;
|
||||||
|
block[len++] = apdu->ins;
|
||||||
|
block[len++] = apdu->p1;
|
||||||
|
block[len++] = apdu->p2;
|
||||||
|
block[len++] = apdu->lc + 3;
|
||||||
|
if ((i = apdu->datalen) + len > sizeof(block))
|
||||||
|
i = sizeof(block) - len;
|
||||||
|
memcpy(block+len, apdu->data, i);
|
||||||
|
len += i;
|
||||||
|
|
||||||
|
/* Set IV */
|
||||||
|
memset(in, 0x00, 8);
|
||||||
|
|
||||||
|
for (j = 0; j < len; ) {
|
||||||
|
for (i = 0; i < 8; i++, j++)
|
||||||
|
in[i] ^= block[j];
|
||||||
|
des_ecb3_encrypt((des_cblock *)in,
|
||||||
|
(des_cblock *)out,
|
||||||
|
k1, k2, k1, DES_ENCRYPT);
|
||||||
|
memcpy(in, out, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy((u_int8_t *) (apdu->data + apdu->datalen), out + 5, 3);
|
||||||
|
apdu->datalen += 3;
|
||||||
|
apdu->lc += 3;
|
||||||
|
apdu->le = 3;
|
||||||
|
if (crycks1)
|
||||||
|
memcpy(crycks1, out, 3);
|
||||||
|
memset(k1, 0, sizeof(k1));
|
||||||
|
memset(k2, 0, sizeof(k2));
|
||||||
|
memset(in, 0, sizeof(in));
|
||||||
|
memset(out, 0, sizeof(out));
|
||||||
|
memset(block, 0, sizeof(block));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a file or directory.
|
||||||
|
* This is a bit tricky because we abuse the ef_structure
|
||||||
|
* field to transport file types that are non-standard
|
||||||
|
* (the GPK4000 has lots of bizarre file types).
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
gpk_create_file(struct sc_card *card, struct sc_file *file)
|
||||||
|
{
|
||||||
|
struct gpk_private_data *priv = OPSDATA(card);
|
||||||
|
struct sc_apdu apdu;
|
||||||
|
u_int8_t data[28+3], crycks[3], resp[3];
|
||||||
|
size_t datalen, namelen;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
/* Prepare APDU */
|
||||||
|
memset(&apdu, 0, sizeof(apdu));
|
||||||
|
apdu.cla = 0x80; /* assume no secure messaging */
|
||||||
|
apdu.cse = SC_APDU_CASE_3_SHORT;
|
||||||
|
apdu.ins = 0xE0;
|
||||||
|
apdu.p2 = 0x00;
|
||||||
|
|
||||||
|
/* clear data */
|
||||||
|
memset(data, 0, sizeof(data));
|
||||||
|
datalen = 12;
|
||||||
|
|
||||||
|
/* FID */
|
||||||
|
data[0] = file->id >> 8;
|
||||||
|
data[1] = file->id & 0xFF;
|
||||||
|
|
||||||
|
/* encode ACLs */
|
||||||
|
if (file->type == SC_FILE_TYPE_DF) {
|
||||||
|
/* The GPK4000 has separate AC bits for
|
||||||
|
* creating sensitive files and creating
|
||||||
|
* data files. Since OpenSC has just the notion
|
||||||
|
* of "file" we use the same ACL for both AC words
|
||||||
|
*/
|
||||||
|
apdu.p1 = 0x01; /* create DF */
|
||||||
|
data[2] = 0x38;
|
||||||
|
acl_to_ac(file->acl[SC_AC_OP_CREATE], data + 6);
|
||||||
|
acl_to_ac(file->acl[SC_AC_OP_CREATE], data + 8);
|
||||||
|
if ((namelen = file->namelen) != 0) {
|
||||||
|
if (namelen > 16)
|
||||||
|
return SC_ERROR_INVALID_ARGUMENTS;
|
||||||
|
memcpy(data+datalen, file->name, namelen);
|
||||||
|
data[5] = namelen;
|
||||||
|
datalen += namelen;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apdu.p1 = 0x02; /* create EF */
|
||||||
|
data[2] = file->ef_structure;
|
||||||
|
data[3] = file->record_length;
|
||||||
|
data[4] = file->size >> 8;
|
||||||
|
data[5] = file->size & 0xff;
|
||||||
|
acl_to_ac(file->acl[SC_AC_OP_UPDATE], data + 6);
|
||||||
|
acl_to_ac(file->acl[SC_AC_OP_WRITE], data + 8);
|
||||||
|
acl_to_ac(file->acl[SC_AC_OP_READ], data + 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
apdu.data = data;
|
||||||
|
apdu.datalen = datalen;
|
||||||
|
apdu.lc = datalen;
|
||||||
|
|
||||||
|
if (priv->key_set) {
|
||||||
|
apdu.cla = 0x84;
|
||||||
|
apdu.cse = SC_APDU_CASE_4_SHORT;
|
||||||
|
r = gpk_compute_crycks(card, &apdu, crycks);
|
||||||
|
if (r)
|
||||||
|
return r;
|
||||||
|
apdu.resp = resp;
|
||||||
|
apdu.resplen = sizeof(resp); /* XXX? */
|
||||||
|
}
|
||||||
|
|
||||||
|
r = sc_transmit_apdu(card, &apdu);
|
||||||
|
SC_TEST_RET(card->ctx, r, "APDU transmit failed");
|
||||||
|
r = gpk_sw_to_errorcode(card, apdu.sw1, apdu.sw2);
|
||||||
|
SC_TEST_RET(card->ctx, r, "Card returned error");
|
||||||
|
|
||||||
|
if (priv->key_set) {
|
||||||
|
/* verify CRYCKS response? */
|
||||||
|
if (apdu.resplen != 3
|
||||||
|
|| memcmp(resp, crycks, 3)) {
|
||||||
|
printf("XXX Secure messaging: bad resp\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set the secure messaging key following a Select FileKey
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
gpk_set_filekey(const u_int8_t *key, const u_int8_t *challenge,
|
||||||
|
const u_int8_t *r_rn, u_int8_t *kats)
|
||||||
|
{
|
||||||
|
des_key_schedule k1, k2;
|
||||||
|
des_cblock out;
|
||||||
|
int r = 0;
|
||||||
|
|
||||||
|
des_set_key_unchecked((des_cblock *) key, k1);
|
||||||
|
des_set_key_unchecked((des_cblock *) (key+8), k2);
|
||||||
|
|
||||||
|
des_ecb3_encrypt((des_cblock *)(r_rn+4), (des_cblock *) kats,
|
||||||
|
k1, k2, k1, DES_ENCRYPT);
|
||||||
|
des_ecb3_encrypt((des_cblock *)(r_rn+4), (des_cblock *) (kats+8),
|
||||||
|
k2, k1, k2, DES_ENCRYPT);
|
||||||
|
|
||||||
|
/* Verify Cryptogram presented by the card terminal
|
||||||
|
* XXX: what is the appropriate error code to return
|
||||||
|
* here? INVALID_ARGS doesn't seem quite right
|
||||||
|
*/
|
||||||
|
des_set_key_unchecked((des_cblock *) kats, k1);
|
||||||
|
des_set_key_unchecked((des_cblock *) (kats+8), k2);
|
||||||
|
|
||||||
|
des_ecb3_encrypt((des_cblock *) challenge, &out,
|
||||||
|
k1, k2, k1, DES_ENCRYPT );
|
||||||
|
if (memcmp(r_rn, out+4, 4) != 0)
|
||||||
|
r = SC_ERROR_INVALID_ARGUMENTS;
|
||||||
|
|
||||||
|
memset(k1, 0, sizeof(k1));
|
||||||
|
memset(k2, 0, sizeof(k2));
|
||||||
|
memset(out, 0, sizeof(out));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Verify a key presented by the user for secure messaging
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
gpk_select_key(struct sc_card *card, int ref, const u8 *buf, size_t buflen)
|
||||||
|
{
|
||||||
|
struct gpk_private_data *priv = OPSDATA(card);
|
||||||
|
struct sc_apdu apdu;
|
||||||
|
u_int8_t random[8], resp[258];
|
||||||
|
unsigned int n, sfi, key_sfi = 0;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
if (buflen != 16)
|
||||||
|
return SC_ERROR_INVALID_ARGUMENTS;
|
||||||
|
|
||||||
|
/* The opensc API doesn't tell us what key it wants to
|
||||||
|
* select, and why. We need to look at the ACs of
|
||||||
|
* the most recently selected file and guess
|
||||||
|
*/
|
||||||
|
key_sfi = 0;
|
||||||
|
for (n = 0; n < 3; n++) {
|
||||||
|
sfi = (priv->ac[n] >> 8) & 0x3F;
|
||||||
|
if (sfi & 0xF) {
|
||||||
|
if (key_sfi && key_sfi != sfi) {
|
||||||
|
/* Hm, the file has ACLs with two
|
||||||
|
* different keys. I'm unable to guess
|
||||||
|
* which one I should use, so I throw
|
||||||
|
* up my hands in disgust.
|
||||||
|
*/
|
||||||
|
/* XXX fix errror code? */
|
||||||
|
return SC_ERROR_INVALID_ARGUMENTS;
|
||||||
|
}
|
||||||
|
key_sfi = sfi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If no key required, assume transport key :-/ */
|
||||||
|
if (key_sfi == 0)
|
||||||
|
key_sfi = 0x01;
|
||||||
|
|
||||||
|
/* XXX now do the SelFk */
|
||||||
|
RAND_pseudo_bytes(random, sizeof(random));
|
||||||
|
memset(&apdu, 0, sizeof(apdu));
|
||||||
|
apdu.cla = 0x80;
|
||||||
|
apdu.cse = SC_APDU_CASE_4_SHORT;
|
||||||
|
apdu.ins = 0x28;
|
||||||
|
apdu.p1 = ref << 1;
|
||||||
|
apdu.p2 = key_sfi;
|
||||||
|
apdu.data = random;
|
||||||
|
apdu.datalen = sizeof(random);
|
||||||
|
apdu.lc = apdu.datalen;
|
||||||
|
apdu.resp = resp;
|
||||||
|
apdu.resplen = sizeof(resp);
|
||||||
|
|
||||||
|
r = sc_transmit_apdu(card, &apdu);
|
||||||
|
SC_TEST_RET(card->ctx, r, "APDU transmit failed");
|
||||||
|
r = gpk_sw_to_errorcode(card, apdu.sw1, apdu.sw2);
|
||||||
|
SC_TEST_RET(card->ctx, r, "Card returned error");
|
||||||
|
|
||||||
|
if (apdu.resplen != 12) {
|
||||||
|
r = SC_ERROR_UNKNOWN_REPLY;
|
||||||
|
} else
|
||||||
|
if ((r = gpk_set_filekey(buf, random, resp, priv->key)) == 0) {
|
||||||
|
priv->key_set = 1;
|
||||||
|
priv->key_local = (key_sfi & 0x20)? 1 : 0;
|
||||||
|
priv->key_sfi = key_sfi & 0x1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(resp, 0, sizeof(resp));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Verify key (for external auth/secure messaging) or PIN
|
||||||
|
* presented by the user
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
gpk_verify(struct sc_card *card, unsigned int type, int ref,
|
||||||
|
const u8 *buf, size_t buflen, int *tries_left)
|
||||||
|
{
|
||||||
|
if (tries_left)
|
||||||
|
*tries_left = -1;
|
||||||
|
switch (type) {
|
||||||
|
case SC_AC_PRO:
|
||||||
|
return gpk_select_key(card, ref, buf, buflen);
|
||||||
|
}
|
||||||
|
return SC_ERROR_INVALID_ARGUMENTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialize the driver struct
|
||||||
|
*/
|
||||||
|
static const struct sc_card_driver *
|
||||||
|
sc_get_driver()
|
||||||
|
{
|
||||||
|
if (gpk_ops.match_card == NULL) {
|
||||||
|
const struct sc_card_driver *iso_drv;
|
||||||
|
|
||||||
|
iso_drv = sc_get_iso7816_driver();
|
||||||
|
gpk_ops = *iso_drv->ops;
|
||||||
|
|
||||||
|
gpk_ops.match_card = gpk_match;
|
||||||
|
gpk_ops.init = gpk_init;
|
||||||
|
gpk_ops.finish = gpk_finish;
|
||||||
|
gpk_ops.select_file = gpk_select_file;
|
||||||
|
/* The GPK4000 doesn't have a read directory command. */
|
||||||
|
//gpk_ops.list_files = gpk_list_files;
|
||||||
|
gpk_ops.verify = gpk_verify;
|
||||||
|
gpk_ops.create_file = gpk_create_file;
|
||||||
|
}
|
||||||
|
return &gpk_drv;
|
||||||
|
}
|
||||||
|
|
||||||
|
const struct sc_card_driver *
|
||||||
|
sc_get_gpk_driver()
|
||||||
|
{
|
||||||
|
return sc_get_driver();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* ifdef OPENSSL */
|
|
@ -57,7 +57,7 @@ static int iso7816_read_record(struct sc_card *card,
|
||||||
int r;
|
int r;
|
||||||
|
|
||||||
sc_format_apdu(card, &apdu, SC_APDU_CASE_2_SHORT, 0xB2, rec_nr, 0);
|
sc_format_apdu(card, &apdu, SC_APDU_CASE_2_SHORT, 0xB2, rec_nr, 0);
|
||||||
apdu.p2 = (rec_nr & SC_READ_RECORD_EF_ID_MASK) << 3;
|
apdu.p2 = (flags & SC_READ_RECORD_EF_ID_MASK) << 3;
|
||||||
if (flags & SC_READ_RECORD_BY_REC_NR)
|
if (flags & SC_READ_RECORD_BY_REC_NR)
|
||||||
apdu.p2 |= 0x04;
|
apdu.p2 |= 0x04;
|
||||||
|
|
||||||
|
@ -663,6 +663,86 @@ static int iso7816_compute_signature(struct sc_card *card,
|
||||||
SC_FUNC_RETURN(card->ctx, 4, sc_sw_to_errorcode(card, apdu.sw1, apdu.sw2));
|
SC_FUNC_RETURN(card->ctx, 4, sc_sw_to_errorcode(card, apdu.sw1, apdu.sw2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int iso7816_change_reference_data(struct sc_card *card, unsigned int type,
|
||||||
|
int ref, const u8 *old, size_t oldlen,
|
||||||
|
const u8 *new, size_t newlen,
|
||||||
|
int *tries_left)
|
||||||
|
{
|
||||||
|
struct sc_apdu apdu;
|
||||||
|
u8 sbuf[SC_MAX_APDU_BUFFER_SIZE];
|
||||||
|
int r, p1 = 0, len = oldlen + newlen;
|
||||||
|
|
||||||
|
if (len >= SC_MAX_APDU_BUFFER_SIZE)
|
||||||
|
SC_FUNC_RETURN(card->ctx, 1, SC_ERROR_INVALID_ARGUMENTS);
|
||||||
|
switch (type) {
|
||||||
|
case SC_AC_CHV1:
|
||||||
|
case SC_AC_CHV2:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return SC_ERROR_INVALID_ARGUMENTS;
|
||||||
|
}
|
||||||
|
if (oldlen == 0)
|
||||||
|
p1 = 1;
|
||||||
|
sc_format_apdu(card, &apdu, SC_APDU_CASE_3_SHORT, 0x24, p1, ref);
|
||||||
|
memcpy(sbuf, old, oldlen);
|
||||||
|
memcpy(sbuf + oldlen, new, newlen);
|
||||||
|
apdu.lc = len;
|
||||||
|
apdu.datalen = len;
|
||||||
|
apdu.data = sbuf;
|
||||||
|
apdu.resplen = 0;
|
||||||
|
|
||||||
|
r = sc_transmit_apdu(card, &apdu);
|
||||||
|
memset(sbuf, 0, len);
|
||||||
|
SC_TEST_RET(card->ctx, r, "APDU transmit failed");
|
||||||
|
if (apdu.sw1 == 0x63 && (apdu.sw2 & 0xF0) == 0xC0) {
|
||||||
|
if (tries_left != NULL)
|
||||||
|
*tries_left = apdu.sw2 & 0x0F;
|
||||||
|
SC_FUNC_RETURN(card->ctx, 1, SC_ERROR_PIN_CODE_INCORRECT);
|
||||||
|
}
|
||||||
|
return sc_sw_to_errorcode(card, apdu.sw1, apdu.sw2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int iso7816_reset_retry_counter(struct sc_card *card, unsigned int type, int ref,
|
||||||
|
const u8 *puk, size_t puklen, const u8 *new,
|
||||||
|
size_t newlen)
|
||||||
|
{
|
||||||
|
struct sc_apdu apdu;
|
||||||
|
u8 sbuf[MAX_BUFFER_SIZE];
|
||||||
|
int r, p1 = 0, len = puklen + newlen;
|
||||||
|
|
||||||
|
if (len >= MAX_BUFFER_SIZE)
|
||||||
|
SC_FUNC_RETURN(card->ctx, 1, SC_ERROR_INVALID_ARGUMENTS);
|
||||||
|
switch (type) {
|
||||||
|
case SC_AC_CHV1:
|
||||||
|
case SC_AC_CHV2:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return SC_ERROR_INVALID_ARGUMENTS;
|
||||||
|
}
|
||||||
|
if (puklen == 0) {
|
||||||
|
if (newlen == 0)
|
||||||
|
p1 = 3;
|
||||||
|
else
|
||||||
|
p1 = 2;
|
||||||
|
} else {
|
||||||
|
if (newlen == 0)
|
||||||
|
p1 = 1;
|
||||||
|
else
|
||||||
|
p1 = 0;
|
||||||
|
}
|
||||||
|
sc_format_apdu(card, &apdu, SC_APDU_CASE_3_SHORT, 0x2C, p1, ref);
|
||||||
|
memcpy(sbuf, puk, puklen);
|
||||||
|
memcpy(sbuf + puklen, new, newlen);
|
||||||
|
apdu.lc = len;
|
||||||
|
apdu.datalen = len;
|
||||||
|
apdu.data = sbuf;
|
||||||
|
apdu.resplen = 0;
|
||||||
|
|
||||||
|
r = sc_transmit_apdu(card, &apdu);
|
||||||
|
memset(sbuf, 0, len);
|
||||||
|
SC_TEST_RET(card->ctx, r, "APDU transmit failed");
|
||||||
|
return sc_sw_to_errorcode(card, apdu.sw1, apdu.sw2);
|
||||||
|
}
|
||||||
|
|
||||||
static struct sc_card_operations iso_ops = {
|
static struct sc_card_operations iso_ops = {
|
||||||
NULL,
|
NULL,
|
||||||
|
@ -698,6 +778,8 @@ const struct sc_card_driver * sc_get_iso7816_driver(void)
|
||||||
iso_ops.set_security_env = iso7816_set_security_env;
|
iso_ops.set_security_env = iso7816_set_security_env;
|
||||||
iso_ops.restore_security_env = iso7816_restore_security_env;
|
iso_ops.restore_security_env = iso7816_restore_security_env;
|
||||||
iso_ops.compute_signature = iso7816_compute_signature;
|
iso_ops.compute_signature = iso7816_compute_signature;
|
||||||
|
iso_ops.reset_retry_counter = iso7816_reset_retry_counter;
|
||||||
|
iso_ops.change_reference_data = iso7816_change_reference_data;
|
||||||
}
|
}
|
||||||
return &iso_driver;
|
return &iso_driver;
|
||||||
}
|
}
|
||||||
|
|
|
@ -347,11 +347,13 @@ struct sc_card_operations {
|
||||||
* to the function decipher. */
|
* to the function decipher. */
|
||||||
int (*compute_signature)(struct sc_card *card, const u8 * data,
|
int (*compute_signature)(struct sc_card *card, const u8 * data,
|
||||||
size_t data_len, u8 * out, size_t outlen);
|
size_t data_len, u8 * out, size_t outlen);
|
||||||
int (*change_reference_data)(struct sc_card *card, int ref_qualifier,
|
int (*change_reference_data)(struct sc_card *card, unsigned int type,
|
||||||
|
int ref_qualifier,
|
||||||
const u8 *old, size_t oldlen,
|
const u8 *old, size_t oldlen,
|
||||||
const u8 *newref, size_t newlen,
|
const u8 *newref, size_t newlen,
|
||||||
int *tries_left);
|
int *tries_left);
|
||||||
int (*reset_retry_counter)(struct sc_card *card, int ref_qualifier,
|
int (*reset_retry_counter)(struct sc_card *card, unsigned int type,
|
||||||
|
int ref_qualifier,
|
||||||
const u8 *puk, size_t puklen,
|
const u8 *puk, size_t puklen,
|
||||||
const u8 *newref, size_t newlen);
|
const u8 *newref, size_t newlen);
|
||||||
/*
|
/*
|
||||||
|
@ -517,11 +519,13 @@ int sc_compute_signature(struct sc_card *card, const u8 * data,
|
||||||
size_t data_len, u8 * out, size_t outlen);
|
size_t data_len, u8 * out, size_t outlen);
|
||||||
int sc_verify(struct sc_card *card, unsigned int type, int ref, const u8 *buf,
|
int sc_verify(struct sc_card *card, unsigned int type, int ref, const u8 *buf,
|
||||||
size_t buflen, int *tries_left);
|
size_t buflen, int *tries_left);
|
||||||
int sc_change_reference_data(struct sc_card *card, int ref, const u8 *old,
|
int sc_change_reference_data(struct sc_card *card, unsigned int type,
|
||||||
size_t oldlen, const u8 *newref, size_t newlen,
|
int ref, const u8 *old, size_t oldlen,
|
||||||
|
const u8 *newref, size_t newlen,
|
||||||
int *tries_left);
|
int *tries_left);
|
||||||
int sc_reset_retry_counter(struct sc_card *card, int ref, const u8 *puk,
|
int sc_reset_retry_counter(struct sc_card *card, unsigned int type,
|
||||||
size_t puklen, const u8 *newref, size_t newlen);
|
int ref, const u8 *puk, size_t puklen,
|
||||||
|
const u8 *newref, size_t newlen);
|
||||||
|
|
||||||
/* ISO 7816-9 */
|
/* ISO 7816-9 */
|
||||||
int sc_create_file(struct sc_card *card, struct sc_file *file);
|
int sc_create_file(struct sc_card *card, struct sc_file *file);
|
||||||
|
@ -545,6 +549,7 @@ extern const struct sc_card_driver *sc_get_iso7816_driver(void);
|
||||||
extern const struct sc_card_driver *sc_get_emv_driver(void);
|
extern const struct sc_card_driver *sc_get_emv_driver(void);
|
||||||
extern const struct sc_card_driver *sc_get_setec_driver(void);
|
extern const struct sc_card_driver *sc_get_setec_driver(void);
|
||||||
extern const struct sc_card_driver *sc_get_flex_driver(void);
|
extern const struct sc_card_driver *sc_get_flex_driver(void);
|
||||||
|
extern const struct sc_card_driver *sc_get_gpk_driver(void);
|
||||||
extern const struct sc_card_driver *sc_get_default_driver(void);
|
extern const struct sc_card_driver *sc_get_default_driver(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|
|
@ -288,7 +288,7 @@ int sc_pkcs15_change_pin(struct sc_pkcs15_card *p15card,
|
||||||
memset(pinbuf, pin->pad_char, pin->stored_length * 2);
|
memset(pinbuf, pin->pad_char, pin->stored_length * 2);
|
||||||
memcpy(pinbuf, oldpin, oldpinlen);
|
memcpy(pinbuf, oldpin, oldpinlen);
|
||||||
memcpy(pinbuf + pin->stored_length, newpin, newpinlen);
|
memcpy(pinbuf + pin->stored_length, newpin, newpinlen);
|
||||||
r = sc_change_reference_data(card, pin->auth_id.value[0], pinbuf,
|
r = sc_change_reference_data(card, SC_AC_CHV1, pin->auth_id.value[0], pinbuf,
|
||||||
pin->stored_length, pinbuf+pin->stored_length,
|
pin->stored_length, pinbuf+pin->stored_length,
|
||||||
pin->stored_length, &pin->tries_left);
|
pin->stored_length, &pin->tries_left);
|
||||||
memset(pinbuf, 0, pin->stored_length * 2);
|
memset(pinbuf, 0, pin->stored_length * 2);
|
||||||
|
|
|
@ -182,12 +182,12 @@ int sc_establish_context(struct sc_context **ctx_out)
|
||||||
#if 1
|
#if 1
|
||||||
ctx->card_drivers[i++] = sc_get_flex_driver();
|
ctx->card_drivers[i++] = sc_get_flex_driver();
|
||||||
#endif
|
#endif
|
||||||
#if 1
|
|
||||||
ctx->card_drivers[i++] = sc_get_iso7816_driver();
|
|
||||||
#endif
|
|
||||||
#if 1
|
#if 1
|
||||||
ctx->card_drivers[i++] = sc_get_emv_driver();
|
ctx->card_drivers[i++] = sc_get_emv_driver();
|
||||||
#endif
|
#endif
|
||||||
|
#if 1 && defined(HAVE_OPENSSL)
|
||||||
|
ctx->card_drivers[i++] = sc_get_gpk_driver();
|
||||||
|
#endif
|
||||||
#if 1
|
#if 1
|
||||||
/* this should be last in line */
|
/* this should be last in line */
|
||||||
ctx->card_drivers[i++] = sc_get_default_driver();
|
ctx->card_drivers[i++] = sc_get_default_driver();
|
||||||
|
|
|
@ -113,68 +113,33 @@ int sc_verify(struct sc_card *card, unsigned int type, int ref,
|
||||||
SC_FUNC_RETURN(card->ctx, 2, r);
|
SC_FUNC_RETURN(card->ctx, 2, r);
|
||||||
}
|
}
|
||||||
|
|
||||||
int sc_change_reference_data(struct sc_card *card, int ref, const u8 *old,
|
int sc_change_reference_data(struct sc_card *card, unsigned int type,
|
||||||
size_t oldlen, const u8 *new, size_t newlen,
|
int ref, const u8 *old, size_t oldlen,
|
||||||
|
const u8 *newref, size_t newlen,
|
||||||
int *tries_left)
|
int *tries_left)
|
||||||
{
|
{
|
||||||
struct sc_apdu apdu;
|
int r;
|
||||||
u8 sbuf[MAX_BUFFER_SIZE];
|
|
||||||
int r, p1 = 0, len = oldlen + newlen;
|
|
||||||
|
|
||||||
|
assert(card != NULL);
|
||||||
SC_FUNC_CALLED(card->ctx, 1);
|
SC_FUNC_CALLED(card->ctx, 1);
|
||||||
if (len >= MAX_BUFFER_SIZE)
|
if (card->ops->change_reference_data == NULL)
|
||||||
SC_FUNC_RETURN(card->ctx, 1, SC_ERROR_INVALID_ARGUMENTS);
|
SC_FUNC_RETURN(card->ctx, 2, SC_ERROR_NOT_SUPPORTED);
|
||||||
if (oldlen == 0)
|
r = card->ops->change_reference_data(card, type, ref, old, oldlen,
|
||||||
p1 = 1;
|
newref, newlen, tries_left);
|
||||||
sc_format_apdu(card, &apdu, SC_APDU_CASE_3_SHORT, 0x24, p1, ref);
|
SC_FUNC_RETURN(card->ctx, 1, r);
|
||||||
memcpy(sbuf, old, oldlen);
|
|
||||||
memcpy(sbuf + oldlen, new, newlen);
|
|
||||||
apdu.lc = len;
|
|
||||||
apdu.datalen = len;
|
|
||||||
apdu.data = sbuf;
|
|
||||||
apdu.resplen = 0;
|
|
||||||
|
|
||||||
r = sc_transmit_apdu(card, &apdu);
|
|
||||||
memset(sbuf, 0, len);
|
|
||||||
SC_TEST_RET(card->ctx, r, "APDU transmit failed");
|
|
||||||
if (apdu.sw1 == 0x63 && (apdu.sw2 & 0xF0) == 0xC0) {
|
|
||||||
if (tries_left != NULL)
|
|
||||||
*tries_left = apdu.sw2 & 0x0F;
|
|
||||||
SC_FUNC_RETURN(card->ctx, 1, SC_ERROR_PIN_CODE_INCORRECT);
|
|
||||||
}
|
|
||||||
SC_FUNC_RETURN(card->ctx, 1, sc_sw_to_errorcode(card, apdu.sw1, apdu.sw2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int sc_reset_retry_counter(struct sc_card *card, int ref, const u8 *puk,
|
int sc_reset_retry_counter(struct sc_card *card, unsigned int type, int ref,
|
||||||
size_t puklen, const u8 *new, size_t newlen)
|
const u8 *puk, size_t puklen, const u8 *newref,
|
||||||
|
size_t newlen)
|
||||||
{
|
{
|
||||||
struct sc_apdu apdu;
|
int r;
|
||||||
u8 sbuf[MAX_BUFFER_SIZE];
|
|
||||||
int r, p1 = 0, len = puklen + newlen;
|
|
||||||
|
|
||||||
if (len >= MAX_BUFFER_SIZE)
|
assert(card != NULL);
|
||||||
SC_FUNC_RETURN(card->ctx, 1, SC_ERROR_INVALID_ARGUMENTS);
|
SC_FUNC_CALLED(card->ctx, 1);
|
||||||
if (puklen == 0) {
|
if (card->ops->reset_retry_counter == NULL)
|
||||||
if (newlen == 0)
|
SC_FUNC_RETURN(card->ctx, 2, SC_ERROR_NOT_SUPPORTED);
|
||||||
p1 = 3;
|
r = card->ops->reset_retry_counter(card, type, ref, puk, puklen,
|
||||||
else
|
newref, newlen);
|
||||||
p1 = 2;
|
SC_FUNC_RETURN(card->ctx, 1, r);
|
||||||
} else {
|
|
||||||
if (newlen == 0)
|
|
||||||
p1 = 1;
|
|
||||||
else
|
|
||||||
p1 = 0;
|
|
||||||
}
|
|
||||||
sc_format_apdu(card, &apdu, SC_APDU_CASE_3_SHORT, 0x2C, p1, ref);
|
|
||||||
memcpy(sbuf, puk, puklen);
|
|
||||||
memcpy(sbuf + puklen, new, newlen);
|
|
||||||
apdu.lc = len;
|
|
||||||
apdu.datalen = len;
|
|
||||||
apdu.data = sbuf;
|
|
||||||
apdu.resplen = 0;
|
|
||||||
|
|
||||||
r = sc_transmit_apdu(card, &apdu);
|
|
||||||
memset(sbuf, 0, len);
|
|
||||||
SC_TEST_RET(card->ctx, r, "APDU transmit failed");
|
|
||||||
SC_FUNC_RETURN(card->ctx, 1, sc_sw_to_errorcode(card, apdu.sw1, apdu.sw2));
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ endif
|
||||||
opensc_tool_SOURCES = opensc-tool.c util.c
|
opensc_tool_SOURCES = opensc-tool.c util.c
|
||||||
opensc_tool_LDADD = @GETOPTSRC@
|
opensc_tool_LDADD = @GETOPTSRC@
|
||||||
opensc_explorer_SOURCES = opensc-explorer.c util.c
|
opensc_explorer_SOURCES = opensc-explorer.c util.c
|
||||||
|
## FIXME: conditionally add -lreadline
|
||||||
opensc_explorer_LDADD = @GETOPTSRC@
|
opensc_explorer_LDADD = @GETOPTSRC@
|
||||||
pkcs15_tool_SOURCES = pkcs15-tool.c util.c
|
pkcs15_tool_SOURCES = pkcs15-tool.c util.c
|
||||||
pkcs15_tool_LDADD = @GETOPTSRC@
|
pkcs15_tool_LDADD = @GETOPTSRC@
|
||||||
|
|
|
@ -22,6 +22,13 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#undef USE_READLINE
|
||||||
|
|
||||||
|
#ifdef USE_READLINE
|
||||||
|
#include <readline/readline.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
int opt_reader = 0;
|
int opt_reader = 0;
|
||||||
|
@ -171,6 +178,7 @@ int do_ls()
|
||||||
check_ret(r, SC_AC_OP_SELECT, "unable to select file", ¤t_file);
|
check_ret(r, SC_AC_OP_SELECT, "unable to select file", ¤t_file);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
file.id = (cur[0] << 8) | cur[1];
|
||||||
cur += 2;
|
cur += 2;
|
||||||
count -= 2;
|
count -= 2;
|
||||||
print_file(&file);
|
print_file(&file);
|
||||||
|
@ -229,12 +237,56 @@ int do_cd(const char *arg)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int do_cat(const char *arg)
|
int read_and_print_binary_file(struct sc_file *file)
|
||||||
|
{
|
||||||
|
unsigned int idx = 0;
|
||||||
|
u8 buf[128];
|
||||||
|
size_t count;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
count = file->size;
|
||||||
|
while (count) {
|
||||||
|
int c = count > sizeof(buf) ? sizeof(buf) : count;
|
||||||
|
|
||||||
|
r = sc_read_binary(card, idx, buf, c, 0);
|
||||||
|
if (r < 0) {
|
||||||
|
check_ret(r, SC_AC_OP_READ, "read failed", file);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (r != c) {
|
||||||
|
printf("expecting %d, got only %d bytes.\n", c, r);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
hex_dump_asc(stdout, buf, c, idx);
|
||||||
|
idx += c;
|
||||||
|
count -= c;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int read_and_print_record_file(struct sc_file *file)
|
||||||
{
|
{
|
||||||
u8 buf[256];
|
u8 buf[256];
|
||||||
|
int rec, r;
|
||||||
|
|
||||||
|
for (rec = 0; ; rec++) {
|
||||||
|
r = sc_read_record(card, rec, buf, sizeof(buf), SC_READ_RECORD_BY_REC_NR);
|
||||||
|
if (r == SC_ERROR_RECORD_NOT_FOUND)
|
||||||
|
return 0;
|
||||||
|
if (r < 0) {
|
||||||
|
check_ret(r, SC_AC_OP_READ, "read failed", file);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
printf("Record %d:\n", rec);
|
||||||
|
hex_dump_asc(stdout, buf, r, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int do_cat(const char *arg)
|
||||||
|
{
|
||||||
int r, error = 0;
|
int r, error = 0;
|
||||||
size_t count = 0;
|
|
||||||
unsigned int idx = 0;
|
|
||||||
struct sc_path path;
|
struct sc_path path;
|
||||||
struct sc_file file;
|
struct sc_file file;
|
||||||
int not_current = 1;
|
int not_current = 1;
|
||||||
|
@ -254,26 +306,10 @@ int do_cat(const char *arg)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
count = file.size;
|
if (file.ef_structure == SC_FILE_EF_TRANSPARENT)
|
||||||
while (count) {
|
read_and_print_binary_file(&file);
|
||||||
int c = count > sizeof(buf) ? sizeof(buf) : count;
|
else
|
||||||
|
read_and_print_record_file(&file);
|
||||||
r = sc_read_binary(card, idx, buf, c, 0);
|
|
||||||
if (r < 0) {
|
|
||||||
check_ret(r, SC_AC_OP_READ, "read failed", &file);
|
|
||||||
error = 1;
|
|
||||||
goto err;
|
|
||||||
}
|
|
||||||
if (r != c) {
|
|
||||||
printf("expecting %d, got only %d bytes.\n", c, r);
|
|
||||||
error = 1;
|
|
||||||
goto err;
|
|
||||||
}
|
|
||||||
hex_dump_asc(stdout, buf, c, idx);
|
|
||||||
idx += c;
|
|
||||||
count -= c;
|
|
||||||
}
|
|
||||||
err:
|
|
||||||
if (not_current) {
|
if (not_current) {
|
||||||
r = sc_select_file(card, ¤t_path, NULL);
|
r = sc_select_file(card, ¤t_path, NULL);
|
||||||
if (r) {
|
if (r) {
|
||||||
|
@ -478,7 +514,7 @@ usage:
|
||||||
int do_verify(const char *arg, const char *arg2)
|
int do_verify(const char *arg, const char *arg2)
|
||||||
{
|
{
|
||||||
const char *types[] = {
|
const char *types[] = {
|
||||||
"CHV", "KEY"
|
"CHV", "KEY", "PRO"
|
||||||
};
|
};
|
||||||
int i, type = -1, ref, r, tries_left = -1;
|
int i, type = -1, ref, r, tries_left = -1;
|
||||||
u8 buf[30];
|
u8 buf[30];
|
||||||
|
@ -486,7 +522,7 @@ int do_verify(const char *arg, const char *arg2)
|
||||||
|
|
||||||
if (strlen(arg) == 0 || strlen(arg2) == 0)
|
if (strlen(arg) == 0 || strlen(arg2) == 0)
|
||||||
goto usage;
|
goto usage;
|
||||||
for (i = 0; i < 2; i++)
|
for (i = 0; i < 3; i++)
|
||||||
if (strncasecmp(arg, types[i], 3) == 0) {
|
if (strncasecmp(arg, types[i], 3) == 0) {
|
||||||
type = i;
|
type = i;
|
||||||
break;
|
break;
|
||||||
|
@ -499,6 +535,11 @@ int do_verify(const char *arg, const char *arg2)
|
||||||
printf("Invalid key reference.\n");
|
printf("Invalid key reference.\n");
|
||||||
goto usage;
|
goto usage;
|
||||||
}
|
}
|
||||||
|
if (arg2[0] == '"') {
|
||||||
|
for (++arg2, i = 0; i < sizeof(buf) && arg2[i] != '"'; i++)
|
||||||
|
buf[i] = arg2[i];
|
||||||
|
buflen = i;
|
||||||
|
} else
|
||||||
if (sc_hex_to_bin(arg2, buf, &buflen) != 0) {
|
if (sc_hex_to_bin(arg2, buf, &buflen) != 0) {
|
||||||
printf("Invalid key value.\n");
|
printf("Invalid key value.\n");
|
||||||
goto usage;
|
goto usage;
|
||||||
|
@ -510,6 +551,9 @@ int do_verify(const char *arg, const char *arg2)
|
||||||
case 1:
|
case 1:
|
||||||
type = SC_AC_AUT;
|
type = SC_AC_AUT;
|
||||||
break;
|
break;
|
||||||
|
case 2:
|
||||||
|
type = SC_AC_PRO;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
r = sc_verify(card, type, ref, buf, buflen, &tries_left);
|
r = sc_verify(card, type, ref, buf, buflen, &tries_left);
|
||||||
if (r) {
|
if (r) {
|
||||||
|
@ -721,10 +765,52 @@ void usage()
|
||||||
printf(" %s\n", cmds[i]);
|
printf(" %s\n", cmds[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int parse_line(char *in, char **argv)
|
||||||
|
{
|
||||||
|
int argc;
|
||||||
|
|
||||||
|
for (argc = 0; argc < 3; argc++) {
|
||||||
|
in += strspn(in, " \t\n");
|
||||||
|
if (*in == '\0')
|
||||||
|
return argc;
|
||||||
|
if (*in == '"') {
|
||||||
|
/* Parse quoted string */
|
||||||
|
argv[argc] = in++;
|
||||||
|
in += strcspn(in, "\"");
|
||||||
|
if (*in++ != '"')
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
/* White space delimited word */
|
||||||
|
argv[argc] = in;
|
||||||
|
in += strcspn(in, " \t\n");
|
||||||
|
}
|
||||||
|
if (*in != '\0')
|
||||||
|
*in++ = '\0';
|
||||||
|
}
|
||||||
|
return argc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef USE_READLINE
|
||||||
|
char * readline(const char *prompt)
|
||||||
|
{
|
||||||
|
static char buf[128];
|
||||||
|
|
||||||
|
printf("%s", prompt);
|
||||||
|
fflush(stdout);
|
||||||
|
if (fgets(buf, sizeof(buf), stdin) == NULL)
|
||||||
|
return NULL;
|
||||||
|
if (strlen(buf) == 0)
|
||||||
|
return NULL;
|
||||||
|
if (buf[strlen(buf)-1] == '\n')
|
||||||
|
buf[strlen(buf)-1] = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char * const argv[])
|
int main(int argc, char * const argv[])
|
||||||
{
|
{
|
||||||
int r, c, long_optind = 0, err = 0;
|
int r, c, long_optind = 0, err = 0;
|
||||||
char line[80], cmd[80], arg[80], arg2[80];
|
char *line, *cargv[3];
|
||||||
|
|
||||||
printf("OpenSC Explorer version %s\n", sc_version);
|
printf("OpenSC Explorer version %s\n", sc_version);
|
||||||
|
|
||||||
|
@ -789,33 +875,29 @@ int main(int argc, char * const argv[])
|
||||||
}
|
}
|
||||||
while (1) {
|
while (1) {
|
||||||
int i;
|
int i;
|
||||||
|
char prompt[40];
|
||||||
|
|
||||||
printf("OpenSC [");
|
sprintf(prompt, "OpenSC [");
|
||||||
for (i = 0; i < current_path.len; i++) {
|
for (i = 0; i < current_path.len; i++) {
|
||||||
if ((i & 1) == 0 && i)
|
if ((i & 1) == 0 && i)
|
||||||
printf("/");
|
sprintf(prompt+strlen(prompt), "/");
|
||||||
printf("%02X", current_path.value[i]);
|
sprintf(prompt+strlen(prompt), "%02X", current_path.value[i]);
|
||||||
}
|
}
|
||||||
printf("]> ");
|
sprintf(prompt+strlen(prompt), "]> ");
|
||||||
fflush(stdout);
|
line = readline(prompt);
|
||||||
fflush(stdin);
|
if (line == NULL)
|
||||||
if (fgets(line, sizeof(line), stdin) == NULL)
|
|
||||||
break;
|
break;
|
||||||
if (strlen(line) == 0)
|
r = parse_line(line, cargv);
|
||||||
break;
|
|
||||||
r = sscanf(line, "%s %s %s", cmd, arg, arg2);
|
|
||||||
if (r < 1)
|
if (r < 1)
|
||||||
continue;
|
continue;
|
||||||
if (r < 3)
|
while (r < 3)
|
||||||
arg2[0] = 0;
|
cargv[r++] = "";
|
||||||
if (r < 2)
|
r = ambiguous_match(cmds, nr_cmds, cargv[0]);
|
||||||
arg[0] = 0;
|
|
||||||
r = ambiguous_match(cmds, nr_cmds, cmd);
|
|
||||||
if (r < 0) {
|
if (r < 0) {
|
||||||
usage();
|
usage();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
handle_cmd(r, arg, arg2);
|
handle_cmd(r, cargv[1], cargv[2]);
|
||||||
}
|
}
|
||||||
end:
|
end:
|
||||||
die(err);
|
die(err);
|
||||||
|
|
Loading…
Reference in New Issue