/*****************************************************************************/ /* SesolaClient.c Secure Sockets Layer client (peer) certificate. VERSION HISTORY --------------- 10-AUG-2020 MGD SesolaClientCert() propagate connect cert into client cert SesolaClientCertEnd() both HTTP/1.n and HTTP/2 17-MAY-2020 MGD bugfix; SesolaClientCertGet() SSL_VERIFY_POST_HANDSHAKE 19-JUN-2019 MGD bugfix; SesolaClientCertGet() status 0 an issue bugfix; SesolaClientCertGet() if (value <= 0) break; 17-APR-2019 MGD bugfix; SesolaClientCert() allow pattern per 25-AUG-2015 10-OCT-2018 MGD SesolaClientCertGet() retrieves the client certificate by renegotiation for TLSv1.2 and earlier, or post handshake for TVSv1.3 and later 27-JUL-2018 MGD bugfix; X509_free() memory leak with ->ClientCertPtr 26-JAN-2018 MGD SesolaClientCertUserName() 10-JUN-2017 MGD SesolaClientCertRenegotiate() allow for pre- and post- OpenSSL 1.1.0 due to MSIE11 (Edge) stalling on a read after renegotiation (pre reverts to v11.0 and earlier code) bugfix; SesolaClientCertConditional() 'IS' processing bugfix; SesolaClientCertRenegotiate() allow for low-level (i.e. SSL) I/O errors (e.g. link disconnection) 23-JUL-2016 MGD SesolaClientCertRenegotiate() rework due to OpenSSL v1.1.0 08-JUL-2016 MGD bugfix; SesolaClientCert() move X509 RENEGOTIATE switch HTTP/2 to HTTP/1.1 after SSL_get_peer_certificate() 05-JUN-2016 MGD bugfix; SesolaClientCertRenegotiate() ensure application data is cleared before renegotiate initiated 11-MAY-2016 MGD bugfix; SesolaClientCert() just return status 20-DEC-2015 MGD SesolaClientCertMetaCon() 27-SEP-2015 MGD SesolaClientCert() if SesolaCertParseDn() does not return the user DN record then try SesolaCertExtension() SesolaClientCert() display client certificate extensions 25-AUG-2015 MGD [ru:/CN=] allows multiple to be selected between (e.g. "[ru:/CN=user*]", "[ru:/CN=^^\[^/=\]*$]" ) [ru:..] escape characters using '\' (especially ']') 07-JUL-2013 MGD SesolaWatchErrors() during renegotiation 21-AUG-2004 MGD significant refinements to SSL processing 22-JUL-2004 MGD SesolaClientCert() call RequestEnd() instead of SesolaNetSesolaClientCert() on network read/write error 14-JAN-2003 MGD DN record /email and /emailAddress 28-AUG-2002 MGD add SHA1 fingerprint (everybody else has it ;^) 07-APR-2002 MGD bugfix; SesolaClientCert() call SesolaNetRequestEnd() after network error for v8.0 SesolaNet..() support 28-FEB-2002 MGD bugfix; SesolaRenegotiateClientCert() reset SSL state to SSL_ST_OK if renegotiation fails 21-OCT-2001 MGD rework SESOLA.C */ /*****************************************************************************/ #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 #include #include #include /* VMS related header files */ #include #include /* application header files */ #define SESOLA_REQUIRED #include "Sesola.h" #define WASD_MODULE "SESOLACLIENT" /***************************************/ #ifdef SESOLA /* secure sockets layer */ /***************************************/ /********************/ /* external storage */ /********************/ extern int ExitStatus, OpcomMessages, SesolaVerifyPeerDataMax; extern char ErrorSanityCheck[], SoftwareID[]; extern uchar SesolaSessionId[16]; extern WATCH_STRUCT Watch; /*****************************************************************************/ /* This function is used for two purposes, authorization via a client certificate, and just getting the client certificate (for reports, etc.) Get the client certificate associated with the request. If none is available on the first call then initiate an SSL renegotiation with the client to supply one. If not authorizing using this certificate then just call the original AST to return the certificate via 'rqptr->NetIoPtr->SesolaPtr->ClientCertPtr' (or of course no certificate if the pointer is NULL). When authorizing (SESOLA_VERIFY_PEER_AUTH) and a certificate is available (either on first call, i.e. session cached, or after renegotiation) then get a fingerprint of the certificate that can be used to identify the client user. Setting 'VerifyParam' to SESOLA_VERIFY_PEER_NONE (aka SSL_VERIFY_NONE) can be used to "logout" the client from it's current authorization, allowing another certficiate to be selected and used (via "?httpd=logout"). If a certificate is available this function generates all the appropriate authorization, user detail and certificate information. Values that can be passed via the 'VerifyMode' argument. SESOLA_VERIFY_PEER_AUTH verify the cert, fail, use for WASD authentication SESOLA_VERIFY_PEER_NONE renegotiate without peer verification SESOLA_VERIFY_PEER_OPTIONAL get and verify the certificate (continue on fail) SESOLA_VERIFY_PEER_REQUIRED abort the connection if the cert does not verify */ int SesolaClientCert ( REQUEST_STRUCT *rqptr, int VerifyParam, REQUEST_AST AstFunction ) { int status, number, value, version, SessionHits, SessionTimeout, SessionTimeCSec, SessionTimeoutCSec, VerifyMode, WatchThisType; char *aptr, *cptr, *sptr, *zptr, *CurrentPtr, *DigestTypePtr; char AuthFingerprint [64], CertFingerprintMD5 [64], CertFingerprintSHA1 [64], CertFingerprintSHA256 [128], CertIssuer [512], CertSubject [512], String [256], TimeString [32], TimeoutString [32], UserDetails [256]; SESOLA_STRUCT *sesolaptr; SSL_SESSION *SessionPtr; struct tm *tmptr; /*********/ /* begin */ /*********/ if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SesolaClientCert() !&A !UL", AstFunction, VerifyParam); /* network errors? */ if (rqptr->NetIoPtr->ReadStatus && VMSnok (rqptr->NetIoPtr->ReadStatus)) return (rqptr->NetIoPtr->ReadStatus); if (rqptr->NetIoPtr->WriteStatus && VMSnok (rqptr->NetIoPtr->WriteStatus)) return (rqptr->NetIoPtr->WriteStatus); if (HTTP2_REQUEST(rqptr)) sesolaptr = rqptr->Http2Stream.Http2Ptr->NetIoPtr->SesolaPtr; else sesolaptr = (SESOLA_STRUCT*)rqptr->NetIoPtr->SesolaPtr; version = SSL_version (sesolaptr->SslPtr); SessionPtr = SSL_get_session (sesolaptr->SslPtr); if (VerifyParam != SESOLA_VERIFY_AST) { /* initial call, not AST delivery - must have an AST address */ if (!AstFunction) ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI); /* set the peer verification type */ sesolaptr->CertVerifyMode = VerifyParam; } /* mask off any WASD-specific bits */ VerifyMode = sesolaptr->CertVerifyMode & SESOLA_VERIFY_PEER_MASK; if (sesolaptr->CertVerifyMode == SESOLA_VERIFY_PEER_AUTH) { /*****************/ /* authorization */ /*****************/ /* Check for directives about how to make the verification. We must do this every time, whether or not an actual renegotiation will take place, to set things like session timeouts, etc. I've tried to make it as efficient as possible. */ if ((cptr = rqptr->rqAuth.PathParameterPtr)[0]) { while (*cptr) { while (*cptr && *cptr != '[') { if (cptr[0] == '\\' && cptr[1]) cptr++; if (*cptr) cptr++; } if (!*cptr) break; switch (*(ULONGPTR)cptr) { case '[dp:' : case '[DP:' : /*****************************/ /* set CA verification depth */ /*****************************/ cptr += sizeof(unsigned long); if (isdigit (*cptr) || *cptr == '-') { number = atoi(cptr); SSL_set_verify_depth (sesolaptr->SslPtr, number); } break; case '[lt:' : case '[LT:' : /************************/ /* set session lifetime */ /************************/ cptr += sizeof(unsigned long); if (MATCH4 (cptr, "expi") || MATCH4 (cptr, "EXPI")) sesolaptr->SessionTimeoutMinutes = -1; else if (isdigit (*cptr) || *cptr == '-') sesolaptr->SessionLifetimeMinutes = atoi(cptr); break; case '[ru:' : case '[RU:' : /***************************/ /* source or 'remote-user' */ /***************************/ zptr = (sptr = sesolaptr->X509RemoteUserDnRecord) + sizeof(sesolaptr->X509RemoteUserDnRecord)-1; cptr += 4; while (*cptr && !ISLWS(*cptr) && *cptr != ']' && sptr < zptr) { if (cptr[0] == '\\' && cptr[1]) cptr++; *sptr++ = *cptr++; } *sptr = '\0'; break; case '[to:' : case '[TO:' : /***********************/ /* set session timeout */ /***********************/ cptr += sizeof(unsigned long); if (MATCH4 (cptr, "expi") || MATCH4 (cptr, "EXPI")) sesolaptr->SessionTimeoutMinutes = -1; else if (isdigit (*cptr) || *cptr == '-') sesolaptr->SessionTimeoutMinutes = atoi(cptr); break; case '[vf:' : case '[VF:' : /************************************/ /* type of client cert verification */ /************************************/ cptr += sizeof(unsigned long); switch (*(ULONGPTR)cptr) { case 'none' : case 'NONE' : /* [VF:NONE] */ VerifyMode = SSL_VERIFY_NONE; break; case 'opti' : case 'OPTI' : /* [VF:OPTIONAL] */ sesolaptr->X509optionalNoCa = true; VerifyMode = SSL_VERIFY_PEER; break; case 'requ' : case 'REQU' : /* [VF:REQUIRED] */ VerifyMode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; break; } #if SESOLA_SINCE_111 if (version >= TLS1_3_VERSION) VerifyMode |= SSL_VERIFY_POST_HANDSHAKE; #endif break; default : /* encountered some other directive, note it's location */ sesolaptr->X509ConditionalPtr = cptr; /* this *must* be done after the notation! */ cptr += sizeof(unsigned long); } } } } /* if the rule wants it pre-expired then ignore any cached certificate */ if (sesolaptr->SessionTimeoutMinutes < 0 && VerifyParam != SESOLA_VERIFY_AST) { if (sesolaptr->ClientCertPtr) X509_free (sesolaptr->ClientCertPtr); sesolaptr->ClientCertPtr = NULL; } else if (sesolaptr->CertVerifyMode == SESOLA_VERIFY_PEER_NONE) { if (sesolaptr->ClientCertPtr) X509_free (sesolaptr->ClientCertPtr); sesolaptr->ClientCertPtr = NULL; } else if (!sesolaptr->ClientCertPtr) sesolaptr->ClientCertPtr = SSL_get_peer_certificate (sesolaptr->SslPtr); if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "->ClientCertPtr !&X", sesolaptr->ClientCertPtr); if (!sesolaptr->ClientCertPtr) { /***********************************************/ /* no client certficiate (currently) available */ /***********************************************/ if (HTTP2_REQUEST(rqptr)) { /* RFC7540 9.2.1 */ if (WATCHPNT(rqptr) && (WATCH_CATEGORY(WATCH_HTTP2) || WATCH_CATEGORY(WATCH_SESOLA) || WATCH_CATEGORY(WATCH_AUTH))) WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "X509 RENEGOTIATE switch HTTP/2 to HTTP/1.1"); Http2Error (rqptr->Http2Stream.Http2Ptr, 0, HTTP2_ERROR_HTTP11); rqptr->rqResponse.HttpStatus = 101; return (SS$_ABORT); } if (VerifyParam == SESOLA_VERIFY_AST) { /* call from SesolaClientCertGet(), no certificate! */ if (sesolaptr->CertVerifyMode == SESOLA_VERIFY_PEER_AUTH) { /*****************/ /* authorization */ /*****************/ /* no certificate - no authentication! */ rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_LOGIN; } /* called as an AST, therefore call the original AST address */ SysDclAst (sesolaptr->ClientCertAstFunction, rqptr); return (SS$_NORMAL); } /* let's renegotiate with the client, trying to get a certificate */ sesolaptr->ClientCertAstFunction = AstFunction; sesolaptr->CertVerifyCallbackCount = 0; SSL_set_verify (sesolaptr->SslPtr, VerifyMode, &SesolaCertVerifyCallback); /* provide the request pointer for the verify callback */ SSL_set_ex_data (sesolaptr->SslPtr, 0, sesolaptr); SesolaClientCertGet (sesolaptr); if (sesolaptr->CertVerifyMode == SESOLA_VERIFY_PEER_AUTH) { /*****************/ /* authorization */ /*****************/ /* returning AUTH_PENDING, activate authorization AST function */ rqptr->rqAuth.AstFunction = rqptr->rqAuth.AstFunctionBuffer; rqptr->rqAuth.FinalStatus = AUTH_PENDING; } /* the faux "waiting for user labels" status indicates renegotiation */ return (SS$_WAITUSRLBL); } /*************************************/ /* yes, we have a client certificate */ /*************************************/ /* if a [LT:integer] was set earlier then now's the time to apply it */ if (sesolaptr->SessionLifetimeMinutes) { /* Hmmm, not sure if this is the absolutely best thing to do! Now that we've got a certificate make it a little easier on the client by extending the session timeout so the user will not need to respecify the certificate too often provided the session is continually used (updated each request by resetting the session timestamp). */ SSL_SESSION_set_time (SessionPtr, time(NULL)); } /* a [TO:integer] value will override a [LT:integer] one */ if (sesolaptr->SessionTimeoutMinutes) SSL_SESSION_set_timeout (SessionPtr, sesolaptr->SessionTimeoutMinutes*60); else if (sesolaptr->SessionLifetimeMinutes) SSL_SESSION_set_timeout (SessionPtr, sesolaptr->SessionLifetimeMinutes*60); WatchThisType = 0; if (WATCH_CAT && WATCHPNT(rqptr)) { if WATCH_CATEGORY(WATCH_SESOLA) WatchThisType = WATCH_SESOLA; else if WATCH_CATEGORY(WATCH_AUTH) WatchThisType = WATCH_AUTH; } if (WatchThisType || sesolaptr->CertVerifyMode == SESOLA_VERIFY_PEER_AUTH) { X509_NAME_oneline (X509_get_issuer_name(sesolaptr->ClientCertPtr), CertIssuer, sizeof(CertIssuer)); X509_NAME_oneline (X509_get_subject_name(sesolaptr->ClientCertPtr), CertSubject, sizeof(CertSubject)); DigestTypePtr = SesolaCertFingerprint (sesolaptr->ClientCertPtr, &EVP_md5, CertFingerprintMD5, sizeof(CertFingerprintMD5)); if (*DigestTypePtr) DigestTypePtr = SesolaCertFingerprint (sesolaptr->ClientCertPtr, &EVP_sha1, CertFingerprintSHA1, sizeof(CertFingerprintSHA1)); } if (WATCH_CAT && WatchThisType) { SesolaCertFingerprint (sesolaptr->ClientCertPtr, &EVP_sha256, CertFingerprintSHA256, sizeof(CertFingerprintSHA256)); zptr = (sptr = AuthFingerprint) + sizeof(AuthFingerprint)-1; for (cptr = CertFingerprintMD5; *cptr; cptr++) if (*cptr != ':') *sptr++ = *cptr; *sptr = '\0'; SessionTimeCSec = SSL_SESSION_get_time (SessionPtr); SessionTimeout = SSL_SESSION_get_timeout (SessionPtr); SessionTimeoutCSec = SessionTimeCSec + SessionTimeout; tmptr = localtime (&SessionTimeCSec); if (!strftime (TimeString, sizeof(TimeString), "%b %d %T %Y", tmptr)) strcpy (TimeString, "strftime() error"); tmptr = localtime (&SessionTimeoutCSec); if (!strftime (TimeoutString, sizeof(TimeoutString), "%b %d %T %Y", tmptr)) strcpy (TimeoutString, "strftime() error"); SessionHits = SSL_CTX_sess_hits (sesolaptr->SslCtx); WatchThis (WATCHITM(rqptr), WatchThisType, "X509 client certificate"); WatchDataFormatted ("ISSUER: !AZ\n", CertIssuer); WatchDataFormatted ("SUBJECT: !AZ\n", CertSubject); SesolaCertExtension (sesolaptr->ClientCertPtr, (char*)-1); while (cptr = SesolaCertExtension (NULL, NULL)) WatchDataFormatted ("EXTENSION: !AZ\n", cptr); WatchDataFormatted ("SHA256: !AZ\n", CertFingerprintSHA256); WatchDataFormatted ("SHA1: !AZ\n", CertFingerprintSHA1); WatchDataFormatted ("MD5: !AZ\n", CertFingerprintMD5); WatchDataFormatted ("FINGERPRINT: !AZ \n", AuthFingerprint); WatchDataFormatted ("SESSION: !UL hit!%s since !AZ, timeout (!SL) at !AZ\n", SessionHits, TimeString, SessionTimeout / 60, TimeoutString); } if (sesolaptr->CertVerifyMode != SESOLA_VERIFY_PEER_AUTH) { if (VerifyParam == SESOLA_VERIFY_AST) { /* call from SesolaClientCertGet() */ SysDclAst (sesolaptr->ClientCertAstFunction, rqptr); } return (SS$_NORMAL); } /*****************/ /* authorization */ /*****************/ if (sesolaptr->X509optionalNoCa) SSL_set_verify_result (sesolaptr->SslPtr, X509_V_OK); if (SSL_get_verify_result (sesolaptr->SslPtr) != X509_V_OK) { rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_LOGIN; if (VerifyParam == SESOLA_VERIFY_AST) { /* call from SesolaClientCertGet() */ SysDclAst (sesolaptr->ClientCertAstFunction, rqptr); } /* cancel the cached session by adjusting the timeout backwards */ SSL_SESSION_set_timeout (SessionPtr, -1); return (SS$_NORMAL); } if (!*DigestTypePtr) { /* hmmm, problem in generating the fingerprint */ rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER; if (VerifyParam == SESOLA_VERIFY_AST) { /* call from SesolaClientCertGet() */ SysDclAst (sesolaptr->ClientCertAstFunction, rqptr); } /* cancel the cached session by adjusting the timeout backwards */ SSL_SESSION_set_timeout (SessionPtr, -1); return (SS$_NORMAL); } /******************/ /* authenticated! */ /******************/ /* X509 authentication, full r+w access implied */ rqptr->rqAuth.UserCan = AUTH_READWRITE_ACCESS; rqptr->rqAuth.FinalStatus = SS$_NORMAL; rqptr->rqAuth.ClientCertIssuerLength = strlen(CertIssuer); rqptr->rqAuth.ClientCertIssuerPtr = VmGetHeap (rqptr, rqptr->rqAuth.ClientCertIssuerLength+1); strcpy (rqptr->rqAuth.ClientCertIssuerPtr, CertIssuer); rqptr->rqAuth.ClientCertSubjectLength = strlen(CertSubject); rqptr->rqAuth.ClientCertSubjectPtr = VmGetHeap (rqptr, rqptr->rqAuth.ClientCertSubjectLength+1); strcpy (rqptr->rqAuth.ClientCertSubjectPtr, CertSubject); /* if a conditional was detected during an earlier phase */ if (sesolaptr->X509ConditionalPtr) { if (!SesolaClientCertConditional (rqptr, sesolaptr->X509ConditionalPtr)) { rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER; if (VerifyParam == SESOLA_VERIFY_AST) { /* call from SesolaClientCertGet() */ SysDclAst (sesolaptr->ClientCertAstFunction, rqptr); } /* cancel the cached session by adjusting the timeout backwards */ SSL_SESSION_set_timeout (SessionPtr, -1); return (SS$_NORMAL); } } /* derive the user details from the /CN and /EMAIL of the subject */ zptr = (sptr = UserDetails) + sizeof(UserDetails)-1; cptr = SesolaCertParseDn (rqptr->rqAuth.ClientCertSubjectPtr, "/CN="); if (cptr) { while (*cptr && *cptr != '=') cptr++; if (*cptr) cptr++; while (*cptr && sptr < zptr) *sptr++ = *cptr++; } cptr = SesolaCertParseDn (rqptr->rqAuth.ClientCertSubjectPtr, "/Email="); if (!cptr) cptr = SesolaCertParseDn (rqptr->rqAuth.ClientCertSubjectPtr, "/emailAddress="); if (cptr) { while (*cptr && *cptr != '=') cptr++; if (*cptr) { cptr++; if (sptr < zptr) *sptr++ = ','; if (sptr < zptr) *sptr++ = ' '; while (*cptr && sptr < zptr) *sptr++ = *cptr++; } } *sptr = '\0'; rqptr->rqAuth.UserDetailsLength = sptr - UserDetails; rqptr->rqAuth.UserDetailsPtr = VmGetHeap (rqptr, rqptr->rqAuth.UserDetailsLength+1); strcpy (rqptr->rqAuth.UserDetailsPtr, UserDetails); rqptr->rqAuth.ClientCertFingerprintLength = strlen(CertFingerprintMD5); rqptr->rqAuth.ClientCertFingerprintPtr = VmGetHeap (rqptr, rqptr->rqAuth.ClientCertFingerprintLength+1); strcpy (rqptr->rqAuth.ClientCertFingerprintPtr, CertFingerprintMD5); zptr = (sptr = rqptr->RemoteUser) + sizeof(rqptr->RemoteUser)-1; if (sesolaptr->X509RemoteUserDnRecord[0]) { cptr = SesolaCertParseDn (rqptr->rqAuth.ClientCertSubjectPtr, sesolaptr->X509RemoteUserDnRecord); if (!cptr) cptr = SesolaCertName (sesolaptr->ClientCertPtr, sesolaptr->X509RemoteUserDnRecord); if (!cptr) cptr = SesolaCertExtension (sesolaptr->ClientCertPtr, sesolaptr->X509RemoteUserDnRecord); if (cptr) { /* check for wildcard pattern */ for (aptr = sesolaptr->X509RemoteUserDnRecord; *aptr && *aptr != '*' && *aptr != '^'; aptr++); /* if something like (DTAG) /CN=User-Id* */ if (*aptr) while (*cptr && *cptr != '=') cptr++; else for (aptr = sesolaptr->X509RemoteUserDnRecord; *aptr && *cptr; aptr++, cptr++); while (*cptr && *cptr != '_' && !isalnum(*cptr)) cptr++; while (*cptr && sptr < zptr) { /* convert white-space to underscores */ if (ISLWS(*cptr)) *sptr++ = '_'; else *sptr++ = *cptr; cptr++; } } } else { /* "remote user name" is derived from fingerprint without the colons */ cptr = rqptr->rqAuth.ClientCertFingerprintPtr; while (*cptr && sptr < zptr) { if (*cptr == ':') cptr++; else *sptr++ = *cptr++; } } /* overflow does not truncate, it empties (does not authorize)!! */ if (sptr >= zptr) { *sptr = '\0'; if (WATCH_CAT && WatchThisType) WatchThis (WATCHITM(rqptr), WatchThisType, "X509 overflow at !UL bytes REMOTE_USER !AZ", sizeof(rqptr->RemoteUser)-1, rqptr->RemoteUser); sptr = rqptr->RemoteUser; } *sptr = '\0'; rqptr->RemoteUserLength = sptr - rqptr->RemoteUser; if (rqptr->RemoteUserLength) strcpy (rqptr->RemoteUserPassword, "anystringwilldo"); if (WATCH_CAT && WatchThisType) WatchThis (WATCHITM(rqptr), WatchThisType, "X509 client certificate REMOTE_USER !AZ", rqptr->RemoteUser[0] ? rqptr->RemoteUser : "(none)"); if (VerifyParam == SESOLA_VERIFY_AST) { /* call from SesolaClientCertGet() */ SysDclAst (sesolaptr->ClientCertAstFunction, rqptr); } return (SS$_NORMAL); } /*****************************************************************************/ /* Called by RequestEnd2() to dispose of any client certificate reference. */ void SesolaClientCertEnd (REQUEST_STRUCT *rqptr) { HTTP2_STRUCT *h2ptr; SESOLA_STRUCT *sesolaptr; /*********/ /* begin */ /*********/ if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaClientCertEnd()"); if (!(sesolaptr = (SESOLA_STRUCT*)rqptr->NetIoPtr->SesolaPtr)) { if (!(h2ptr = rqptr->Http2Stream.Http2Ptr)) return; if (!(sesolaptr = (SESOLA_STRUCT*)h2ptr->NetIoPtr->SesolaPtr)) return; } if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "->ClientCertPtr !8XL", sesolaptr->ClientCertPtr); if (!sesolaptr->ClientCertPtr) return; X509_free (sesolaptr->ClientCertPtr); sesolaptr->ClientCertPtr = NULL; } /*****************************************************************************/ /* Scan the authorization parameter string evaluating the conditions! Return true or false. Anything it cannot understand it ignores! (and yes, it does look a little like MapUrl_Conditional() :^) */ BOOL SesolaClientCertConditional ( REQUEST_STRUCT *rqptr, char *ConditionalPtr ) { BOOL NegateThisCondition, NegateEntireConditional, Result, SoFarSoGood, WatchThisOne; int AlgKeySize, ConditionalCount, MinKeySize, UseKeySize; char *cptr, *csptr, *sptr, *zptr, *CipherNamePtr, *CurrentPtr, *VersionNamePtr; char Scratch [AUTH_MAX_PATH_PARAM_LENGTH+1]; SESOLA_STRUCT *sesolaptr; SSL_CIPHER *CipherPtr; /*********/ /* begin */ /*********/ if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SesolaClientCertConditional() !&Z\n", ConditionalPtr); sesolaptr = (SESOLA_STRUCT*)rqptr->NetIoPtr->SesolaPtr; if (WATCHPNT(rqptr) && (WATCH_CATEGORY(WATCH_SESOLA) || WATCH_CATEGORY(WATCH_AUTH))) WatchThisOne = true; else WatchThisOne = false; CurrentPtr = NULL; ConditionalCount = 0; NegateEntireConditional = NegateThisCondition = SoFarSoGood = false; cptr = ConditionalPtr; while (*cptr) { while (ISLWS(*cptr)) cptr++; if (!*cptr) break; if (*cptr == '[' || SAME2(cptr,'![')) { CurrentPtr = cptr; if (*cptr == '!') { NegateEntireConditional = true; cptr++; } else NegateEntireConditional = false; cptr++; ConditionalCount = 0; SoFarSoGood = false; continue; } if (*cptr == ']') { cptr++; if (NegateEntireConditional) { SoFarSoGood = !SoFarSoGood; NegateEntireConditional = false; } if (ConditionalCount && !SoFarSoGood) { cptr = ""; break; } continue; } if (SoFarSoGood) { if (NegateEntireConditional) { SoFarSoGood = !SoFarSoGood; NegateEntireConditional = false; } /* at least one OK, skip to the end of the conditional */ while (*cptr && *cptr != ']') cptr++; if (!*cptr) break; } if (!CurrentPtr) CurrentPtr = cptr; NegateThisCondition = Result = false; zptr = (sptr = Scratch) + sizeof(Scratch)-1; if (*cptr == '!') { cptr++; NegateThisCondition = true; } switch (*(USHORTPTR)cptr) { case 'ci' : case 'CI' : /***************/ /* Cipher Name */ /***************/ ConditionalCount++; cptr += 3; while (*cptr && !ISLWS(*cptr) && *cptr != ']' && sptr < zptr) { if (cptr[0] == '\\' && cptr[1]) cptr++; if (*cptr) *sptr++ = *cptr++; } *sptr = '\0'; if (!CipherPtr) CipherPtr = SSL_get_current_cipher (sesolaptr->SslPtr); if (!CipherPtr) { CipherNamePtr = ""; AlgKeySize = UseKeySize = 0; } if (!CipherNamePtr) CipherNamePtr = (char*)SSL_CIPHER_get_name (CipherPtr); if (!CipherNamePtr) CipherNamePtr = ""; Result = StringMatch (rqptr, CipherNamePtr, Scratch); break; case 'is' : case 'IS' : /******************/ /* Cert Issuer DN */ /******************/ ConditionalCount++; cptr += 3; while (*cptr && !ISLWS(*cptr) && *cptr != ']' && sptr < zptr) { if (cptr[0] == '\\' && cptr[1]) cptr++; if (*cptr) *sptr++ = *cptr++; } *sptr = '\0'; csptr = rqptr->rqAuth.ClientCertIssuerPtr; /* if it begins with DN record name then confine to that record */ if (*(sptr = Scratch) == '/') { /* must begin with something like "/CN=string" */ if (!SesolaCertParseDn (csptr, sptr)) Result = false; else { /* found the (example) "/CN=", skip over BOTH */ while (*sptr && *sptr != '=') sptr++; if (*sptr) sptr++; while (*csptr && *csptr != '=') csptr++; if (*csptr) csptr++; /* now search only the returned DN record value */ Result = StringMatch (rqptr, csptr, sptr); } } else { /* search the entire DN */ Result = StringMatch (rqptr, csptr, sptr); } break; case 'ks' : case 'KS' : /*****************/ /* User Key Size */ /*****************/ ConditionalCount++; cptr += 3; while (*cptr && !ISLWS(*cptr) && *cptr != ']' && sptr < zptr) { if (cptr[0] == '\\' && cptr[1]) cptr++; if (*cptr) *sptr++ = *cptr++; } *sptr = '\0'; MinKeySize = atoi(Scratch); if (MinKeySize < 0) MinKeySize = 0; if (!CipherPtr) CipherPtr = SSL_get_current_cipher (sesolaptr->SslPtr); if (!CipherPtr) UseKeySize = 0; else UseKeySize = SSL_CIPHER_get_bits(CipherPtr, &AlgKeySize); Result = UseKeySize >= MinKeySize; break; case 'su' : case 'SU' : /*******************/ /* Cert Subject DN */ /*******************/ ConditionalCount++; cptr += 3; while (*cptr && !ISLWS(*cptr) && *cptr != ']' && sptr < zptr) { if (cptr[0] == '\\' && cptr[1]) cptr++; if (*cptr) *sptr++ = *cptr++; } *sptr = '\0'; csptr = rqptr->rqAuth.ClientCertSubjectPtr; /* if it begins with DN record name then confine to that record */ if (*(sptr = Scratch) == '/') { /* must begin with something like "/CN=string" */ if (!(csptr = SesolaCertParseDn (csptr, sptr))) Result = false; else { /* found the (example) "/CN=", skip over BOTH */ while (*sptr && *sptr != '=') sptr++; if (*sptr) sptr++; while (*csptr && *csptr != '=') csptr++; if (*csptr) csptr++; /* now search only the returned DN record value */ Result = StringMatch (rqptr, csptr, sptr); } } else { /* search the entire DN */ Result = StringMatch (rqptr, csptr, sptr); } break; default : /***********************************/ /* unknown (or 'VF', etc.), ignore */ /***********************************/ if (WATCH_CAT && WatchThisOne) WatchDataFormatted ("IGNORE !AZ\n", CurrentPtr); while (*cptr && !ISLWS(*cptr) && *cptr != ']') { if (cptr[0] == '\\' && cptr[1]) cptr++; if (*cptr) cptr++; } continue; } if (NegateThisCondition) SoFarSoGood = SoFarSoGood || !Result; else SoFarSoGood = SoFarSoGood || Result; if (WATCH_CAT && WatchThisOne) WatchDataFormatted ("!AZ !AZ\n", SoFarSoGood ? "PASS" : "FAIL", CurrentPtr); CurrentPtr = NULL; } if (!ConditionalCount) SoFarSoGood = true; else if (NegateEntireConditional) SoFarSoGood = !SoFarSoGood; if (WATCH_CAT && WatchThisOne) WatchDataFormatted ("!AZ conditional\n", SoFarSoGood ? "PASSED" : "FAILED"); return (SoFarSoGood); } /*****************************************************************************/ /* Get an X509 certificate from the client, or try to anyway. With TLSv1.2 and earlier: Initiate an SSL renegotiate to give the client an opportunity to supply a client certificate. With TLSv1.3 (OpenSSL 1.1.1 and later): Renegotation is no longer available and it is done by having the service enable the TLSv1.3 post handshake authentication extension that provides a client certificate. Due to the non-blocking I/O used by WASD this function will be called multiple times to complete the handshake. Ensure pending request data (typically from a PROPFIND, PUT or POST) is cleared before attempting to renegotiate. As there is lots of negotiation going on underneath all the asynchronous I/O in this behaviour some fine-grained inspection of the connection state is needed. Essentially at each handshake I/O the state is checked for client certificate exchange. This sets a flag and the next "SSL negotiation finished successfully" message considered certificate negotiation concluded. */ void SesolaClientCertGet (SESOLA_STRUCT *sesolaptr) { static char buf[1]; int sane, state, value, verify, version; char *cptr; REQUEST_STRUCT *rqptr; /*********/ /* begin */ /*********/ rqptr = sesolaptr->RequestPtr; if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SesolaClientCertGet() !&B !&B", sesolaptr->SslStateFunction == &SesolaClientCertGet, sesolaptr->ReadClientCert); if (sesolaptr->VerifyPeerDataSize && sesolaptr->VerifyPeerDataCount < sesolaptr->VerifyPeerDataSize) { if ((value = SesolaClientRequestData (sesolaptr)) < 0) { SesolaWatchErrors (sesolaptr); SesolaClientCert (rqptr, SESOLA_VERIFY_AST, NULL); return; } /* if waiting on I/O */ if (value == 0) return; } version = SSL_version (sesolaptr->SslPtr); if (sesolaptr->SslStateFunction != &SesolaClientCertGet) { /*********/ /* start */ /*********/ if (rqptr->rqHeader.ContentLength64) { if ((value = SesolaClientRequestData (sesolaptr)) < 0) { SesolaWatchErrors (sesolaptr); SesolaClientCert (rqptr, SESOLA_VERIFY_AST, NULL); return; } /* if waiting on I/O */ if (value == 0) return; } if (WATCHING (rqptr, WATCH_SESOLA)) { verify = SSL_get_verify_mode (sesolaptr->SslPtr); switch (verify) { case SSL_VERIFY_NONE : cptr = "NONE"; break; case SSL_VERIFY_PEER : cptr = "OPTIONAL"; break; case (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) : cptr = "REQUIRED"; break; #if SESOLA_SINCE_111 case (SSL_VERIFY_PEER | SSL_VERIFY_POST_HANDSHAKE) : cptr = "OPTIONAL-POST_HANDSHAKE"; break; case (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT | SSL_VERIFY_POST_HANDSHAKE) : cptr = "REQUIRED-POST_HANDSHAKE"; break; #endif default : cptr = "unknown!"; } WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "X509 client certificate VERIFY:!AZ", cptr); SesolaWatchErrors (sesolaptr); } #if SESOLA_SINCE_111 if (version >= TLS1_3_VERSION) { if (SSL_verify_client_post_handshake (sesolaptr->SslPtr) != 1) { SesolaWatchErrors (sesolaptr); SesolaClientCert (rqptr, SESOLA_VERIFY_AST, NULL); return; } } else #endif /* <= TLS1_2_VERSION */ { SSL_set_session_id_context (sesolaptr->SslPtr, SesolaSessionId, SSL_MAX_SSL_SESSION_ID_LENGTH); SSL_renegotiate (sesolaptr->SslPtr); } sesolaptr->ReadClientCert = false; sesolaptr->X509CertRequested = true; sesolaptr->SslStateFunction = &SesolaClientCertGet; } for (sane = 16; sane; sane--) { /*************/ /* handshake */ /*************/ /* allow for low-level SSL I/O here (e.g. link disconnection) */ if (VMSnok(sesolaptr->ReadIOsb.Status)) break; if (VMSnok(sesolaptr->WriteIOsb.Status)) break; value = SSL_do_handshake (sesolaptr->SslPtr); if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) { WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SSL_do_handshake() !SL !SL", value, SSL_get_error(sesolaptr->SslPtr,value)); SesolaWatchErrors (sesolaptr); } /* tweak the client into providing data (per Apache mod_ssl) */ SSL_peek (sesolaptr->SslPtr, buf, 0); /*****************/ /* current state */ /*****************/ if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "STATE !UL \"!AZ\" !AZ cert:!&B", SSL_get_state (sesolaptr->SslPtr), SSL_state_string (sesolaptr->SslPtr), SSL_state_string_long (sesolaptr->SslPtr), sesolaptr->ReadClientCert); state = SSL_get_state (sesolaptr->SslPtr); #if SESOLA_SINCE_110 if (state == TLS_ST_SR_CERT) sesolaptr->ReadClientCert = true; else if (state == TLS_ST_SR_CERT_VRFY) sesolaptr->ReadClientCert = true; else if (state == TLS_ST_OK) if (sesolaptr->ReadClientCert) break; #else if (state == SSL3_ST_SR_CERT_A) sesolaptr->ReadClientCert = true; else if (state == SSL3_ST_SR_CERT_B) sesolaptr->ReadClientCert = true; else if (state == SSL_ST_OK) if (sesolaptr->ReadClientCert) break; #endif /* if non-blocking IO in progress just return and wait for delivery */ if (sesolaptr->ReadInProgress || sesolaptr->WriteInProgress) { /****************/ /* wait for I/O */ /****************/ if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "READ:!&?yes\rno\r WRITE:!&?yes\rno\r", sesolaptr->ReadInProgress, sesolaptr->WriteInProgress); return; } /* if the handshake was broken */ if (value <= 0) break; } /************/ /* finished */ /************/ if (!sane) ErrorNoticed (rqptr, SS$_BUGCHECK, NULL, FI_LI); if (WATCHING (rqptr, WATCH_SESOLA)) SesolaWatchSession (sesolaptr); sesolaptr->ReadClientCert = false; sesolaptr->SslStateFunction = NULL; SesolaClientCert (rqptr, SESOLA_VERIFY_AST, NULL); } /*****************************************************************************/ /* Request data being sent by the client (e.g. PROFIND/PUT/POST) will interfere with renegotiation so read and buffer this (up to a reasonable quantity). Reinsert this into the application stream in SesolaNetIoRead(). Return -1 to indicate an error, 0 that application data is still being read, or the number of bytes in the application data (at the end of the read(s)). Renegotiation does not proceed until non-zero is returned. */ int SesolaClientRequestData (SESOLA_STRUCT *sesolaptr) { int size, value; uchar *aptr; REQUEST_STRUCT *rqptr; SESOLA_CONTEXT *ctxptr; /*********/ /* begin */ /*********/ rqptr = sesolaptr->RequestPtr; if (WATCHING (rqptr, WATCH_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "BUFFER client data !UL !UL/!UL", rqptr->rqHeader.ContentLength64, sesolaptr->VerifyPeerDataCount, sesolaptr->VerifyPeerDataSize); if (sesolaptr->VerifyPeerDataSize && sesolaptr->VerifyPeerDataCount >= sesolaptr->VerifyPeerDataSize) return (sesolaptr->VerifyPeerDataCount); if (!sesolaptr->VerifyPeerDataSize) { /* per-service data max falling back to global data max */ ctxptr = (SESOLA_CONTEXT*)rqptr->ServicePtr->SSLserverPtr; if (!(size = ctxptr->VerifyDataMax)) size = SesolaVerifyPeerDataMax; value = rqptr->rqHeader.ContentLength64; if (value > size) { if (WATCHING (rqptr, WATCH_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "X509 RENEGOTIATE request data exceeds !UL kBytes", size / 1024); return (sesolaptr->VerifyPeerDataCount = -1); } sesolaptr->VerifyPeerDataSize = value; /* global memory used (SesolaNetRead() is request-agnostic) */ sesolaptr->VerifyPeerDataPtr = sesolaptr->VerifyPeerReadPtr = aptr = VmGet (value); } while (sesolaptr->VerifyPeerDataCount < sesolaptr->VerifyPeerDataSize) { size = sesolaptr->VerifyPeerDataSize - sesolaptr->VerifyPeerDataCount; aptr = sesolaptr->VerifyPeerDataPtr + sesolaptr->VerifyPeerDataCount; value = SSL_read (sesolaptr->SslPtr, aptr, size); if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) { WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SSL_read() !SL !SL", value, SSL_get_error(sesolaptr->SslPtr,value)); SesolaWatchErrors (sesolaptr); } /* if non-blocking IO in progress just return and wait for delivery */ if (sesolaptr->ReadInProgress || sesolaptr->WriteInProgress) { if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "READ:!&?yes\rno\r WRITE:!&?yes\rno\r", sesolaptr->ReadInProgress, sesolaptr->WriteInProgress); return (0); } if (value <= 0) { if (WATCHING (rqptr, WATCH_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "X509 RENEGOTIATE request data read FAILURE"); SesolaWatchErrors (sesolaptr); return (sesolaptr->VerifyPeerDataCount = -1); } sesolaptr->VerifyPeerDataCount += value; if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "content:!UL/!UL !UL/!UL", value, size, sesolaptr->VerifyPeerDataCount, sesolaptr->VerifyPeerDataSize); } return (sesolaptr->VerifyPeerDataCount); } /*****************************************************************************/ /* MetaConEvaluate() has hit an "X509:" conditional. Process this and return true if the conditional is met, false if not met. The conditional can be empty in which case the availability of an X509 client certificate is tested, returning true if one is, false if not. The conditional can also supply a keyword, equate symbol, and optional wildcard or regex. The keyword is progressively searched for in the client certificate subject, certificate name and then in the certificate extensions (in much the same way as authorisation X509). If a keyword has no parameter then the directive just tests for the presence of the keyword in the certificate. A parameter is a wildcard or regex which is matched against the content corresponding to the keyword, returning true if it matches, false if it does not. */ BOOL SesolaClientCertMetaCon ( REQUEST_STRUCT *rqptr, METACON_LINE *mclptr, int WatchThisOne ) { BOOL result; char *cptr, *sptr, *zptr; char keyword [256]; SESOLA_STRUCT *sesolaptr; /*********/ /* begin */ /*********/ if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SesolaClientCertMetaCon() !AZ", mclptr->BufferPtr); if (HTTP2_REQUEST(rqptr)) sesolaptr = rqptr->Http2Stream.Http2Ptr->NetIoPtr->SesolaPtr; else sesolaptr = rqptr->NetIoPtr->SesolaPtr; if (sesolaptr == NULL) { /* not SSL service so X509 certificate impossible - test with "ssl:" */ if (WatchThisOne) WatchDataFormatted ("X509 requires an SSL service!!\n"); return (false); } /* parse the keyword */ cptr = mclptr->BufferPtr; zptr = (sptr = keyword) + sizeof(keyword)-1; while (*cptr && *cptr != '=' && sptr < zptr) *sptr++ = *cptr++; *sptr = '\0'; while (*cptr && *cptr != '=') cptr++; /* |cptr| now points to any keyword parameter (wildcard or regex) */ if (*cptr == '=') cptr++; if (sesolaptr->X509CertRequested) { /* has been requested (at SSL connect or previous renegotiation) */ rqptr->X509ClientCertMeta = false; if (sesolaptr->ClientCertPtr == NULL) { /* no certificate supplied - if just testing for one */ if (!keyword[0]) return (false); /* wanting to match to X509 certificate (i.e. keyword supplied) */ return (false); } } else { /* certificate has not (yet) been requested (via renegotiation) */ rqptr->X509ClientCertMeta = true; return (false); } /* if no keyword then just testing for an X509 certificate */ if (keyword[0]) return (true); /* search for the keyword */ sptr = SesolaCertParseDn (rqptr->rqAuth.ClientCertSubjectPtr, keyword); if (!sptr) sptr = SesolaCertName (sesolaptr->ClientCertPtr, keyword); if (!sptr) sptr = SesolaCertExtension (sesolaptr->ClientCertPtr, keyword); /* if keyword not found */ if (!sptr) { if (WatchThisOne) WatchDataFormatted ("Keyword \"!AZ\" not found.\n"); return (false); } /* if keyword has no parameter */ if (!*cptr) return (true); result = StringMatchAndRegex (rqptr, sptr, cptr, SMATCH_GREEDY_REGEX, mclptr->RegexPregPtr, NULL); return (result); } /*****************************************************************************/ /* Return a pointer to a null-terminated string containing the common name from any client certificate present. */ char* SesolaClientCertRemoteUser (REQUEST_STRUCT *rqptr) { char *bptr, *cptr, *sptr; SESOLA_STRUCT *sesolaptr; /*********/ /* begin */ /*********/ if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SesolaClientCertRemoteUser()"); if (HTTP2_REQUEST(rqptr)) sesolaptr = rqptr->Http2Stream.Http2Ptr->NetIoPtr->SesolaPtr; else sesolaptr = (SESOLA_STRUCT*)rqptr->NetIoPtr->SesolaPtr; if (!sesolaptr) return (NULL); if (!sesolaptr->ClientCertPtr) return (NULL); cptr = SesolaCertParseDn (rqptr->rqAuth.ClientCertSubjectPtr, "/CN="); if (!cptr) return (cptr); for (sptr = (cptr += 4); *sptr; sptr++); bptr = sptr = VmGetHeap (rqptr, sptr-cptr+1); while (*cptr) *sptr++ = *cptr++; *sptr = '\0'; return (bptr); } /*****************************************************************************/ /* For compilations without SSL these functions provide LINKage stubs for the rest of the HTTPd modules, allowing for just recompiling the Sesola module to integrate the SSL functionality. */ /*********************/ #else /* not SESOLA */ /*********************/ extern char ErrorSanityCheck[]; void SesolaClientCertEnd (REQUEST_STRUCT *rqptr) { /* always called by RequestEnd(); empty function; just return */ } SesolaClientCertGet (void *sesolaptr) { ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI); } char* SesolaClientCertRemoteUser (void *sesolaptr) { ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI); } BOOL SesolaClientCertMetaCon ( REQUEST_STRUCT *rqptr, METACON_LINE *mclptr, int WatchThisOne ) { ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI); } /************************/ #endif /* ifdef SESOLA */ /************************/ /*****************************************************************************/