/*****************************************************************************/ #ifdef COMENTS_WITH_COMMENTS /* acme_tls_1.c Executes as a CGI script though does not use CGI variables or provide a CGI compliant response. Performs its job via callouts. Relies on the WASD v12.3 SesolaHelloCallback() to be activated during TLS negotiation calling SesolaAcmeTls1() to check whether it is a Let's Encrypt TLS-ALPN-01 (acme-tls/1). https://letsencrypt.org/docs/challenge-types/ https://datatracker.ietf.org/doc/html/rfc8737 It relies on the WASD v12.3 and later AGENT-BEGIN:, AGENT-END: and OPAQUE: callouts to provide the agent request parameter and response. AGENT-BEGIN: 200 !OPAQUE:................ !AGENT-END: 200 success !AGENT-END: 5nn Works in concert with [SRC.HTTPD]SESOLACME.C code. Using the server name supplied by the AGENT-BEGIN: build a certificate and private key, transmit these as binary data to WASD server via OPAQUE: callout. The server then stores them in the sesola structure for use when the handsahke continues. TESTING ------- $ openssl s_client -connect :443 -servername -alpn "acme-tls/1" And WATCH [x]SSL when the ACME agent being activated, and as expected failing, with messages similar to the following: SESOLACM 0290 000003 SSL ACME acme-tls/1 AGENT /cgi-bin/acme_tls_1| SESOLANE 0403 000002 SSL BEGIN retry| SESOLACM 0340 ****** SSL ACME 500 base642bin() x86vms.dyndns.org 1 0| SESOLACM 0396 000003 SSL ACME acme-tls/1 %X0000002C (%SYSTEM-F-ABORT, abort)| This demonstrates the WASD ACME code and agent code is operating correctly. The failure and abort being down to no real LE transaction involved. Adding [x]CGI and [x]DCL provides further detail. COPYRIGHT --------- Copyright (C) 2020-2024 Mark G.Daniel This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under the conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version. http://www.gnu.org/licenses/gpl.txt /* * Copyright (C) 2019-2024 Nicola Di Lieto * * This file is part of uacme. * * uacme is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * uacme is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see * . */ VERSION LOG ----------- 18-SEP-2022 MGD initial */ #endif /* COMENTS_WITH_COMMENTS */ /*****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "wucme.h" /* basically purloined from CGILIB.C */ char* CgiPlusCallout (char *fmt, ...); void CgiPlusInGets (char*, int); void CgiPlusEOT (); void CgiPlusESC (); static char *CgiPlusEotPtr = NULL; static char *CgiPlusEscPtr = NULL; static FILE *CgiPlusInFile = NULL; #define __attribute__(stuff) #include "base64.h" static char msg [256], ServerName [256]; extern char SoftwareId[]; char* acmeTls1 (); /* purloined from the same function as in ualpn.c */ int auth_crt (const char *ident, const uint8_t *id, size_t id_len, unsigned char **crt, unsigned int *crt_len, unsigned char **key, unsigned int *key_len); /*****************************************************************************/ /* */ void acme_tls_1 () { int status; char *cptr, *sptr, *zptr; ServerName[0] = '\0'; if (!(stdout = freopen ("SYS$OUTPUT:", "w", stdout, "ctx=bin"))) exit (vaxc$errno); cptr = CgiPlusCallout ("AGENT-BEGIN: %s", SoftwareId); if (*cptr != '2') { /* if callout response not "200 ..." */ CgiPlusCallout ("!AGENT-END: 500 %s", cptr); exit (SS$_NORMAL); } /* skip over response status (e.g. "200") */ while (*cptr && isdigit(*cptr)) cptr++; while (*cptr && *cptr == ' ') cptr++; zptr = (sptr = ServerName) + sizeof(ServerName)-1; while (*cptr && *cptr != ' ' && sptr < zptr) *sptr++ = *cptr++; if (sptr >= zptr) ServerName[0] = '\0'; while (*cptr && *cptr == ' ') cptr++; if (!ServerName[0]) { /* empty parameter */ CgiPlusCallout ("!AGENT-END: 500 server name?"); exit (SS$_NORMAL); } if (cptr = acmeTls1()) CgiPlusCallout ("!AGENT-END: 500 %s", cptr); else CgiPlusCallout ("!AGENT-END: 200"); exit (SS$_NORMAL); } /*****************************************************************************/ /* */ char* acmeTls1 () { int retval, size, status; uint id_len; char *bptr, *buf, *chkey, *sptr; struct { unsigned char *data; unsigned int size; } crt = {NULL, 0}, key = {NULL, 0}; /* 32 bytes storage, 34 bytes total */ uint8_t id[0x22] = { 0x04, // OCTET_STRING 0x20, // LENGTH }; wucmeGetSyi (); chkey = wucmeChallenge (WUCME_ALPN1_TOKEN, NULL); if (chkey) { retval = base642bin (id + 2, sizeof(id) - 2, chkey, strlen(chkey), NULL, &id_len, NULL, base64_VARIANT_URLSAFE_NO_PADDING); if (retval) { sprintf (msg, "base642bin() \"%s\" %d %d", chkey, retval, id_len); return (msg); } } else retval = 1; if (retval || id_len != sizeof(id) - 2) { sprintf (msg, "base642bin() %s %d %d", ServerName, retval, id_len); return (msg); } retval = auth_crt (ServerName, id, sizeof(id), &crt.data, &crt.size, &key.data, &key.size); if (retval) { /* tack the message on any addtional openssl_error() possibly present */ if (*(sptr = msg)) { while (*sptr) sptr++; if (sptr > msg) strcat (msg, " PLUS "); sptr += 6; } sprintf (sptr, "auth_cert() %d 0x%x %d 0x%x %d", retval, crt.data, crt.size, key.data, key.size); return (msg); } size = 8 + 2 + crt.size + 2 + key.size; bptr = buf = calloc (1, size); /* construct data */ memcpy (bptr, "!OPAQUE:", 8); bptr += 8; *(ushort*)bptr = (ushort)crt.size; bptr += 2; memcpy (bptr, crt.data, crt.size); bptr += crt.size; *(ushort*)bptr = (ushort)key.size; bptr += 2; memcpy (bptr, key.data, key.size); /* transmit this to the WASD ACME code as a callout */ CgiPlusESC(); retval = fwrite ((void*)buf, size, 1, stdout); CgiPlusEOT(); if (!retval) { sprintf (msg, "fwrite() %d %d %%X%08.08X", retval, size, vaxc$errno); return (msg); } return (NULL); } /*****************************************************************************/ /* Provide a callout to WASD server. */ char* CgiPlusCallout (char *fmt, ...) { static char CalloutResponse [256]; int retval; char *cptr; char buf [256]; va_list ap; /*********/ /* begin */ /*********/ if (CgiPlusInFile == NULL) if ((CgiPlusInFile = fopen (getenv("CGIPLUSIN"), "r", "ctx=rec")) == NULL) exit (vaxc$errno); CgiPlusESC(); va_start (ap, fmt); retval = vsnprintf (buf, sizeof(buf), fmt, ap); va_end (ap); if (retval >= 0) fputs (buf, stdout); CgiPlusEOT(); if (buf[0] == '!' || buf[0] == '#') return (NULL); memset (CalloutResponse, 0, sizeof(CalloutResponse)); CgiPlusInGets (CalloutResponse, sizeof(CalloutResponse)); *(strchr (CalloutResponse, '\n')) = '\0'; return (CalloutResponse); } /*****************************************************************************/ /* Read a record (string) from the CGIPLUSIN stream. If the buffer is too small the record will be truncated, but will always be null-terminated. This function is provided to allow a CGIplus callout to read a single record response from the server. */ void CgiPlusInGets ( char *String, int SizeOfString ) { /*********/ /* begin */ /*********/ /* the CGIplus input stream should always have been opened by now! */ if (CgiPlusInFile == NULL) exit (SS$_BUGCHECK); if (fgets (String, SizeOfString, CgiPlusInFile) == NULL) exit (vaxc$errno); /* absorb intermediate empty lines that seem to be present */ if (String[0] == '\n' && !String[1]) if (fgets (String, SizeOfString, CgiPlusInFile) == NULL) exit (vaxc$errno); String[SizeOfString-1] = '\0'; } /*****************************************************************************/ /* For CGIplus output the "end-of-text" record (end of CGIplus callout). */ void CgiPlusEOT () { /*********/ /* begin */ /*********/ if (CgiPlusEotPtr == NULL) CgiPlusEotPtr = getenv("CGIPLUSEOT"); /* CGI EOT must be in a record by itself ... flush! */ fflush (stdout); fputs (CgiPlusEotPtr, stdout); fflush (stdout); } /*****************************************************************************/ /* For CGIplus output the "escape" record (start of CGIplus callout). */ void CgiPlusESC () { /*********/ /* begin */ /*********/ if (CgiPlusEscPtr == NULL) CgiPlusEscPtr = getenv("CGIPLUSESC"); /* CGI ESC must be in a record by itself ... flush! */ fflush (stdout); fputs (CgiPlusEscPtr, stdout); fflush (stdout); } /*****************************************************************************/ /* */ void openssl_error(const char *prefix) { unsigned long e; while ((e = ERR_get_error()) != 0) { sprintf (msg, "openssl %s %s", prefix, ERR_error_string(e, NULL)); return; } } /*****************************************************************************/ /* Purloined from the same function as in ualpn.c */ int auth_crt (const char *ident, const uint8_t *id, size_t id_len, unsigned char **crt, unsigned int *crt_len, unsigned char **key, unsigned int *key_len) { EVP_PKEY_CTX *pc = NULL; EVP_PKEY *k = NULL; X509_NAME *name = NULL; X509 *c = NULL; X509V3_CTX ctx; BIGNUM *bn = NULL; ASN1_OBJECT *acmeid = NULL; ASN1_OCTET_STRING *idos = NULL; X509_EXTENSION *ext = NULL; char *san = NULL; struct addrinfo *ai = NULL; time_t now = time(NULL); int ret = -1; int rc; idos = ASN1_OCTET_STRING_new(); if (!idos || !ASN1_OCTET_STRING_set(idos, id, id_len)) { openssl_error("auth_crt"); goto out; } pc = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL); if (!pc || !EVP_PKEY_keygen_init(pc) || !EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pc, NID_X9_62_prime256v1) || !EVP_PKEY_keygen(pc, &k)) { openssl_error("auth_crt"); goto out; } name = X509_NAME_new(); if (!name || !X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (const unsigned char *)ident, -1, -1, 0)) { openssl_error("auth_crt"); goto out; } bn = BN_new(); if (!bn || !BN_rand(bn, 127, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY)) { openssl_error("auth_crt"); goto out; } c = X509_new(); if (!c || !X509_set_version(c, 2) || !X509_set_subject_name(c, name) || !X509_set_issuer_name(c, name) || !BN_to_ASN1_INTEGER(bn, X509_get_serialNumber(c)) || !ASN1_TIME_adj(X509_getm_notBefore(c), now, -30, 0) || !ASN1_TIME_adj(X509_getm_notAfter(c), now, 30, 0) || !X509_set_pubkey(c, k)) { openssl_error("auth_crt"); goto out; } #ifdef IS_WUCME if (asprintf(&san, "DNS:%s", ident) < 0) { warnx("auth_crt: asprintf failed"); san = NULL; goto out; } #else rc = parse_addr(ident, AI_NUMERICHOST | AI_NUMERICSERV, AF_UNSPEC, &ai); if (rc == 0 && (ai->ai_family == AF_INET || ai->ai_family == AF_INET6)) { freeaddrinfo(ai); if (asprintf(&san, "IP:%s", ident) < 0) { warnx("auth_crt: asprintf failed"); san = NULL; goto out; } } else { if (rc == 0) freeaddrinfo(ai); if (asprintf(&san, "DNS:%s", ident) < 0) { warnx("auth_crt: asprintf failed"); san = NULL; goto out; } } #endif /* IS_WUCME */ acmeid = OBJ_txt2obj("1.3.6.1.5.5.7.1.31",1); if (!acmeid) { openssl_error("auth_crt"); goto out; } X509V3_set_ctx_nodb(&ctx); X509V3_set_ctx(&ctx, c, c, NULL, NULL, 0); ext = X509V3_EXT_conf_nid(NULL, &ctx, NID_subject_alt_name, san); if (!ext || !X509_add_ext(c, ext, -1)) { openssl_error("auth_crt"); goto out; } X509_EXTENSION_free(ext); ext = X509V3_EXT_conf_nid(NULL, &ctx, NID_key_usage, "critical, keyCertSign, digitalSignature"); if (!ext || !X509_add_ext(c, ext, -1)) { openssl_error("auth_crt"); goto out; } X509_EXTENSION_free(ext); ext = X509V3_EXT_conf_nid(NULL, &ctx, NID_basic_constraints, "critical,CA:TRUE"); if (!ext || !X509_add_ext(c, ext, -1)) { openssl_error("auth_crt"); goto out; } X509_EXTENSION_free(ext); ext = X509V3_EXT_conf_nid(NULL, &ctx, NID_subject_key_identifier, "hash"); if (!ext || !X509_add_ext(c, ext, -1)) { openssl_error("auth_crt"); goto out; } X509_EXTENSION_free(ext); ext = X509V3_EXT_conf_nid(NULL, &ctx, NID_authority_key_identifier, "keyid,issuer"); if (!ext || !X509_add_ext(c, ext, -1)) { openssl_error("auth_crt"); goto out; } X509_EXTENSION_free(ext); ext = X509_EXTENSION_create_by_OBJ(NULL, acmeid, 1, idos); if (!ext || !X509_add_ext(c, ext, -1)) { openssl_error("auth_crt"); goto out; } if (!X509_sign(c, k, EVP_sha256())) { openssl_error("auth_crt"); goto out; } *crt = NULL; rc = i2d_X509(c, crt); if (rc < 0) { openssl_error("auth_crt"); goto out; } *crt_len = rc; *key = NULL; rc = i2d_PrivateKey(k, key); if (rc < 0) { openssl_error("auth_crt"); goto out; } *key_len = rc; ret = 0; out: EVP_PKEY_CTX_free(pc); EVP_PKEY_free(k); X509_NAME_free(name); X509_free(c); BN_free(bn); ASN1_OBJECT_free(acmeid); ASN1_OCTET_STRING_free(idos); X509_EXTENSION_free(ext); free(san); if (ret != 0) { OPENSSL_free(*key); *key = NULL; *key_len = 0; OPENSSL_free(*crt); *crt = NULL; *crt_len = 0; } return ret; } /*****************************************************************************/