scd: Implement RSA signing for PIV cards.
authorWerner Koch <wk@gnupg.org>
Fri, 8 Feb 2019 15:46:52 +0000 (16:46 +0100)
committerWerner Koch <wk@gnupg.org>
Fri, 8 Feb 2019 16:03:32 +0000 (17:03 +0100)
* 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 <wk@gnupg.org>
scd/app-piv.c

index 4387b3a..1d70db5 100644 (file)
@@ -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);
-          goto leave;
+          need_algo = GCRY_MD_SHA256;
+          need_digestlen = 32;
         }
-      if (indatalen != tmpllen + 32 || memcmp (indata, tmpl, tmpllen))
+      else
         {
-          err = GPG_ERR_INV_VALUE;
-          log_error ("piv: bad formatted input for ECC-P256 auth\n");
+          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;
         }
-      indata +=tmpllen;
-      indatalen -= tmpllen;
+
+      if (indatalen > need_digestlen)
+        {
+          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;
+        }
     }
-  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;