/*****************************************************************************/
/*
                               SesolaMkCert.c

Make an X509 server certificate for an SSL service when there is none
configured.  WASD used to include a static key/cert PEM periodically manually 
generated for the same purpose.  As browsers have become more and more
reluctant to accept home-brew certificates, and more recently to accept those
with lives exceeding one year it has become increasingly difficult to bootstrap
a new TLS site.  This dynamically generated certificate goes some way to
addressing this with a per-startup 180 day certificate lifetime.  Browsers are
still reluctant and if they do accept the certificate generally require
manual user exception processing.  Private/Incognito/"porn-mode" browser
instances seem to be more relaxed about accepting these certificates.

Stores the certificate in a process logical so that it can be reused.  If the
server is just restarted the certificate remains.  If the server is killed and
completely started the certificate is regenerated (because the process logical
name storage has gone).

The per-process logical name cert storage can be disabled by defining the
logical name WASD_MKCERT_NO_RETAIN prior to startup.

Core functionality purloined from:

https://opensource.apple.com/source/OpenSSL/OpenSSL-22/openssl/demos/x509/mkcert.c


VERSION HISTORY
---------------
20-MAY-2021  MGD  SesolaMkCertRetain()
18-JUL-2020  MGD  initial
*/
/*****************************************************************************/

#ifdef WASD_VMS_V7
#undef _VMS__V6__SOURCE
#define _VMS__V6__SOURCE
#undef __VMS_VER
#define __VMS_VER 70000000
#undef __CRTL_VER
#define __CRTL_VER 70000000
#endif

/* standard C header files */
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* VMS related header files */
#include <lnmdef.h>
#include <ssdef.h>
#include <stsdef.h>

/* application header files */
#define SESOLA_REQUIRED
#include "Sesola.h"

#define WASD_MODULE "SESOLAMKCERT"

/***************************************/
#ifdef SESOLA  /* secure sockets layer */
/***************************************/

#include <stdio.h>
#include <stdlib.h>

#include <openssl/pem.h>
#include <openssl/conf.h>
#include <openssl/x509v3.h>

static char* SesolaMkCertRetain (char* name, char *cert);
static int mkcert (X509 **x509p, EVP_PKEY **pkeyp,
                   int bits, int serial, int days, char *san);
static int add_ext (X509 *cert, int nid, char *value);

/********************/
/* external storage */
/********************/

#ifdef DBUG
extern BOOL Debug;
#else
#define Debug 0 
#endif

extern char  ErrorSanityCheck[],
             ServerHostName[],
             SoftwareID[];

extern unsigned long SysNamMask[];

extern BIO  *SesolaBioMemPtr;

extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
Called by SesolaInitService().
Generate a self-signed (actually an unsigned) certificate.
*/

char* SesolaMkCert (char *san)
{
   int  bits = 2048,
        days = 180;
   unsigned long  serial[2];
   char  *cptr, *pemptr;
   BIO  *bioptr;
   X509  *x5o9 = NULL;  /* note this is an oh not a zero */
   EVP_PKEY  *pkey = NULL;
   RSA  *rkey = NULL;
   
   /*********/
   /* begin */
   /*********/

   if (pemptr = SesolaMkCertRetain (san, NULL)) return (pemptr);

   /* generate a (likely) unique serial number */
   sys$gettim (&serial);
   serial[0] &= 0x7fffffff;

   mkcert (&x5o9, &pkey, bits, serial[0], days, san);
   
   rkey = EVP_PKEY_get1_RSA (pkey);
   bioptr = BIO_new (BIO_s_mem());
   RSA_print (bioptr, rkey, 0);
   X509_print (bioptr, x5o9);
   
   PEM_write_bio_PrivateKey (bioptr, pkey, NULL, NULL, 0, NULL, NULL);
   PEM_write_bio_X509 (bioptr, x5o9);
   
   X509_free (x5o9);
   EVP_PKEY_free (pkey);

   CRYPTO_cleanup_all_ex_data();
   
   BIO_get_mem_data (bioptr, &pemptr);

   if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchDataDump (pemptr, strlen(pemptr));

   if (cptr = strstr (pemptr, "-----BEGIN PRIVATE KEY-----"))
      SesolaMkCertRetain (san, cptr);

   return (cptr);
}

/*****************************************************************************/
/*
Stores the certificate in a process logical so that it can be reused.
*/

char* SesolaMkCertRetain (char* name, char *cert)
{
   static int  CertTextSize = 4096;
   static int  LnmCount;
   static unsigned short  LogValueLength;
   static char  *CertTextPtr = NULL;
   static char  LogValue [256];
   static char  LogicalName[256];
   static $DESCRIPTOR (LogNameDsc, LogicalName);
   static $DESCRIPTOR (LnmProcessDsc, "LNM$PROCESS");
   static uchar  ExecMode = 1;  /* PSL$C_EXEC */
   static VMS_ITEM_LIST3 LnmItems [] =
   {
      { sizeof(LnmCount), LNM$_INDEX, &LnmCount, 0 },
      { sizeof(LogValue)-1, LNM$_STRING, &LogValue, &LogValueLength },
      { 0,0,0,0 }
   };

   int  status;
   char  *cptr, *czptr, *sptr, *zptr;
   VMS_ITEM_LIST3 CreLnmItems [128];

   /*********/
   /* begin */
   /*********/

   if (SysTrnLnm (WASD_MKCERT_NO_RETAIN)) return (NULL);

   LogNameDsc.dsc$w_length = sprintf (LogicalName, "WASD_CERT_%s", name);
   for (cptr = LogicalName + 10; *cptr; *cptr++ = to_upper(*cptr));

   if (cert)
   {
      /**************/
      /* store cert */
      /**************/

      memset (CreLnmItems, 0, sizeof(CreLnmItems));
      cptr = cert;
      for (LnmCount = 0; LnmCount <= 127; LnmCount++)
      {
         for (sptr = cptr; *sptr && sptr - cptr < 255; sptr++);
         CreLnmItems[LnmCount].buf_len = sptr - cptr;
         CreLnmItems[LnmCount].item = LNM$_STRING;
         CreLnmItems[LnmCount].buf_addr = cptr;
         CreLnmItems[LnmCount].ret_len = 0;
         if (!*sptr) break;
         cptr = sptr;
      }

      if (VMSnok (status = sys$setprv (1, &SysNamMask, 0, 0)))
         ErrorExitVmsStatus (status, "sys$setprv()", FI_LI);

      status = sys$crelnm (0, &LnmProcessDsc, &LogNameDsc,
                           &ExecMode, &CreLnmItems);
      if (VMSnok (status)) ErrorNoticed (NULL, status, LogicalName, FI_LI);

      if (VMSnok (status = sys$setprv (0, &SysNamMask, 0, 0)))
         ErrorExitVmsStatus (status, "sys$setprv()", FI_LI);

      return (NULL);
   }
   else
   {
      /*************/
      /* read cert */
      /*************/

      for (;;)
      {
         if (!CertTextPtr) CertTextPtr = VmGet (CertTextSize);

         zptr = (sptr = CertTextPtr) + CertTextSize - 1;
         *sptr = '\0';
         for (LnmCount = 0; LnmCount <= 127; LnmCount++)
         {
            status = sys$trnlnm (0, &LnmProcessDsc, &LogNameDsc, 0, &LnmItems);
            if (VMSok (status))
            {
               if (!LogValueLength) break;
               for (czptr = (cptr = LogValue) + LogValueLength;
                    cptr < czptr && sptr < zptr;
                    *sptr++ = *cptr++);
               *sptr = '\0';
            }
            else
               break;
         }
         if (VMSnok (status)) return (NULL);
         if (!LogValueLength) return (CertTextPtr);
         VmFree (CertTextPtr, FI_LI);
         CertTextPtr = NULL;
         CertTextSize *= 2;
      }
   }
}

/*****************************************************************************/

static void callback(int p, int n, void *arg)
{
   char c='B';
   
   if (p == 0) c='.';
   if (p == 1) c='+';
   if (p == 2) c='*';
   if (p == 3) c='\n';
   fputc(c,stderr);
}

/*****************************************************************************/
/*
*/

static int mkcert
(
X509 **x509p,
EVP_PKEY **pkeyp,
int bits,
int serial,
int days,
char *san
)
{
   char  *cptr, *sptr, *zptr;
   char  buf [256];
   X509  *x5o9;  /* note this is an oh not a zero */
   EVP_PKEY  *pkey;
   RSA  *rsa;
   X509_NAME  *name = NULL;
   
   if ((pkeyp == NULL) || (*pkeyp == NULL)) {
      if ((pkey=EVP_PKEY_new()) == NULL)
         return(0);
   }
   else
      pkey = *pkeyp;
   
   if ((x509p == NULL) || (*x509p == NULL)) {
      if ((x5o9 = X509_new()) == NULL)
         goto err;
   }
   else
      x5o9 = *x509p;
   
   FaoToStdout ("Generate !AZ !UL bit private key:\n", san, bits);
   rsa = RSA_generate_key (bits, RSA_F4, callback, NULL);
   if (!EVP_PKEY_assign_RSA (pkey, rsa)) goto err;
   rsa = NULL;
   
   X509_set_version (x5o9, 2);
   ASN1_INTEGER_set (X509_get_serialNumber(x5o9), serial);
   X509_gmtime_adj (X509_get_notBefore(x5o9), 0);
   X509_gmtime_adj (X509_get_notAfter(x5o9), (long)60*60*24*days);
   X509_set_pubkey (x5o9, pkey);
   
   name = X509_get_subject_name (x5o9);
   
   /* This function creates and adds the entry, working out the
    * correct string type and performing checks on its length.
    * Normally we'd check the return value for errors...
    */
   X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, "ARPA", -1, -1, 0);
   X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
                                    ServerHostName, -1, -1, 0);

   /* it's self signed so set the issuer name to be the same as the subject */
   X509_set_issuer_name (x5o9, name);
   
   /* add various extensions: standard extensions */
   add_ext (x5o9, NID_basic_constraints, "critical,CA:TRUE");
   add_ext (x5o9, NID_key_usage, "critical,keyCertSign,cRLSign,\
digitalSignature,keyEncipherment");
   add_ext (x5o9, NID_ext_key_usage, "serverAuth");
   
   add_ext (x5o9, NID_subject_key_identifier, "hash");

   zptr = (sptr = buf) + sizeof(buf)-1;
   for (cptr = "DNS.1:"; *cptr && sptr < zptr; *sptr++ = *cptr++);
   for (cptr = san; *cptr && sptr < zptr; *sptr++ = *cptr++);
   *sptr = '\0';
   add_ext (x5o9, NID_subject_alt_name, buf);
   
   zptr = (sptr = buf) + sizeof(buf)-1;
   for (cptr = SoftwareID; *cptr && sptr < zptr; *sptr++ = *cptr++);
   for (cptr = " dynamic certificate"; *cptr && sptr < zptr; *sptr++ = *cptr++);
   *sptr = '\0';
   add_ext (x5o9, NID_netscape_comment, buf);

   if (!X509_sign (x5o9, pkey, EVP_md5())) goto err;
   
   *x509p = x5o9;
   *pkeyp = pkey;
   return (1);

err:
   return (0);
}

/*****************************************************************************/
/*
Add extension using V3 code: we can set the config file as NULL because we wont
reference any other sections.
*/

static int add_ext (X509 *cert, int nid, char *value)
{
   X509_EXTENSION *ex;
   X509V3_CTX ctx;

   /* This sets the 'context' of the extensions. */
   /* No configuration database */
   X509V3_set_ctx_nodb (&ctx);

   /* Issuer and subject certs: both the target since it is self signed,
      no request and no CRL */
   X509V3_set_ctx (&ctx, cert, cert, NULL, NULL, 0);
   ex = X509V3_EXT_conf_nid (NULL, &ctx, nid, value);
   if (!ex) return 0;
   
   X509_add_ext (cert, ex, -1);
   X509_EXTENSION_free (ex);

   return (1);
}
        
/*****************************************************************************/

#else /* SESOLA */

char* SesolaMkCert ()
{
   ErrorNoticed (NULL, SS$_BUGCHECK, "This is a non-SSL version.", FI_LI);
   return (NULL);
}

/*****************************************************************************/

/************************/
#endif  /* ifdef SESOLA */
/************************/

