From 53beea56afecde76f0f4ca93fc50ca59298a093e Mon Sep 17 00:00:00 2001 From: Werner Koch Date: Fri, 8 Feb 2019 16:46:52 +0100 Subject: [PATCH] scd: Implement RSA signing for PIV cards. * scd/app-piv.c (concat_tlv_list): New. (get_key_algorithm_by_dobj): Rename args for clarity. (do_auth): factor all code out to ... (do_sign): new. Implement RSA signing. Signed-off-by: Werner Koch --- scd/app-piv.c | 398 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 299 insertions(+), 99 deletions(-) diff --git a/scd/app-piv.c b/scd/app-piv.c index 4387b3aef..1d70db51c 100644 --- a/scd/app-piv.c +++ b/scd/app-piv.c @@ -469,6 +469,107 @@ add_tlv (unsigned char *buffer, unsigned int tag, size_t length) } +/* Function to build a list of TLV and return the result in a mallcoed + * buffer. The varargs are tuples of (int,size_t,void) each with the + * tag, the length and the actual data. A (0,0,NULL) tuple terminates + * the list. Up to 10 tuples are supported. */ +static gpg_error_t +concat_tlv_list (unsigned char **r_result, size_t *r_resultlen, ...) +{ + gpg_error_t err; + va_list arg_ptr; + struct { + int tag; + unsigned int len; + unsigned int contlen; + const void *data; + } argv[10]; + int i, j, argc; + unsigned char *data = NULL; + size_t datalen; + unsigned char *p; + size_t n; + + *r_result = NULL; + *r_resultlen = 0; + + /* Collect all args. Check that length is <= 2^16 to match the + * behaviour of add_tlv. */ + va_start (arg_ptr, r_resultlen); + argc = 0; + while (((argv[argc].tag = va_arg (arg_ptr, int)))) + { + argv[argc].len = va_arg (arg_ptr, size_t); + argv[argc].contlen = 0; + argv[argc].data = va_arg (arg_ptr, const void *); + if (argc >= DIM (argv)-1 || argv[argc].len > 0xffff) + { + va_end (arg_ptr); + err = gpg_error (GPG_ERR_EINVAL); + goto leave; + } + argc++; + } + va_end (arg_ptr); + + /* Compute the required buffer length and allocate the buffer. */ + datalen = 0; + for (i=0; i < argc; i++) + { + if (!argv[i].len && !argv[i].data) + { + /* Constructed tag. Compute its length. Note that we + * currently allow only one constructed tag in the list. */ + for (n=0, j = i + 1; j < argc; j++) + { + log_assert (!(!argv[j].len && !argv[j].data)); + n += add_tlv (NULL, argv[j].tag, argv[j].len); + n += argv[j].len; + } + argv[i].contlen = n; + datalen += add_tlv (NULL, argv[i].tag, n); + } + else + { + datalen += add_tlv (NULL, argv[i].tag, argv[i].len); + datalen += argv[i].len; + } + } + data = xtrymalloc (datalen); + if (!data) + { + err = gpg_error_from_syserror (); + goto leave; + } + + /* Copy that data to the buffer. */ + p = data; + for (i=0; i < argc; i++) + { + if (!argv[i].len && !argv[i].data) + { + /* Constructed tag. */ + p += add_tlv (p, argv[i].tag, argv[i].contlen); + } + else + { + p += add_tlv (p, argv[i].tag, argv[i].len); + memcpy (p, argv[i].data, argv[i].len); + p += argv[i].len; + } + } + log_assert ( data + datalen == p ); + *r_result = data; + data = NULL; + *r_resultlen = datalen; + err = 0; + + leave: + xfree (data); + return err; +} + + /* Wrapper around iso7816_put_data_odd which also sets the tag into * the '5C' data object. The varargs are tuples of (int,size_t,void) * with the tag, the length and the actual data. A (0,0,NULL) tuple @@ -1354,7 +1455,7 @@ do_readkey (app_t app, int advanced, const char *keyrefstr, * store it at R_ALGO. The algorithm is taken from the corresponding * certificate or from a cache. */ static gpg_error_t -get_key_algorithm_by_dobj (app_t app, data_object_t dobj, int *r_algo) +get_key_algorithm_by_dobj (app_t app, data_object_t dobj, int *r_mechanism) { gpg_error_t err; unsigned char *certbuf = NULL; @@ -1369,7 +1470,7 @@ get_key_algorithm_by_dobj (app_t app, data_object_t dobj, int *r_algo) size_t n; const char *curve_name; - *r_algo = 0; + *r_mechanism = 0; err = readcert_by_tag (app, dobj->tag, &certbuf, &certbuflen, &mechanism); if (err) @@ -1382,7 +1483,7 @@ get_key_algorithm_by_dobj (app_t app, data_object_t dobj, int *r_algo) case PIV_ALGORITHM_RSA: case PIV_ALGORITHM_ECC_P256: case PIV_ALGORITHM_ECC_P384: - *r_algo = mechanism; + *r_mechanism = mechanism; break; default: @@ -1468,7 +1569,7 @@ get_key_algorithm_by_dobj (app_t app, data_object_t dobj, int *r_algo) dobj->keyref, algoname, gpg_strerror (err)); goto leave; } - *r_algo = algo; + *r_mechanism = algo; leave: gcry_free (algoname); @@ -1862,10 +1963,11 @@ do_check_chv (app_t app, const char *pwidstr, * stored there and an error code returned. For ECDSA the result is * the simple concatenation of R and S without any DER encoding. R * and S are left extended with zeroes to make sure they have an equal - * length. + * length. If HASHALGO is not zero, the function prepends the hash's + * OID to the indata or checks that it is consistent. */ static gpg_error_t -do_auth (app_t app, const char *keyidstr, +do_sign (app_t app, const char *keyidstr, int hashalgo, gpg_error_t (*pincb)(void*, const char *, char **), void *pincb_arg, const void *indata_arg, size_t indatalen, @@ -1874,13 +1976,16 @@ do_auth (app_t app, const char *keyidstr, const unsigned char *indata = indata_arg; gpg_error_t err; data_object_t dobj; - unsigned char tmpl[2+2+2+128]; - size_t tmpllen; + unsigned char oidbuf[64]; + size_t oidbuflen; unsigned char *outdata = NULL; size_t outdatalen; const unsigned char *s; size_t n; - int keyref, algo; + int keyref, mechanism; + unsigned char *indata_buffer = NULL; /* Malloced helper. */ + unsigned char *apdudata = NULL; + size_t apdudatalen; if (!keyidstr || !*keyidstr) { @@ -1888,9 +1993,6 @@ do_auth (app_t app, const char *keyidstr, goto leave; } - /* Fixme: Shall we support the KEYID/FINGERPRINT syntax? Does it - * make sense for X.509 certs? */ - dobj = find_dobj_by_keyref (app, keyidstr); if ((keyref = keyref_from_dobj (dobj)) == -1) { @@ -1898,69 +2000,141 @@ do_auth (app_t app, const char *keyidstr, goto leave; } - err = get_key_algorithm_by_dobj (app, dobj, &algo); + err = get_key_algorithm_by_dobj (app, dobj, &mechanism); if (err) goto leave; - /* We need to remove the ASN.1 prefix from INDATA. We use TEMPL as - * a temporary buffer for the OID. */ - if (algo == PIV_ALGORITHM_ECC_P256) + /* For ECC we need to remove the ASN.1 prefix from INDATA. For RSA + * we need to add the padding and possible also the ASN.1 prefix. */ + if (mechanism == PIV_ALGORITHM_ECC_P256 + || mechanism == PIV_ALGORITHM_ECC_P384) { - tmpllen = sizeof tmpl; - err = gcry_md_get_asnoid (GCRY_MD_SHA256, &tmpl, &tmpllen); - if (err) + int need_algo, need_digestlen; + + if (mechanism == PIV_ALGORITHM_ECC_P256) { - err = gpg_error (GPG_ERR_INTERNAL); - log_debug ("piv: no OID for hash algo %d\n", GCRY_MD_SHA256); + need_algo = GCRY_MD_SHA256; + need_digestlen = 32; + } + else + { + need_algo = GCRY_MD_SHA384; + need_digestlen = 48; + } + + if (hashalgo && hashalgo != need_algo) + { + err = gpg_error (GPG_ERR_UNSUPPORTED_ALGORITHM); + log_error ("piv: hash algo %d does not match mechanism %d\n", + need_algo, mechanism); goto leave; } - if (indatalen != tmpllen + 32 || memcmp (indata, tmpl, tmpllen)) + + if (indatalen > need_digestlen) { - err = GPG_ERR_INV_VALUE; - log_error ("piv: bad formatted input for ECC-P256 auth\n"); - goto leave; + oidbuflen = sizeof oidbuf; + err = gcry_md_get_asnoid (need_algo, &oidbuf, &oidbuflen); + if (err) + { + err = gpg_error (GPG_ERR_INTERNAL); + log_debug ("piv: no OID for hash algo %d\n", need_algo); + goto leave; + } + if (indatalen != oidbuflen + need_digestlen + || memcmp (indata, oidbuf, oidbuflen)) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("piv: bad input for signing with mechanism %d\n", + mechanism); + goto leave; + } + indata += oidbuflen; + indatalen -= oidbuflen; } - indata +=tmpllen; - indatalen -= tmpllen; } - else if (algo == PIV_ALGORITHM_ECC_P384) + else if (mechanism == PIV_ALGORITHM_RSA) { - tmpllen = sizeof tmpl; - err = gcry_md_get_asnoid (GCRY_MD_SHA384, &tmpl, &tmpllen); - if (err) + /* PIV requires 2048 bit RSA. */ + unsigned int framelen = 2048 / 8; + unsigned char *frame; + int i; + + oidbuflen = sizeof oidbuf; + if (!hashalgo) { - err = gpg_error (GPG_ERR_INTERNAL); - log_debug ("piv: no OID for hash algo %d\n", GCRY_MD_SHA384); + /* We assume that indata already has the required + * digestinfo; thus merely prepend the padding below. */ + } + else if ((err = gcry_md_get_asnoid (hashalgo, &oidbuf, &oidbuflen))) + { + log_debug ("piv: no OID for hash algo %d\n", hashalgo); goto leave; } - if (indatalen != tmpllen + 48 || memcmp (indata, tmpl, tmpllen)) + else { - err = GPG_ERR_INV_VALUE; - log_error ("piv: bad formatted input for ECC-P384 auth\n"); + unsigned int digestlen = gcry_md_get_algo_dlen (hashalgo); + + if (indatalen == digestlen) + { + /* Plain hash in INDATA; prepend the digestinfo. */ + indata_buffer = xtrymalloc (oidbuflen + indatalen); + if (!indata_buffer) + { + err = gpg_error_from_syserror (); + goto leave; + } + memcpy (indata_buffer, oidbuf, oidbuflen); + memcpy (indata_buffer+oidbuflen, indata, indatalen); + indata = indata_buffer; + indatalen = oidbuflen + indatalen; + } + else if (indatalen == oidbuflen + digestlen + && !memcmp (indata, oidbuf, oidbuflen)) + ; /* Correct prefix. */ + else + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("piv: bad input for signing with RSA and hash %d\n", + hashalgo); + goto leave; + } + } + /* Now prepend the pkcs#v1.5 padding. We require at least 8 + * byte of padding and 3 extra bytes for the prefix and the + * delimiting nul. */ + if (!indatalen || indatalen + 8 + 4 > framelen) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("piv: input does not fit into a %u bit PKCS#v1.5 frame\n", + 8*framelen); goto leave; } - indata += tmpllen; - indatalen -= tmpllen; - } - else if (algo == PIV_ALGORITHM_RSA) - { - err = gpg_error (GPG_ERR_NOT_IMPLEMENTED); - log_error ("piv: FIXME: implement RSA authentication\n"); - goto leave; + frame = xtrymalloc (framelen); + if (!frame) + { + err = gpg_error_from_syserror (); + goto leave; + } + n = 0; + frame[n++] = 0; + frame[n++] = 1; /* Block type. */ + i = framelen - indatalen - 3 ; + memset (frame+n, 0xff, i); + n += i; + frame[n++] = 0; /* Delimiter. */ + memcpy (frame+n, indata, indatalen); + n += indatalen; + log_assert (n == framelen); + /* And now put it into the indata_buffer. */ + xfree (indata_buffer); + indata_buffer = frame; + indata = indata_buffer; + indatalen = framelen; } else { err = gpg_error (GPG_ERR_INTERNAL); - log_debug ("piv: unknown PIV algo %d from helper function\n", algo); - goto leave; - } - - /* Because we don't have a dynamic template builder we make sure - * that we can encode all lengths in one octet. FIXME: Use add_tls - * from app-openpgp as a base for an strconcat like function. */ - if (indatalen >= 100) - { - err = gpg_error (GPG_ERR_TOO_LARGE); + log_debug ("piv: unknown PIV mechanism %d while signing\n", mechanism); goto leave; } @@ -1970,19 +2144,18 @@ do_auth (app_t app, const char *keyidstr, return err; /* Build the Dynamic Authentication Template. */ - tmpl[0] = 0x7c; - tmpl[1] = indatalen + 4; - tmpl[2] = 0x82; /* Response. */ - tmpl[3] = 0; /* Must be 0 to get the tag in the answer. */ - tmpl[4] = 0x81; /* Challenge. */ - tmpl[5] = indatalen; - memcpy (tmpl+6, indata, indatalen); - tmpllen = indatalen + 6; + err = concat_tlv_list (&apdudata, &apdudatalen, + (int)0x7c, (size_t)0, NULL, /* Constructed. */ + (int)0x82, (size_t)0, "", + (int)0x81, (size_t)indatalen, indata, + (int)0, (size_t)0, NULL); + if (err) + goto leave; /* Note: the -1 requests command chaining. */ err = iso7816_general_authenticate (app->slot, -1, - algo, keyref, - tmpl, (int)tmpllen, 0, + mechanism, keyref, + apdudata, (int)apdudatalen, 0, &outdata, &outdatalen); if (err) goto leave; @@ -1991,42 +2164,50 @@ do_auth (app_t app, const char *keyidstr, if (outdatalen && *outdata == 0x7c && (s = find_tlv (outdata, outdatalen, 0x82, &n))) { - const unsigned char *rval, *sval; - size_t rlen, rlenx, slen, slenx, resultlen; - char *result; - /* The result of an ECDSA signature is - * SEQUENCE { r INTEGER, s INTEGER } - * We re-pack that by concatenating R and S and making sure that - * both have the same length. We simplify parsing by using - * find_tlv and not a proper DER parser. */ - s = find_tlv (s, n, 0x30, &n); - if (!s) - goto bad_der; - rval = find_tlv (s, n, 0x02, &rlen); - if (!rval) - goto bad_der; - log_assert (n >= (rval-s)+rlen); - sval = find_tlv (rval+rlen, n-((rval-s)+rlen), 0x02, &slen); - if (!rval) - goto bad_der; - rlenx = slenx = 0; - if (rlen > slen) - slenx = rlen - slen; - else if (slen > rlen) - rlenx = slen - rlen; - - resultlen = rlen + rlenx + slen + slenx; - result = xtrycalloc (1, resultlen); - if (!result) + if (mechanism == PIV_ALGORITHM_RSA) { - err = gpg_error_from_syserror (); - goto leave; + memmove (outdata, outdata + (s - outdata), n); + outdatalen = n; + } + else /* ECC */ + { + const unsigned char *rval, *sval; + size_t rlen, rlenx, slen, slenx, resultlen; + char *result; + /* The result of an ECDSA signature is + * SEQUENCE { r INTEGER, s INTEGER } + * We re-pack that by concatenating R and S and making sure + * that both have the same length. We simplify parsing by + * using find_tlv and not a proper DER parser. */ + s = find_tlv (s, n, 0x30, &n); + if (!s) + goto bad_der; + rval = find_tlv (s, n, 0x02, &rlen); + if (!rval) + goto bad_der; + log_assert (n >= (rval-s)+rlen); + sval = find_tlv (rval+rlen, n-((rval-s)+rlen), 0x02, &slen); + if (!rval) + goto bad_der; + rlenx = slenx = 0; + if (rlen > slen) + slenx = rlen - slen; + else if (slen > rlen) + rlenx = slen - rlen; + + resultlen = rlen + rlenx + slen + slenx; + result = xtrycalloc (1, resultlen); + if (!result) + { + err = gpg_error_from_syserror (); + goto leave; + } + memcpy (result + rlenx, rval, rlen); + memcpy (result + rlenx + rlen + slenx, sval, slen); + xfree (outdata); + outdata = result; + outdatalen = resultlen; } - memcpy (result + rlenx, rval, rlen); - memcpy (result + rlenx + rlen + slenx, sval, slen); - xfree (outdata); - outdata = result; - outdatalen = resultlen; } else { @@ -2048,10 +2229,29 @@ do_auth (app_t app, const char *keyidstr, *r_outdata = outdata; *r_outdatalen = outdatalen; } + xfree (apdudata); + xfree (indata_buffer); return err; } +/* AUTH for PIV cards is actually the same as SIGN. The difference + * between AUTH and SIGN is that AUTH expects that pkcs#1.5 padding + * for RSA has already been done (digestInfo part w/o the padding) + * whereas SIGN may accept a plain digest and does the padding if + * needed. This is also the reason why SIGN takes a hashalgo. */ +static gpg_error_t +do_auth (app_t app, const char *keyidstr, + gpg_error_t (*pincb)(void*, const char *, char **), + void *pincb_arg, + const void *indata, size_t indatalen, + unsigned char **r_outdata, size_t *r_outdatalen) +{ + return do_sign (app, keyidstr, 0, pincb, pincb_arg, indata, indatalen, + r_outdata, r_outdatalen); +} + + /* Check whether a key for DOBJ already exists. We detect this by * reading the certificate described by DOBJ. If FORCE is TRUE a * diagnositic will be printed but no error returned if the key @@ -2464,7 +2664,7 @@ app_select_piv (app_t app) app->fnc.writecert = do_writecert; /* app->fnc.writekey = do_writekey; */ app->fnc.genkey = do_genkey; - /* app->fnc.sign = do_sign; */ + app->fnc.sign = do_sign; app->fnc.auth = do_auth; /* app->fnc.decipher = do_decipher; */ app->fnc.change_pin = do_change_chv;