/*****************************************************************************/ /* SesolaClient.c Secure Sockets Layer client (peer) certificate. VERSION HISTORY --------------- 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 BIO_METHOD *SesolaBioMemPtr; 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, SessionHits, SessionTimeout, SessionTimeCSec, SessionTimeoutCSec, VerifyMode, WatchThisType; char *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; 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; } 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) sesolaptr->ClientCertPtr = NULL; else if (sesolaptr->CertVerifyMode == SESOLA_VERIFY_PEER_NONE) sesolaptr->ClientCertPtr = NULL; else 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 == NULL) { /***********************************************/ /* 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 SesolaClientCertRenegotiate(), 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); SesolaClientCertRenegotiate (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 "medium is offline" status is faux to indicate 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 !AZ", VerifyParam == SESOLA_VERIFY_AST ? "RENEGOTIATED" : "CACHED"); 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 SesolaClientCertRenegotiate() */ 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 SesolaClientCertRenegotiate() */ 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 SesolaClientCertRenegotiate() */ 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 SesolaClientCertRenegotiate() */ 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) { while (*cptr && *cptr != '=') cptr++; if (*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 SesolaClientCertRenegotiate() */ SysDclAst (sesolaptr->ClientCertAstFunction, rqptr); } return (SS$_NORMAL); } /*****************************************************************************/ /* 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" */ /** 11-JUN-2017 if (!(csptr = SesolaCertParseDn (csptr, sptr))) **/ 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); } #if OPENSSL_VERSION_NUMBER < 0x10100000L /*****************************************************************************/ /* Versions of OpenSSL before 1.1.0. Initiate an SSL "renegotiate" (state 1) and then an SSL "accept" (state 2) sequence to give the client an opportunity to supply a client certificate. Due to the non-blocking I/O used by WASD this function will be called multiple times to complete the SSL renegotiate/accept dialog. Ensure pending request data (typically from a PROPFIND, PUT or POST) is cleared before attempting to renegotiate. */ SesolaClientCertRenegotiate (SESOLA_STRUCT *sesolaptr) { int size, value, verify; char *aptr; REQUEST_STRUCT *rqptr; /*********/ /* begin */ /*********/ rqptr = sesolaptr->RequestPtr; if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SesolaClientCertRenegotiate() !&F state:!UL", &SesolaClientCertRenegotiate, sesolaptr->RenegotiateState); sesolaptr->SslStateFunction = &SesolaClientCertRenegotiate; if (rqptr->rqHeader.ContentLength) if ((value = SesolaClientRequestData (sesolaptr)) < 0) sesolaptr->RenegotiateState = 3; else if (value == 0) return; /* else just continue on thru */ if (!sesolaptr->RenegotiateState) { sesolaptr->RenegotiateState = 1; sesolaptr->X509CertRequested = true; if (WATCHING (rqptr, WATCH_SESOLA)) { verify = SSL_get_verify_mode (sesolaptr->SslPtr); switch (verify) { case SSL_VERIFY_NONE : aptr = "NONE"; break; case SSL_VERIFY_PEER : aptr = "OPTIONAL"; break; case SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT : aptr = "REQUIRED"; break; default : "unknown!"; } WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "X509 RENEGOTIATE client certificate verify:!AZ", aptr); SesolaWatchErrors (sesolaptr); } if (!SSL_set_session_id_context (sesolaptr->SslPtr, SesolaSessionId, SSL_MAX_SSL_SESSION_ID_LENGTH)) { if (WATCHING (rqptr, WATCH_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "SET session id FAILURE"); SesolaWatchErrors (sesolaptr); sesolaptr->RenegotiateState = 3; } if (SSL_renegotiate (sesolaptr->SslPtr) <= 0) { if (WATCHING (rqptr, WATCH_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "X509 RENEGOTIATE client certificate FAILURE"); SesolaWatchErrors (sesolaptr); sesolaptr->RenegotiateState = 3; } } while (sesolaptr->RenegotiateState <= 2) { 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); } /* 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; } if (value <= 0) { if (WATCHING (rqptr, WATCH_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "X509 RENEGOTIATE client certificate FAILURE"); SesolaWatchErrors (sesolaptr); /* hammer the SSL state back to OK */ sesolaptr->SslPtr->state = SSL_ST_OK; } if (sesolaptr->RenegotiateState++ == 1) { /* SSL_set_accept_state() does too much! */ sesolaptr->SslPtr->state = SSL_ST_ACCEPT; } } if (WATCHING (rqptr, WATCH_SESOLA)) SesolaWatchSession (sesolaptr); sesolaptr->RenegotiateState = 0; sesolaptr->SslStateFunction = NULL; SesolaClientCert (rqptr, SESOLA_VERIFY_AST, NULL); } #else /* OPENSSL_VERSION_NUMBER < 0x10100000L */ /*****************************************************************************/ /* Versions of OpenSSL 1.1.0 and later. Initiate an SSL renegotiate to give the client an opportunity to supply a client certificate. Due to the non-blocking I/O used by WASD this function will be called multiple times to complete the SSL renegotiate/accept dialog. Ensure pending request data (typically from a PROPFIND, PUT or POST) is cleared before attempting to renegotiate. */ SesolaClientCertRenegotiate (SESOLA_STRUCT *sesolaptr) { int value, peekcnt; char *aptr; char buf[1]; REQUEST_STRUCT *rqptr; SSL *SslPtr; /*********/ /* begin */ /*********/ rqptr = sesolaptr->RequestPtr; if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SesolaClientCertRenegotiate() !&F !&B", &SesolaClientCertRenegotiate, sesolaptr->SslStateFunction != &SesolaClientCertRenegotiate); SslPtr = sesolaptr->SslPtr; if (sesolaptr->SslStateFunction != &SesolaClientCertRenegotiate) { if (rqptr->rqHeader.ContentLength) if ((value = SesolaClientRequestData (sesolaptr)) <= 0) return; sesolaptr->X509CertRequested = true; sesolaptr->SslStateFunction = &SesolaClientCertRenegotiate; if (WATCHING (rqptr, WATCH_SESOLA)) { switch (SSL_get_verify_mode(SslPtr)) { case SSL_VERIFY_NONE : aptr = "NONE"; break; case SSL_VERIFY_PEER : aptr = "OPTIONAL"; break; case SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT : aptr = "REQUIRED"; break; default : "unknown!"; } WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "X509 RENEGOTIATE client certificate verify:!AZ", aptr); SesolaWatchErrors (sesolaptr); } SSL_set_session_id_context (SslPtr, SesolaSessionId, sizeof(SesolaSessionId)); SSL_renegotiate (SslPtr); } for (;;) { /* allow for low-level SSL I/O here (e.g. link disconnection) */ if (sesolaptr->ReadIOsb.Status && VMSnok(sesolaptr->ReadIOsb.Status)) break; if (sesolaptr->WriteIOsb.Status && VMSnok(sesolaptr->WriteIOsb.Status)) break; value = SSL_do_handshake (SslPtr); if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) { WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SSL_do_handshake() !SL !SL", value, SSL_get_error(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; } /* renegotation successful (with or without certificate provided) */ if (value == 1) break; /* handshake returned -1 (error) or 0 (failed) */ if (WATCHING (rqptr, WATCH_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "X509 RENEGOTIATE client certificate FAILURE"); SesolaWatchErrors (sesolaptr); if (!SSL_renegotiate_pending (SslPtr)) break; /* tweak the client into sending some data (per Apache mod_ssl) */ SSL_peek (SslPtr, buf, sizeof(buf)); } if (WATCHING (rqptr, WATCH_SESOLA)) SesolaWatchSession (sesolaptr); sesolaptr->SslStateFunction = NULL; SesolaClientCert (rqptr, SESOLA_VERIFY_AST, NULL); } #endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */ /*****************************************************************************/ /* 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; /*********/ /* begin */ /*********/ rqptr = sesolaptr->RequestPtr; if (WATCHMOD (rqptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SesolaClientRequestData() !UL/!UL", sesolaptr->VerifyPeerDataCount, rqptr->rqHeader.ContentLength); if ((uint)sesolaptr->VerifyPeerDataCount >= (uint)rqptr->rqHeader.ContentLength) return (sesolaptr->VerifyPeerDataCount); /* per-service data max falling back to global data max */ if (!(size = ((SESOLA_CONTEXT*)rqptr->ServicePtr->SSLserverPtr)->VerifyDataMax)) size = SesolaVerifyPeerDataMax; if (rqptr->rqHeader.ContentLength > size) { if (WATCHING (rqptr, WATCH_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "X509 RENEGOTIATE request data exceeds !UL kBytes", size / 1024); return (sesolaptr->VerifyPeerDataCount = -1); } if (!sesolaptr->VerifyPeerDataSize) { sesolaptr->VerifyPeerDataSize = size = rqptr->rqHeader.ContentLength; /* global memory used (SesolaNetRead() is request-agnostic) */ sesolaptr->VerifyPeerDataPtr = sesolaptr->VerifyPeerReadPtr = aptr = VmGet (size); } while (sesolaptr->VerifyPeerDataCount < rqptr->rqHeader.ContentLength) { size = rqptr->rqHeader.ContentLength - 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 (-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[]; SesolaClientCertRenegotiate (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 */ /************************/ /*****************************************************************************/