/* $Id: TestSc14nPKI.c $
 * Last updated:
 *   $Date: 2017-07-16 07:31 $
 *   $Version: 0.9.0 $
 */
/* Some tests using the Sc14n C/C++ interface with CryptoSys PKI.
 * Please report any bugs to <http://www.di-mgt.com.au/contact>
 */
/******************************* LICENSE ***********************************
* Copyright (C) 2017 David Ireland, DI Management Services Pty Limited.
* All rights reserved. <www.di-mgt.com.au> <www.cryptosys.net>
* The code in this module is licensed under the terms of the MIT license,
* unless otherwise marked.
* For a copy, see <http://opensource.org/licenses/MIT>
****************************************************************************
*/
#if _MSC_VER >= 1100
/* Detect memory leaks in MSVC++ */
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#else
#include <stdlib.h>
#endif
#include <stdio.h>
#include <string.h>
#include "diSc14n.h"
#include "diCrPKI.h"

/* 
 * Requires `Sc14n` and `CryptoSys PKI` to be installed on your system,
 * available from <http://di-mgt.com.au/sc14n/> and <http://cryptosys.net/pki/>.
 * In particular that `diSc14n.dll` and `diCrPKI.dll` are in your library path.
 * You must link to `diSc14n.lib` and `diCrPKI.lib`. In MSVC++ IDE, use
 * Project > Configuration Properties > Linker > Input > Additional Dependencies 
 * and add the .lib file paths, e.g.
 *     Additional Dependencies = $(OutDir)diSc14n.lib;$(OutDir)diCrPKI.lib;%(AdditionalDependencies)
 * Using command-line:
 *     CL TestSc14nPKI.c /link ..\Release\diSc14n.lib  diCrPKI.lib
 *
 * Test files, e.g. `olamundo.xml`, are in `sc14n-testfiles.zip`. These must be in the CWD.
 */

#ifdef NDEBUG
/* Make sure assertion testing is turned on */
#undef NDEBUG
#endif
#include <assert.h>

// DEBUGGING UTILS
// Comment/uncomment next line to turn on/off debugging output
#define NO_DPRINTF
#if (defined(_DEBUG) && !(defined(NO_DPRINTF)))
#define DPRINTF0(s) printf(s)
#define DPRINTF1(s, a1) printf(s, a1)
#define DPRINTF2(s, a1, a2) printf(s, a1, a2)
#else
#define DPRINTF0(s) 
#define DPRINTF1(s, a1) 
#define DPRINTF2(s, a1, a2) 
#endif


// HARD-CODED PRIVATE KEY AND CERTIFICATE (FOR OUR CONVENIENCE IN TESTING)
// Alice's PKCS8 encrypted key and X.509 certificate
// from RFC 4134 "Examples of S/MIME Messages"
// Private key password is "password"
static const char *myPassword = "password";
static const char *myPriKey = "-----BEGIN ENCRYPTED PRIVATE KEY-----"
  "MIICojAcBgoqhkiG9w0BDAEDMA4ECFleZ90vhGrRAgIEAASCAoA9rti16XVH"
  "K4AJVe1CNf61NIpIogu/Xs4Yn4hXflvewiOwe6/9FkxBXLbhKdbQWn1Z4p3C"
  "njVns2VYEO/qpJR3LciHMwp5dsqedUVVia//CqFHtEV9WfvCKWgmlkkT1YEm"
  "1aChZnPP5i6IhwVT9qvFluTZhvVmjW0YyF86OrOp0uxxVic7phPbnPrOMelf"
  "ZPc3A3EGpzDPkxN+o0obw87tUgCL+s0KtUOr3c6Si4KQ3IQjrjZxQF4Se3t/"
  "4PEpqUl5EpYiCx9q5uqb0Lr1kWiiQ5/inZm5ETc+qO+ENcp0KjnX523CATYd"
  "U5iOjl/X9XZeJrMpOCXogEuhmLPRauYP1HEWnAY/hLW93v10QJXY6ALlbkL0"
  "sd5WU8Ces7T04b/p4/12yxqYqV68QePyfHpegdraDq3vRfopSwrUxtL9cisP"
  "jsQcJ5FL/SfloFbmld4CKIjMsromsEWqo6rfo3JqNizgTVIIWExy3jDT9VvK"
  "d9ADH0g3JCbuFzaWVOZMmZ0wlo28PKkLQ8FkW8CG/Lq/Q/bHLPM+sPdLN+ke"
  "gpA6fvL4wpku4ST7hmeN1vWbRLlCfuFijux77hdM7knO9/MawICsA4XdzR78"
  "p0C2hJlc6p46IWZaINQXGstTbJMh+mJ7i1lrbG2kvZ2Twf9R+RaLp2mPHjb1"
  "+P+3f2L3tOoC31oJ18u/L1MXEWxLEZHB0+ANg+N/0/icwImcI0D+wVN2puU4"
  "m58j81sGZUEAB3aFEbPxoX3y+qYlOnt1OfdY7WnNdyr9ZzI09fkrTvujF4LU"
  "nycqE+MXerf0PxkNu1qv9bQvCoH8x3J2EVdMxPBtH1Fb7SbE66cNyh//qzZo"
  "B9Je"
  "-----END ENCRYPTED PRIVATE KEY-----";
static const char *myCert = "-----BEGIN CERTIFICATE-----"
  "MIICLDCCAZWgAwIBAgIQRjRrx4AAVrwR024uxBCzsDANBgkqhkiG9w0BAQUFADAS"
  "MRAwDgYDVQQDEwdDYXJsUlNBMB4XDTk5MDkxOTAxMDg0N1oXDTM5MTIzMTIzNTk1"
  "OVowEzERMA8GA1UEAxMIQWxpY2VSU0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ"
  "AoGBAOCJczmN2PX16Id2OX9OsAW7U4PeD7er3H3HdSkNBS5tEt+mhibU0m+qWCn8"
  "l+z6glEPMIC+sVCeRkTxLLvYMs/GaG8H2bBgrL7uNAlqE/X3BQWT3166NVbZYf8Z"
  "f8mB5vhs6odAcO+sbSx0ny36VTq5mXcCpkhSjE7zVzhXdFdfAgMBAAGjgYEwfzAM"
  "BgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAfBgNVHSMEGDAWgBTp4JAnrHgg"
  "eprTTPJCN04irp44uzAdBgNVHQ4EFgQUd9K00bdMioqjzkWdzuw8oDrj/1AwHwYD"
  "VR0RBBgwFoEUQWxpY2VSU0FAZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEFBQADgYEA"
  "PnBHqEjME1iPylFxa042GF0EfoCxjU3MyqOPzH1WyLzPbrMcWakgqgWBqE4lradw"
  "FHUv9ceb0Q7pY9Jkt8ZmbnMhVN/0uiVdfUnTlGsiNnRzuErsL2Tt0z3Sp0LF6DeK"
  "tNufZ+S9n/n+dO/q+e5jatg/SyUJtdgadq7rm9tJsCI="
  "-----END CERTIFICATE-----";


/* PKI HELPER FUNCTIONS */

/** Read private key into internal key string, valid only for this session. 
@returns Pointer to string buffer containing key string or NULL on error
@remark Caller to free allocated memory
*/
char *pki_rsaReadPrivateKey(const char *prikey, const char *password, int *pstatus)
{
  long nchars;
  char *buf;
  nchars = RSA_ReadAnyPrivateKey(NULL, 0, prikey, password, 0);
  if (nchars < 0) {
    *pstatus = nchars;
    return NULL;
  }
  buf = malloc(nchars + 1);
  nchars = RSA_ReadAnyPrivateKey(buf, nchars, prikey, password, 0);
  *pstatus = 0;
  return buf;
}

/** Compute digest value in base64 form.
@param digestbuf Buffer to receive digest value
@param bufsize Size of buffer in bytes
@param s String to be digested
@param digalg Digest algorithm flag (0 = SHA-1)
@return Number of characters in digestbuf or negative error code
*/
int pki_hashStringToBase64(char *digestbuf, size_t bufsize, const char *s, long digalg)
{
  unsigned char b[PKI_MAX_HASH_BYTES];
  long nb, nc;
  // Compute digest value in raw bytes
  nb = HASH_Bytes(b, sizeof(b), s, strlen(s), digalg);
  if (nb < 0) return nb;
  // Convert to base64 encoded value
  nc = CNV_B64StrFromBytes(digestbuf, bufsize - 1, b, nb);
  return nc;
}

/** Compute base64-encoded signature value from base64-encoded digest value. */
char *pki_SigValFromDigVal(const char *digval, const char *prikey, const char *password, int *pstatus)
{
  unsigned char b[PKI_MAX_HASH_BYTES];
  long nb, nchars;
  char *buf;
  // Convert base64-encoded value to raw bytes
  nb = CNV_BytesFromB64Str(b, sizeof(b), digval);
  // Sign the digest value
  nchars = SIG_SignData(NULL, 0, b, nb, prikey, password, "sha1WithRSAEncryption", PKI_SIG_USEDIGEST);
  if (nchars < 0) {
    *pstatus = nchars;
    return NULL;
  }
  buf = malloc(nchars + 1);
  nchars = SIG_SignData(buf, nchars, b, nb, prikey, password, "sha1WithRSAEncryption", PKI_SIG_USEDIGEST);
  *pstatus = 0;
  return buf;
}

/** Extract XML-style <RSAKeyValue> from RSA private key. */
char *pki_KeyValFromPriKey(const char *prikey, const char *password, int *pstatus)
{
  char *buf;
  char *keystr;
  long nchars;
  keystr = pki_rsaReadPrivateKey(prikey, password, pstatus);
  if (!keystr) return NULL;
  // Form XML RSAKeyValue:
  // CAUTION: do not include your private key data, just the public key
  nchars = RSA_ToXMLString(NULL, 0, keystr, PKI_XML_EXCLPRIVATE);
  if (nchars < 0) {
    free(keystr);
    *pstatus = nchars;
    return NULL;
  }
  buf = malloc(nchars + 1);
  nchars = RSA_ToXMLString(buf, nchars, keystr, PKI_XML_EXCLPRIVATE);
  free(keystr);
  *pstatus = 0;
  return buf;
}

/* FILE UTILITIES */

/** Read a binary file into a null-terminated string.
*  @return Pointer to allocated string buffer or NULL on error
*  @remark Caller must free allocated memory
*/
static unsigned char *file_to_string(const char *fname) 
{
  FILE *fp;
  char *buf;
  long flen;
  size_t nread;

  fp = fopen(fname, "rb");
  if (!fp) return NULL;
  fseek(fp, 0, SEEK_END);
  flen = ftell(fp);
  if (flen < 0) return NULL;
  buf = malloc(flen + 1);
  rewind(fp);
  nread = fread(buf, 1, flen, fp);
  fclose(fp);
  buf[nread] = '\0';

  return buf;
}

/** Write a new binary file from a string. */
static int file_from_string(const char *fname, const char *s)
{
  FILE *fpo;
  size_t nwritten;
  int r;

  fpo = fopen(fname, "wb");
  if (!fpo) return -1;
  nwritten = fwrite(s, 1, strlen(s), fpo);
  r = fclose(fpo);

  return r; /* 0 = success */
}


/// <summary>
/// Create a XML-DSIG signed file given proforma XML document
/// </summary>
/// <param name="outFile">Name of outfile to create</param>
/// <param name="baseFile">Name of input XML document</param>
/// <param name="priKey">PKCS8 encrypted private key file or PEM-string</param>
/// <param name="password">Password for private key</param>
/// <returns>Zero (0) on success otherwise nonzero error code</returns>
/// <remarks>Input XML document is expected to be enveloped-signature with single reference URI="",
/// C14N method REC-xml-c14n-20010315, signature method xmldsig#rsa-sha1, and digest method xmldsig#sha1.
/// KeyValue is expected to be in RSAKeyValue form.
/// Items to be replaced should be marked "%digval%", "%sigval%" and "%keyval%".
/// </remarks>
int MakeSignedXml(const char *outfile, const char *basefile, const char *prikey, const char *password)
{
  /* declaration for 3rd-party function with code below */
  char *replace_str(const char *str, const char *old, const char *news);
  int status;
  long r, nchars;
  char *buf = NULL;
  char *newsi = NULL;
  char digval[SC14N_MAX_DIGEST_CHARS + 1];
  char digval_si[SC14N_MAX_DIGEST_CHARS + 1];
  char *sigval = NULL;
  char *keyval = NULL;
  char *xmlstr = NULL;
  char *newxml1 = NULL;
  char *newxml2 = NULL;
  char *newxml3 = NULL;

  // 1. Compute digest value of body excluding <Signature> element
  // (this assumes Reference URI="" and DigestMethod is SHA-1)
  r = C14N_File2Digest(digval, sizeof(digval) - 1, basefile, "Signature", "", SC14N_TRAN_EXCLUDEBYTAG);
  if (r < 0) {
    status = r;
    goto clean_up;
  }
  DPRINTF1("DIGVAL=%s\n", digval);

  // 2. Extract the SignedInfo element into memory
  // Note %digval% parameter to be completed
  nchars = C14N_File2String(NULL, 0, basefile, "SignedInfo", "", SC14N_TRAN_SUBSETBYTAG);
  if (nchars < 0) {
    status = nchars;
    goto clean_up;
  }
  buf = malloc(nchars + 1);
  nchars = C14N_File2String(buf, nchars, basefile, "SignedInfo", "", SC14N_TRAN_SUBSETBYTAG);
  DPRINTF1("SIGNEDINFO (BASE):\n%s\n", buf);

  // 3. Insert the required DigestValue we prepared earlier
  newsi = replace_str(buf, "%digval%", digval);
  DPRINTF0("SIGNEDINFO (COMPLETED):\n");
  DPRINTF1("%s\n", newsi);

  // 4. Compute the digest value of this string
  pki_hashStringToBase64(digval_si, sizeof(digval_si), newsi, PKI_HASH_SHA1);
  DPRINTF1("SHA1(signedinfo)=%s\n", digval_si);

  // 5. Compute signature from this digest value
  sigval = pki_SigValFromDigVal(digval_si, prikey, password, &status);
  if (!sigval) {
    fprintf(stderr, "ERROR: failed to create signature value");
    goto clean_up;
  }
  DPRINTF1("SIGVAL:\n%s\n", sigval);

  // 6. Get the RSA Key Value in required XML form
  // NB We can extract the public key value from the RSA private key
  keyval = pki_KeyValFromPriKey(prikey, password, &status);
  if (!keyval) {
    fprintf(stderr, "ERROR: failed to create Key value");
    goto clean_up;
  }
  DPRINTF1("KEYVAL:\n%s\n", keyval);

  // 7. Compose the output file by substituting the correct values
  // (Note we make no other checks of the input XML - that's up to you)

  // 7.1 Read in the base XML file
  xmlstr = file_to_string(basefile);
  if (!xmlstr) {
    fprintf(stderr, "ERROR: cannot read file '%s'", basefile);
    status = -1; 
    goto clean_up;
  }
  // 7.2 Substitute %% values
  newxml1 = replace_str(xmlstr, "%digval%", digval);
  newxml2 = replace_str(newxml1, "%sigval%", sigval);
  newxml3 = replace_str(newxml2, "%keyval%", keyval);

  // 7.3 Write out new string to file
  r = file_from_string(outfile, newxml3);
  DPRINTF1("file_from_string() returns %d (expected 0)\n", r);
  if (r != 0) {
    fprintf(stderr, "ERROR: failed to create file '%s'", outfile);
    status = r;
    goto clean_up;
  }

  // If we got here all is well
  status = 0;

clean_up:
  if (buf) free(buf);
  if (newsi) free(newsi);
  if (sigval) free(sigval);
  if (keyval) free(keyval);
  if (xmlstr) free(xmlstr);
  if (newxml1) free(newxml1);
  if (newxml2) free(newxml2);
  if (newxml3) free(newxml3);

  return status;
}

/* DO THE BUSINESS... */
int main(void)
{
  long n;
  char *fname, *oname;

  /* MSVC memory leak checking stuff */
#if _MSC_VER >= 1100
  _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
  _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
  _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDOUT);
  _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
  _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDOUT);
  _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
  _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDOUT);
#endif

  /* General information about the core library DLLs */
  // If either of these fail, the package is not installed properly...
  printf("Sc14n Version=%ld\n", SC14N_Gen_Version());
  printf("CrPKI Version=%ld\n", PKI_Version(0, 0));

  fname = "olamundo-base.xml";
  oname = "olamundo-new-signed.xml";
  printf("FILE: %s\n", fname);
  n = MakeSignedXml(oname, fname, myPriKey, myPassword);
  printf("MakeSignedXML->'%s' returns %ld (expecting 0)\n", oname, n);

  // Input XML contains Chinese characters UTF-8-encoded
  fname = "daiwei-base.xml";
  oname = "daiwei-new-signed.xml";
  printf("FILE: %s\n", fname);
  n = MakeSignedXml(oname, fname, myPriKey, myPassword);
  printf("MakeSignedXML->'%s' returns %ld (expecting 0)\n", oname, n);

  // Input XML contains Chinese characters as character entities
  // Note that digest value and signature value should be identical to previous one
  fname = "daiwei-ents-base.xml";
  oname = "daiwei-ents-new-signed.xml";
  printf("FILE: %s\n", fname);
  n = MakeSignedXml(oname, fname, myPriKey, myPassword);
  printf("MakeSignedXML->'%s' returns %ld (expecting 0)\n", oname, n);

  printf("\nALL DONE.\n");

}


/* THIRD-PARTY CODE ********************************************************************************/

/* Ref: http://creativeandcritical.net/str-replace-c/
* Description:  Replaces in the string str all the occurrences of the source string old
* with the destination string new. The parameters old and new may be of any length,
* and their lengths are allowed to differ.
* None of the three parameters may be NULL.
*
* Returns:  The post-replacement string, or NULL if memory for the new string could not be allocated.
* Does not modify the original string. The memory for the returned post-replacement
* string may be deallocated with the standard library function free() when it is no longer required.
*
* Licence: Public domain. You may use this code in any way you see fit,
* optionally crediting its author (me, Laird Shaw, with assistance from comp.lang.c).
* http://creativeandcritical.net/contact/
*/

char *replace_str(const char *str, const char *old, const char *news)
{
  char *ret, *r;
  const char *p, *q;
  size_t oldlen = strlen(old);
  size_t count, retlen, newlen = strlen(news);

  if (oldlen != newlen) {
    for (count = 0, p = str; (q = strstr(p, old)) != NULL; p = q + oldlen)
      count++;
    /* this is undefined if p - str > PTRDIFF_MAX */
    retlen = p - str + strlen(p) + count * (newlen - oldlen);
  }
  else
    retlen = strlen(str);

  if ((ret = malloc(retlen + 1)) == NULL)
    return NULL;

  for (r = ret, p = str; (q = strstr(p, old)) != NULL; p = q + oldlen) {
    /* this is undefined if q - p > PTRDIFF_MAX */
    ptrdiff_t l = q - p;
    memcpy(r, p, l);
    r += l;
    memcpy(r, news, newlen);
    r += newlen;
  }
  strcpy(r, p);

  return ret;
}
/*****************************************************************************************************/