From 33b60adc743db6e62bd9f580b148175ad1a3a5ca Mon Sep 17 00:00:00 2001 From: Mihael Schmidt Date: Wed, 27 May 2026 07:01:52 +0200 Subject: [PATCH] Added mTLS support --- TLS.md | 70 ++++++++++++++++++++++- headers/ileastic.bnd | 2 + headers/ileastic.rpgle | 71 ++++++++++++++++++++++- headers/sysdef.h | 12 +++- itests/ilbasics.rpgle | 21 +++++++ src/api.c | 8 +++ src/ileastic.c | 126 +++++++++++++++++++++++++++-------------- 7 files changed, 260 insertions(+), 50 deletions(-) diff --git a/TLS.md b/TLS.md index 0561ae9..742d11e 100644 --- a/TLS.md +++ b/TLS.md @@ -60,7 +60,7 @@ curl -v --cacert server.pub --url https://localhost:35800 ``` -## Certificate Information +## Server Certificate Information Information about the used server certificate can be queried during runtime. The data is available via the thread local storage which is a noxDB graph and can be queried with the noxDB API, see @@ -81,10 +81,74 @@ certificate values can be accessed via their corresponding keys. The keys are ba GSKit certificate data ids. The value for `CERT_COMMON_NAME` can be access by the key `common_name`. All values are strings. -The certificate is only available if it has been enabled by calling `il_setTlsServerCertEnabled`. +The certificate information is only available if it has been enabled by calling `il_setTlsServerCertEnabled`, +`Tls` in this case means thread local storage. ``` il_setTlsServerCertEnabled(config : IL_TRUE); ``` -For more information and available keys see the GSKit API [gsk_attribute_get_cert_info](https://www.ibm.com/docs/en/i/7.4.0?topic=ssw_ibm_i_74/apis/gsk_attribute_get_cert_info.html). \ No newline at end of file +For more information and available keys see the GSKit API [gsk_attribute_get_cert_info](https://www.ibm.com/docs/en/i/7.4.0?topic=ssw_ibm_i_74/apis/gsk_attribute_get_cert_info.html). + + + +## mTLS + +ILEastic supports mutual TLS which means that not only the server certificate is checked by the +client but the client must also send a certficiate which is validated by the server). + +This can be enabled by setting the `client auth mode` to something other than NONE. + +With the configuration set to IL_CLIENT_AUTH_MODE_REQUIRED the client must provide a valid client +certificate to establish a TLS connection. + +With the configuration set to IL_CLIENT_AUTH_MODE_PASSTHRU the client is asked for a client +certficate but does not need to provide one. + +A requirement for this is having TLS enabled by providing a server certificate in the +configured keystore file. + +All client certificates must also be available in the same keystore file. + +### Client Certificate Information + +Information about the provided client certificate can be queried during runtime. The data is available +via the thread local storage which is a noxDB graph and can be queried with the noxDB API, see +`il_getThreadMem`. + +Example: + +``` +dcl-s tls pointer; +dcl-s value varchar(100); + +tls = il_getThreadMem(request); +value = jx_getStr(tls : '/ileastic/certificate/client/common_name'); +``` + +The client certificate information is stored at the path `/ileastic/certificate/client`. The +certificate values can be accessed via their corresponding keys. The keys are based on the +GSKit certificate data ids. The value for `CERT_COMMON_NAME` can be access by the key `common_name`. +All values are strings. + +The certificate information is only available if it has been enabled by calling `il_setTlsClientCertEnabled`, +`Tls` in this case means thread local storage. + +``` +il_setTlsClientCertEnabled(config : IL_TRUE); +``` + +For more information and available keys see the GSKit API [gsk_attribute_get_cert_info](https://www.ibm.com/docs/en/i/7.4.0?topic=ssw_ibm_i_74/apis/gsk_attribute_get_cert_info.html). + +### Client Certifcate Validation Result + +The result of the validation of the client certificate can be queried via the thread local storage +at `/ileastic/certificate/client/validationcode`. The value is the returned value of the call to +`gsk_attribute_get_numeric_value` with enum id `GSK_CERTIFICATE_VALIDATION_CODE`. + +TLDR: validationcode = 0 means client certificate is valid + +### Plugins + +Plugins can access the certificate information by querying the thread local store. This can be used +to further filter requests by custom criterias. diff --git a/headers/ileastic.bnd b/headers/ileastic.bnd index 6202334..9c6d369 100644 --- a/headers/ileastic.bnd +++ b/headers/ileastic.bnd @@ -36,4 +36,6 @@ STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('ILEastic 1.1.2') EXPORT SYMBOL("il_mediatype_parseMediaType") EXPORT SYMBOL("il_setKeyfile") EXPORT SYMBOL("il_setTlsServerCertEnabled") + EXPORT SYMBOL("il_setTlsClientCertEnabled") + EXPORT SYMBOL("il_setClientAuthMode") ENDPGMEXP diff --git a/headers/ileastic.rpgle b/headers/ileastic.rpgle index 96dda86..bd56e34 100644 --- a/headers/ileastic.rpgle +++ b/headers/ileastic.rpgle @@ -366,9 +366,28 @@ dcl-c IL_MEDIA_TYPE_JSON 'application/json'; /// dcl-c IL_MEDIA_TYPE_XML 'application/xml'; +/// +// Generic true constant +/// dcl-c IL_TRUE 1; +/// +// Generic false constant +/// dcl-c IL_FALSE 0; +/// +// mTLS : No client certificate requested (no mTLS) +/// +dcl-c IL_CLIENT_AUTH_MODE_NONE 0; +/// +// mTLS : client certificate is required else TLS session will not be started +/// +dcl-c IL_CLIENT_AUTH_MODE_REQUIRED 1; +/// +// mTLS : client certificate is always request but does not need to be provided by the client. TLS session is always started. +/// +dcl-c IL_CLIENT_AUTH_MODE_PASSTHRU 2; + /// // Configuration /// @@ -382,6 +401,8 @@ dcl-ds il_config qualified template; isWorker ind; threadingMode int(5); tlsServerCertEnabled int(3); + tlsClientCertEnabled int(3); + clientAuthMode int(3); filler char(4096); // required - contains the private internal handlers end-ds; @@ -1067,7 +1088,7 @@ dcl-pr il_setKeyfile extproc(*dclcase); end-pr; /// -// Enable Server Certificate Infos in TLS +// Enable server certificate infos in TLS // // If enabled the available infos about the used server certificate for the TLS // connection are copied to the thread local storage avaiable in the request, @@ -1083,5 +1104,51 @@ end-pr; /// dcl-pr il_setTlsServerCertEnabled extproc(*dclcase); config likeds(il_config); - enabled int(5) value; + enabled int(3) value; end-pr; + +/// +// Enable client certificate infos in TLS +// +// If enabled the available infos about the used client certificate for the TLS +// connection are copied to the thread local storage avaiable in the request, +// see il_getThreadMem. +//

+// The information will be copied to the path /ileastic/certificate/client. +// The information is only available if the connection uses TLS (HTTPS). +// +// @param Configuration +// @param IL_TRUE = enabled , IL_FALSE = disable (default) +// +// @info This setting has no effect if no TLS is used. +/// +dcl-pr il_setTlsClientCertEnabled extproc(*dclcase); + config likeds(il_config); + enabled int(3) value; +end-pr; + +/// +// Set client auth mode (mTLS) +// +// Sets the client auth mode. The client auth mode defines if the client is asked +// for a client certificate which will be validated using the configured key +// store. +//

+// By default the client is not asked for a certificated (no mTLS). +//

+// With the configuration set to IL_CLIENT_AUTH_MODE_REQUIRED the client must +// provide a valid client certificate to establish a TLS connection. +//

+// With the configuration set to IL_CLIENT_AUTH_MODE_PASSTHRU the client is +// asked for a client certficate but does not need to provide one. +//

+// The result of the client certificate validation is provided via the thread +// local storage at /ileastic/certificate/client/validationcode. +// +// @param Configuration +// @param mTLS client auth mode (see IL_CLIENT_AUTH_MODE_xyz) +/// +dcl-pr il_setClientAuthMode extproc(*dclcase); + config likeds(il_config); + mode int(3) value; +end-pr; \ No newline at end of file diff --git a/headers/sysdef.h b/headers/sysdef.h index 2c79106..92c71c5 100644 --- a/headers/sysdef.h +++ b/headers/sysdef.h @@ -46,6 +46,14 @@ typedef enum _THREADING_MODE { } THREADING_MODE , *PTHREADING_MODE; #pragma enum (pop) +#pragma enum (1) +typedef enum _CLIENT_AUTH_MODE { + CLIENT_AUTH_MODE_NONE = 0, + CLIENT_AUTH_MODE_REQUIRED = 1, + CLIENT_AUTH_MODE_PASSTHRU = 2 +} CLIENT_AUTH_MODE; +#pragma enum (pop) + typedef _Packed struct { FCGX_Stream * out; FCGX_Stream * in; @@ -66,7 +74,9 @@ typedef _Packed struct _CONFIG { BOOL isWorker; THREADING_MODE threadingMode; BOOL tlsServerCertEnabled; - UCHAR filler[1022]; + BOOL tlsClientCertEnabled; + CLIENT_AUTH_MODE clientAuthMode; + UCHAR filler[1021]; // Private: int mainSocket; int clientSocket; diff --git a/itests/ilbasics.rpgle b/itests/ilbasics.rpgle index cf333e2..8208578 100644 --- a/itests/ilbasics.rpgle +++ b/itests/ilbasics.rpgle @@ -81,6 +81,8 @@ dcl-proc main; endif; il_setTlsServerCertEnabled(config : IL_TRUE); + il_setTlsClientCertEnabled(config : IL_TRUE); + il_setClientAuthMode(config : IL_CLIENT_AUTH_MODE_PASSTHRU); il_addRoute(config : %paddr(test_getBookExcerpt) : IL_GET : REGEX_START + '/book/excerpt' + REGEX_END); il_addRoute(config : %paddr(test_deleteBook) : IL_DELETE: REGEX_START + '/book/' + CURLY_OPEN + 'isbn' + CURLY_CLOSE + REGEX_END); @@ -90,6 +92,7 @@ dcl-proc main; il_addRoute(config : %paddr(test_search) : IL_GET : REGEX_START + '/book' + REGEX_END); il_addRoute(config : %paddr(test_bookOptions) : IL_OPTIONS : REGEX_START + '/book' + REGEX_END); il_addRoute(config : %paddr(test_getServerCertInfos) : IL_GET : REGEX_START + '/cert/server' + REGEX_END); + il_addRoute(config : %paddr(test_getClientCertInfos) : IL_GET : REGEX_START + '/cert/client' + REGEX_END); il_addRoute(config : %paddr(test_notFound)); il_listen (config); @@ -295,3 +298,21 @@ dcl-proc test_getServerCertInfos; response.contentType = 'application/json'; il_responseWriteStream(response: jx_stream(serverCertInfos)); end-proc; + + +dcl-proc test_getClientCertInfos; + dcl-pi *n; + request likeds(IL_REQUEST); + response likeds(IL_RESPONSE); + end-pi; + + dcl-s tls pointer; + dcl-s clientCertInfos pointer; + + tls = il_getThreadMem(request); + clientCertInfos = jx_locate(tls : '/ileastic/certificate/client'); + + response.status = 200; + response.contentType = 'application/json'; + il_responseWriteStream(response: jx_stream(clientCertInfos)); +end-proc; diff --git a/src/api.c b/src/api.c index 599d437..336dd9f 100644 --- a/src/api.c +++ b/src/api.c @@ -456,3 +456,11 @@ void il_setKeyfile(PCONFIG config, PVARCHAR256 keyfilePath , PVARCHAR64 keyfileP void il_setTlsServerCertEnabled(PCONFIG config, BOOL enabled) { config->tlsServerCertEnabled = enabled; } + +void il_setTlsClientCertEnabled(PCONFIG config, BOOL enabled) { + config->tlsClientCertEnabled = enabled; +} + +void il_setClientAuthMode(PCONFIG config, CLIENT_AUTH_MODE mode) { + config->clientAuthMode = mode; +} \ No newline at end of file diff --git a/src/ileastic.c b/src/ileastic.c index 9b94c2f..5895383 100644 --- a/src/ileastic.c +++ b/src/ileastic.c @@ -65,7 +65,8 @@ BOOL isSecure(PCONFIG config); void log_gskit_error(const char * msg, int rc); int setupGskitEnvironment(PCONFIG pConfig); int setupSecureSocket(PCONFIG pConfig, int clientSocket); -void addServerCertInfos(gsk_handle * sslHandle, void * threadMem); +void addCertificateDetails(gsk_handle * sslHandle, void * tlsCertNode, GSK_CERT_ID certificateType); +void addClientCertificateValidationResult(gsk_handle * sslHandle, void * tlsCertNode); VARCHAR256 il_getCallingProgramPath(); @@ -816,16 +817,28 @@ static void * serverThread (PINSTANCE pInstance) PROUTING matchingRouting; BOOL connected = true; UCHAR temp [256]; - PVOID serverCertNode; + PVOID serverCertNode = NULL; + PVOID clientCertNode = NULL; if(! pInstance->config.isWorker) { pthread_detach(pthread_self()); } - if (isSecure(&(pInstance->config)) && pInstance->config.tlsServerCertEnabled) { - serverCertNode = jx_NewObject(NULL); - addServerCertInfos(&(pInstance->config.envHandle), serverCertNode); + if (isSecure(&(pInstance->config))) { + if (pInstance->config.tlsServerCertEnabled) { + serverCertNode = jx_NewObject(NULL); + addCertificateDetails(&(pInstance->config.envHandle), serverCertNode, GSK_LOCAL_CERT_INFO); + } + + if (pInstance->config.clientAuthMode != CLIENT_AUTH_MODE_NONE) { + clientCertNode = jx_NewObject(NULL); + addClientCertificateValidationResult(&(pInstance->config.sessionHandle), clientCertNode); + + if (pInstance->config.tlsClientCertEnabled) { + addCertificateDetails(&(pInstance->config.sessionHandle), clientCertNode, GSK_PARTNER_CERT_INFO); + } + } } while (pInstance->config.clientSocket > 0 && connected) { @@ -846,9 +859,16 @@ static void * serverThread (PINSTANCE pInstance) pResponse = &response; request.threadMem = (PVOID) jx_NewObject(NULL); - if (isSecure(&(pInstance->config)) && pInstance->config.tlsServerCertEnabled) { - PJXNODE tlsCertNode = jx_GetOrCreateNode(request.threadMem, "/ileastic/certificate"); - jx_NodeMoveInto(tlsCertNode, "server", serverCertNode); + if (isSecure(&(pInstance->config))) { + if (serverCertNode != NULL) { + PJXNODE tlsCertNode = jx_GetOrCreateNode(request.threadMem, "/ileastic/certificate"); + jx_NodeMoveInto(tlsCertNode, "server", serverCertNode); + } + + if (clientCertNode != NULL) { + PJXNODE tlsCertNode = jx_GetOrCreateNode(request.threadMem, "/ileastic/certificate"); + jx_NodeMoveInto(tlsCertNode, "client", clientCertNode); + } } #pragma exception_handler(handleServletException, pResponse, _C1_ALL, _C2_MH_ESCAPE, _CTLA_HANDLE) @@ -1280,7 +1300,7 @@ void il_listen (PCONFIG pConfig, SERVLET servlet) } } -void addServerCertInfos(gsk_handle * sslHandle, void * serverCertInfos) { +void addCertificateDetails(gsk_handle * sslHandle, void * tlsCertNode, GSK_CERT_ID certificateType) { // All certificate data is returned in ASCII CCSID 850. PXLATEDESC a2e = XlateXdOpen(850, 0); @@ -1292,13 +1312,11 @@ void addServerCertInfos(gsk_handle * sslHandle, void * serverCertInfos) { rc = gsk_attribute_get_cert_info ( *sslHandle, - GSK_LOCAL_CERT_INFO, + certificateType, &certElements, &certElementCount); if (rc == GSK_OK) { - il_joblog("got cert infos: %d", certElementCount); - // typedef struct gsk_cert_data_elem_t { // GSK_CERT_DATA_ID cert_data_id; // char *cert_data_p; @@ -1458,7 +1476,7 @@ void addServerCertInfos(gsk_handle * sslHandle, void * serverCertInfos) { break; } - if (certInfoKey) jx_SetStrByName (serverCertInfos, certInfoKey, certInfoValue); + if (certInfoKey) jx_SetStrByName (tlsCertNode, certInfoKey, certInfoValue); } } else { @@ -1468,6 +1486,15 @@ void addServerCertInfos(gsk_handle * sslHandle, void * serverCertInfos) { XlateXdClose(a2e); } +void addClientCertificateValidationResult(gsk_handle * sslHandle, void * tlsCertNode) { + int value = 0; + int rc = gsk_attribute_get_numeric_value(*sslHandle, GSK_CERTIFICATE_VALIDATION_CODE, &value); + if (rc == GSK_OK) + jx_SetIntByName(tlsCertNode , "validationcode", value); + else + log_gskit_error("Get client certificate validation code", rc); +} + BOOL isSecure(PCONFIG config) { BOOL secured = config->certificateFile.Length > 0; return secured; @@ -1524,35 +1551,46 @@ int setupGskitEnvironment(PCONFIG pConfig) { } int setupSecureSocket(PCONFIG pConfig, int clientSocket) { - int rc; - - // Create secure socket handle - rc = gsk_secure_soc_open(pConfig->envHandle, &(pConfig->sessionHandle)); - if (rc != GSK_OK) { - log_gskit_error("gsk_secure_soc_open", rc); - return -1; - } - - // Associate socket with SSL handle - rc = gsk_attribute_set_numeric_value(pConfig->sessionHandle, GSK_FD, clientSocket); - if (rc != GSK_OK) { - log_gskit_error("gsk_attribute_set_numeric_value GSK_FD", rc); - return -1; - } - - // Set as server socket - rc = gsk_attribute_set_enum(pConfig->sessionHandle, GSK_SESSION_TYPE, GSK_SERVER_SESSION); - if (rc != GSK_OK) { - log_gskit_error("gsk_attribute_set_enum GSK_SESSION_TYPE", rc); - return -1; - } - - // Initialize secure session - rc = gsk_secure_soc_init(pConfig->sessionHandle); - if (rc != GSK_OK) { - log_gskit_error("gsk_secure_soc_init", rc); - return -1; - } - - return 0; + int rc; + + // Create secure socket handle + rc = gsk_secure_soc_open(pConfig->envHandle, &(pConfig->sessionHandle)); + if (rc != GSK_OK) { + log_gskit_error("gsk_secure_soc_open", rc); + return -1; + } + + // Associate socket with SSL handle + rc = gsk_attribute_set_numeric_value(pConfig->sessionHandle, GSK_FD, clientSocket); + if (rc != GSK_OK) { + log_gskit_error("gsk_attribute_set_numeric_value GSK_FD", rc); + return -1; + } + + // Set as server socket + GSK_ENUM_ID sessionType = pConfig->clientAuthMode != CLIENT_AUTH_MODE_NONE ? GSK_SERVER_SESSION_WITH_CL_AUTH : GSK_SERVER_SESSION; + rc = gsk_attribute_set_enum(pConfig->sessionHandle, GSK_SESSION_TYPE, sessionType); + if (rc != GSK_OK) { + log_gskit_error("gsk_attribute_set_enum GSK_SESSION_TYPE", rc); + return -1; + } + + if (pConfig->clientAuthMode != CLIENT_AUTH_MODE_NONE) { + GSK_ENUM_ID authType = pConfig->clientAuthMode == CLIENT_AUTH_MODE_PASSTHRU ? GSK_CLIENT_AUTH_PASSTHRU : GSK_IBMI_CLIENT_AUTH_REQUIRED; + + gsk_attribute_set_enum(pConfig->sessionHandle, GSK_CLIENT_AUTH_TYPE , authType ); + if (rc != GSK_OK) { + log_gskit_error("gsk_attribute_set_enum GSK_CLIENT_AUTH_TYPE", rc); + return -1; + } + } + + // Initialize secure session + rc = gsk_secure_soc_init(pConfig->sessionHandle); + if (rc != GSK_OK) { + log_gskit_error("gsk_secure_soc_init", rc); + return -1; + } + + return 0; } \ No newline at end of file