From 07fce879314f8a2044accf35935e95180c95e2df Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 19 Feb 2026 14:17:55 +0100 Subject: [PATCH] Implement OCSP client and responder with HTTP and SCGI transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Depends on https://github.com/wolfSSL/wolfssl/pull/9761 Core OCSP implementation: - Register the new WOLFCLU_OCSP mode enum value - The responder main loop accepts connections and handles the request in a transport-agnostic way. - Add the OCSP mode to the help text in src/tools/clu_funcs.c. New HTTP utilities (src/tools/clu_http.c): - Move the static `kHttpGetMsg` from src/client/client.c and the static `kHttpServerMsg` from src/server/server.c into shared accessor functions - Add HTTP builder and server helpers New SCGI protocol implementation (src/tools/clu_scgi.c): - Implement the SCGI wire protocol per https://python.ca/scgi/protocol.txt Certificate and config additions (certs/): - Add ocsp-responder-cert.pem which is an authorized responder for ca-cert.pem Test suites: - tests/ocsp/ocsp-test.sh: top-level test runner with four interop combinations (wolfssl↔openssl, wolfssl↔wolfssl, openssl↔wolfssl, openssl↔openssl) sequentially - tests/ocsp/ocsp-interop-test.sh: test script taking in $OCSP_CLIENT and $OCSP_RESPONDER. Written to take in the same commands when run with wolfssl or openssl on either side - tests/ocsp-scgi/ocsp-scgi-test.sh: SCGI integration test using nginx for HTTP termination --- .github/workflows/fsanitize-check.yml | 8 + .github/workflows/ubuntu-check.yml | 7 + .gitignore | 5 + Makefile.am | 2 + README.md | 96 ++ autogen.sh | 12 - certs/ocsp-responder-cert.pem | 27 + certs/ocsp-responder-key.pem | 28 + certs/ocsp.cnf | 5 + certs/renew.sh | 11 + src/client/client.c | 14 +- src/clu_main.c | 9 + src/include.am | 6 +- src/ocsp/clu_ocsp.c | 1335 +++++++++++++++++++++++++ src/server/server.c | 21 +- src/tools/clu_funcs.c | 3 + src/tools/clu_http.c | 420 ++++++++ src/tools/clu_pem_der.c | 134 +++ src/tools/clu_scgi.c | 277 +++++ tests/ocsp-scgi/include.am | 11 + tests/ocsp-scgi/ocsp-scgi-test.sh | 493 +++++++++ tests/ocsp-scgi/scgi_params | 17 + tests/ocsp/include.am | 6 + tests/ocsp/ocsp-interop-test.sh | 520 ++++++++++ tests/ocsp/ocsp-test.sh | 82 ++ tests/x509/x509-req-test.sh | 1 + wolfCLU.vcxproj | 4 + wolfclu/client.h | 1 - wolfclu/clu_header_main.h | 199 ++++ wolfclu/clu_optargs.h | 1 + 30 files changed, 3718 insertions(+), 37 deletions(-) create mode 100644 certs/ocsp-responder-cert.pem create mode 100644 certs/ocsp-responder-key.pem create mode 100644 certs/ocsp.cnf create mode 100644 src/ocsp/clu_ocsp.c create mode 100644 src/tools/clu_http.c create mode 100644 src/tools/clu_pem_der.c create mode 100644 src/tools/clu_scgi.c create mode 100644 tests/ocsp-scgi/include.am create mode 100755 tests/ocsp-scgi/ocsp-scgi-test.sh create mode 100644 tests/ocsp-scgi/scgi_params create mode 100644 tests/ocsp/include.am create mode 100755 tests/ocsp/ocsp-interop-test.sh create mode 100755 tests/ocsp/ocsp-test.sh diff --git a/.github/workflows/fsanitize-check.yml b/.github/workflows/fsanitize-check.yml index 9f91c062..fb1cd4cb 100644 --- a/.github/workflows/fsanitize-check.yml +++ b/.github/workflows/fsanitize-check.yml @@ -62,6 +62,14 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 4 steps: + - name: Install dependencies + run: | + # Don't prompt for anything + export DEBIAN_FRONTEND=noninteractive + sudo apt-get update + # openssl and nginx used for ocsp testing + sudo apt-get install -y openssl nginx + - name: Checking cache for wolfssl uses: actions/cache@v4 with: diff --git a/.github/workflows/ubuntu-check.yml b/.github/workflows/ubuntu-check.yml index d6c5f57f..58a064cf 100644 --- a/.github/workflows/ubuntu-check.yml +++ b/.github/workflows/ubuntu-check.yml @@ -12,6 +12,13 @@ jobs: runs-on: ubuntu-latest steps: + - name: Install dependencies + run: | + # Don't prompt for anything + export DEBIAN_FRONTEND=noninteractive + sudo apt-get update + # openssl and nginx used for ocsp testing + sudo apt-get install -y openssl nginx - uses: actions/checkout@master with: repository: wolfssl/wolfssl diff --git a/.gitignore b/.gitignore index 29208a08..c8f7499c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ autom4te.cache/ config.log config.status configure +configure~ clu_src/config.h.in* *.lo *.Plo @@ -35,3 +36,7 @@ src/config.h.in~ src/stamp-h1 *.gcno *.gcda +.cproject +.project +.settings/ +AGENTS.md diff --git a/Makefile.am b/Makefile.am index 12554d89..e63d8d9b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -51,6 +51,8 @@ include tests/pkey/include.am include tests/dgst/include.am include tests/rand/include.am include tests/base64/include.am +include tests/ocsp/include.am +include tests/ocsp-scgi/include.am include tests/pkcs/include.am include tests/x509/include.am include tests/encrypt/include.am diff --git a/README.md b/README.md index 1a2d1bdf..dee05fc3 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,102 @@ wolfssl ca -altextend -in B.cert -keyfile ecc-key-A.priv -altkey ml-dsa-key-A.pr wolfssl ca -altextend -in C.cert -keyfile ecc-key-B.priv -altkey ml-dsa-key-B.priv -altpub ml-dsa-key-C.pub -subjkey ecc-key-C.priv -cert B-chimera.cert -out C-chimera.cert ``` +## Deploying an OCSP Responder with nginx and SCGI + +wolfCLU includes an OCSP responder that can be deployed in production using nginx as an HTTP frontend and SCGI (Simple Common Gateway Interface) as the communication protocol. **This is the preferred deployment method** because SCGI is much simpler than HTTP while allowing you to leverage a mature and robust HTTP implementation like nginx. + +### Why SCGI? + +- **Simplicity**: SCGI is a straightforward protocol that's easier to implement than full HTTP +- **Robustness**: nginx handles all HTTP concerns (timeouts, keep-alive, TLS, load balancing, etc.) +- **Separation of roles**: The OCSP responder focuses on OCSP logic, nginx handles web serving + +### Prerequisites + +Install nginx: +``` +sudo apt-get install nginx # Debian/Ubuntu +sudo yum install nginx # RHEL/CentOS +brew install nginx # macOS +``` + +### Basic Setup + +1. **Start the wolfCLU OCSP responder in SCGI mode:** + +```bash +wolfssl ocsp -scgi \ + -port 8081 \ + -index /path/to/index.txt \ + -rsigner /path/to/ca-cert.pem \ + -rkey /path/to/ca-key.pem \ + -CA /path/to/ca-cert.pem +``` + +The responder will listen on port 8081 for SCGI connections. + +2. **Configure nginx to proxy HTTP OCSP requests to the SCGI backend:** + +Create a file `/etc/nginx/ocsp-scgi.conf`: + +```nginx +# SCGI parameters for OCSP +scgi_param REQUEST_METHOD $request_method; +scgi_param REQUEST_URI $request_uri; +scgi_param QUERY_STRING $query_string; +scgi_param CONTENT_TYPE $content_type; +scgi_param CONTENT_LENGTH $content_length; + +scgi_param SCRIPT_NAME $fastcgi_script_name; +scgi_param DOCUMENT_URI $document_uri; +scgi_param DOCUMENT_ROOT $document_root; +scgi_param SERVER_PROTOCOL $server_protocol; +scgi_param HTTPS $https if_not_empty; + +scgi_param REMOTE_ADDR $remote_addr; +scgi_param REMOTE_PORT $remote_port; +scgi_param SERVER_PORT $server_port; +scgi_param SERVER_NAME $server_name; +``` + +Add to your nginx site configuration (e.g., `/etc/nginx/sites-available/default`): + +```nginx +server { + listen 80; + server_name ocsp.example.com; + + location /ocsp { + scgi_pass localhost:8081; + include /etc/nginx/ocsp-scgi.conf; + + scgi_connect_timeout 5s; + scgi_send_timeout 10s; + scgi_read_timeout 10s; + } +} +``` + +3. **Reload nginx:** + +```bash +sudo nginx -t # Test configuration +sudo systemctl reload nginx # Reload nginx +``` + +4. **Test the OCSP responder:** + +```bash +wolfssl ocsp \ + -issuer ca-cert.pem \ + -cert server-cert.pem \ + -url http://ocsp.example.com/ocsp +``` + +### Index File Format + +The `-index` file uses OpenSSL's CA index format. + ## Contacts Please contact support@wolfssl.com with any questions or comments. diff --git a/autogen.sh b/autogen.sh index eaed8f98..739b603f 100755 --- a/autogen.sh +++ b/autogen.sh @@ -3,18 +3,6 @@ # Create configure and makefile stuff... # -# Git hooks should come before autoreconf. -if [ -d .git ]; then - if [ ! -d .git/hooks ]; then - mkdir .git/hooks || exit $? - fi - - if [ ! -e .git/hooks/pre-commit ]; then - ln -s ../../pre-commit.sh .git/hooks/pre-commit || exit $? - fi -fi - - set -e # if get an error about libtool not setup diff --git a/certs/ocsp-responder-cert.pem b/certs/ocsp-responder-cert.pem new file mode 100644 index 00000000..ffab3f95 --- /dev/null +++ b/certs/ocsp-responder-cert.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEoTCCA4mgAwIBAgIBZDANBgkqhkiG9w0BAQsFADCBlDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoMCFNh +d3Rvb3RoMRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29sZnNz +bC5jb20xHzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wHhcNMjYwMjE4 +MTkxNDQxWhcNMjgxMTE0MTkxNDQxWjB1MQswCQYDVQQGEwJVUzEQMA4GA1UECAwH +TW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjEVMBMGA1UECgwMd29sZlNTTCBPQ1NQ +MRIwEAYDVQQLDAlSZXNwb25kZXIxFzAVBgNVBAMMDk9DU1AgUmVzcG9uZGVyMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApMVQ84ki9B8QgCRpwhEE0z5A +uKgi3KDK2cFimJrLX/JG7Rzq+SMj8X3yIEB4eP5FYVLvugAAbR0p4Vqb2mgOrleo +hwGg/1tgqoZaxXJVFFySpUUW+L0VCh6+CB80+KHss2Sb7xFNfWzd+yFRyN/jKhaT +nvusFwSnYKdMMxJlV/zAhVEPKIfqS4N8JRQhLJW38z1f7Jb+uB94JReCuKZiayx4 +wFh189EjLq9u/bJoCNy60HzzyAHZqiGM2I3Pk6mw2+twJZGOEhH0q+kL/w9ZfJpt +3j+Dct/5jpNsPI3aIw94qAnIQC5de7xdAk9JvVm0ujxsat6t5NyRLYx/xOClOQID +AQABo4IBGjCCARYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUFqtdxd3qLuCtJ53mlNZQ +ugLQ/wIwgdQGA1UdIwSBzDCByYAUJ45nEXTDJh0/7TNjs6TYHTDl6NWhgZqkgZcw +gZQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdNb250YW5hMRAwDgYDVQQHDAdCb3pl +bWFuMREwDwYDVQQKDAhTYXd0b290aDETMBEGA1UECwwKQ29uc3VsdGluZzEYMBYG +A1UEAwwPd3d3LndvbGZzc2wuY29tMR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdvbGZz +c2wuY29tghRrm3DG8aOUZRmhCFjvp40reoPB2jATBgNVHSUEDDAKBggrBgEFBQcD +CTANBgkqhkiG9w0BAQsFAAOCAQEAPZSIsDoFVKciK39jhVI/uBbfwKlgLvRhrofc +5Xt2GsX/ebaY7SXmFcBycG7t6PL8wZCMtisqPb5XFjEXYoTvgYeDSuuEYW7hTYqW +hXLdqLFaMaql2chMDmx5dMO84KKLaq+z3QTHO1Imbz0gsagT4yz3Xk3zcUN07EDc +R/gWq41CxcCyiPeeoscKE8EOq+E9eN8mc34EbUU6swuHNHwGqSLo7d9y0w5/cgRD +Ma0WAC0FvLqfNwek08UzEYOD2BMZhzDQOynINbaGuY1GZdPUK2RwzVgkOOaR08Bv +ZBJs3IwBBgE8u5n63xHrmCnehxxtl0vC0IXw44+lt+fF9gc6Mg== +-----END CERTIFICATE----- diff --git a/certs/ocsp-responder-key.pem b/certs/ocsp-responder-key.pem new file mode 100644 index 00000000..5e759113 --- /dev/null +++ b/certs/ocsp-responder-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkxVDziSL0HxCA +JGnCEQTTPkC4qCLcoMrZwWKYmstf8kbtHOr5IyPxffIgQHh4/kVhUu+6AABtHSnh +WpvaaA6uV6iHAaD/W2CqhlrFclUUXJKlRRb4vRUKHr4IHzT4oeyzZJvvEU19bN37 +IVHI3+MqFpOe+6wXBKdgp0wzEmVX/MCFUQ8oh+pLg3wlFCEslbfzPV/slv64H3gl +F4K4pmJrLHjAWHXz0SMur279smgI3LrQfPPIAdmqIYzYjc+TqbDb63AlkY4SEfSr +6Qv/D1l8mm3eP4Ny3/mOk2w8jdojD3ioCchALl17vF0CT0m9WbS6PGxq3q3k3JEt +jH/E4KU5AgMBAAECggEADmjxPPMz2tyyoTpOA3pgjSbnGx8dOWVYiDW47TawbZov +KMJ8LECt/oswtzBcONyn7ayGqaIhZ2mDBaHaen3aNtYUt4Xlch+oMxGf85+doDO+ +YXTK3wMOSX3JycUM6Wej30Z/uqctOzhfq3xM/j/SSpaB34gME1FFYBcRe2/y7ABb +TycvgSmvK0hVklDa488He+lNdHPh01aJnKGpKU33qMndhNf+dAcZtXUvkBSe1RdP +y4KWUG+paHWVB7r7upTuVvVV964Ie0+Ji7PwG3eq1yf2SgOpe+/rf8WmNu5qEoLW +Y/nZXwD27RaVb5L4p0itfSM+m61R+lzMasPYruhnAQKBgQDYT2RiDYUdMfgcH/Kl +jmfIpgWrI5rkiN/wbEI+xbkWdzjbbUnAgA6CC5F5xmQA2B/ekNBqxcB6AqPGkD+t +4kx0Kz2VQSBVIWQ0Q2z0xZ9GrYiIGIv8CGdNN+XBvXBXMMYx6QcYtY3aSNV2fx7W +IUVnbzLyHI6KzElx3OT1uWXrwQKBgQDDAP8G0qT9KUnyXBNK27WurGMJo1bOLS1l +8odVXrijQy/geDfxrr0UkoootBIs9aOP/Y1SZ32cqW9H9GDTb6HB4zAaUQvY07wS +wq/0rgwIDWjL7zKaK8Pv9BhZmpQqRDqhZypJ2EkEGdgdd1to2zL5k3OdPN/XW4nG +VrHhsH33eQKBgQCPG0dQT52HiS2afdBsk2A6MQyDAtVQ6PUu/JB/MxSWtl2ZXh5z +CsWOZ9Tg+c3jeRjsiGY6nYYPsntjvL9EbPkjyg++FQ4tBCBlK06ESdJsUhaH46WJ +Io4lWhvZJ1mRdaVKE98sC8FDbvg6ozNlezGNktXjs9ziGvFkMT4RC41QgQKBgGHY +eh5+S3ML6KLHOJbzL3J55SfM4Z2KZaEl1GotoQ+qgrdrGwcV2qIb9V7/G6+bgXqa +ivKyIwEcs02zfXIaLVwQFu7dg8hEVbZEIe3v9vGDaPYLC6T4GNSp8h3jxjx/B7w8 ++6cZ82kvXpVKcn9mnWlFZ1maVebFc5gloBPSbyJhAoGAJ2p1TzI8/c2xVOxhE0AO +WTcpIiYNiHMotIBruSl9I2siaKklcyQKaMieAFQgylhsiORhKq0hUfMPQEfFcYL5 +mOPEzqf5/FBAFvzNxgk8lvXXM11TuZoKEFg5LTjgdHojl74urWk5asN4OziCZ+qI +W3r7lHub6JynJhqe91sJmnE= +-----END PRIVATE KEY----- diff --git a/certs/ocsp.cnf b/certs/ocsp.cnf new file mode 100644 index 00000000..8a921a78 --- /dev/null +++ b/certs/ocsp.cnf @@ -0,0 +1,5 @@ +[ v3_ocsp ] +basicConstraints = CA:false +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer:always +extendedKeyUsage = OCSPSigning diff --git a/certs/renew.sh b/certs/renew.sh index 3089754f..ad0363bf 100755 --- a/certs/renew.sh +++ b/certs/renew.sh @@ -46,6 +46,17 @@ cp $CERTS_DIR/intermediate/client-int-cert.pem . #cp $CERTS_DIR/../examples/client/client.c ../src/client/client.c #sed -i '' "s/examples\/client\//wolfclu\//" ../src/client/client.c +echo "Generate OCSP responder certificate" +openssl genrsa -out ocsp-responder-key.pem 2048 +openssl req -new -key ocsp-responder-key.pem -out ocsp-responder.csr \ + -subj "/C=US/ST=Montana/L=Bozeman/O=wolfSSL OCSP/OU=Responder/CN=OCSP Responder" +openssl x509 -req -in ocsp-responder.csr \ + -CA ca-cert.pem -CAkey ca-key.pem \ + -extfile ocsp.cnf -extensions v3_ocsp \ + -days 1000 -set_serial 100 -out ocsp-responder-cert.pem +rm ocsp-responder.csr +echo "OCSP responder certificate created" + echo "Recreate expected encrypted data with new files" openssl enc -aes-256-cbc -nosalt -in ./crl.der -out ./crl.der.enc -k "" openssl enc -base64 -aes-256-cbc -nosalt -in ./crl.der -out ./crl.der.enc.base64 -k "" diff --git a/src/client/client.c b/src/client/client.c index 94242757..45253805 100644 --- a/src/client/client.c +++ b/src/client/client.c @@ -36,6 +36,8 @@ #include +#include + #define HAVE_GETADDRINFO 1 #define HAVE_ERRNO_H 1 @@ -389,7 +391,6 @@ static const char kResumeMsg[] = "resuming wolfssl!" TEST_STR_TERM; #if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EARLY_DATA) static const char kEarlyMsg[] = "A drop of info" TEST_STR_TERM; #endif -static const char kHttpGetMsg[] = "GET /index.html HTTP/1.0\r\n\r\n"; /* Write needs to be largest of the above strings (29) */ #define CLI_MSG_SZ 32 @@ -943,7 +944,8 @@ static int ClientBenchmarkConnections(WOLFSSL_CTX* ctx, char* host, word16 port, #endif { /* no null term */ - if (wolfSSL_write(ssl, kHttpGetMsg, sizeof(kHttpGetMsg)-1) <= 0) + if (wolfSSL_write(ssl, wolfCLU_GetDefaultHttpGet(), + wolfCLU_GetDefaultHttpGetLength()) <= 0) err_sys("SSL_write failed"); if (wolfSSL_read(ssl, reply, sizeof(reply)-1) <= 0) @@ -4141,8 +4143,8 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args) if (sendGET) { printf("SSL connect ok, sending GET...\n"); - msgSz = (int)XSTRLEN(kHttpGetMsg); - XMEMCPY(msg, kHttpGetMsg, msgSz); + msgSz = wolfCLU_GetDefaultHttpGetLength(); + XMEMCPY(msg, wolfCLU_GetDefaultHttpGet(), msgSz); } else { msgSz = (int)XSTRLEN(kHelloMsg); @@ -4436,8 +4438,8 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args) XMEMSET(msg, 0, sizeof(msg)); if (sendGET) { - msgSz = (int)XSTRLEN(kHttpGetMsg); - XMEMCPY(msg, kHttpGetMsg, msgSz); + msgSz = wolfCLU_GetDefaultHttpGetLength(); + XMEMCPY(msg, wolfCLU_GetDefaultHttpGet(), msgSz); } else { msgSz = (int)XSTRLEN(kResumeMsg); diff --git a/src/clu_main.c b/src/clu_main.c index 6cbf55ff..7a90f5a6 100644 --- a/src/clu_main.c +++ b/src/clu_main.c @@ -74,6 +74,9 @@ static const struct option mode_options[] = { {"rand", no_argument, 0, WOLFCLU_RAND }, {"dsaparam", no_argument, 0, WOLFCLU_DSA }, {"dhparam", no_argument, 0, WOLFCLU_DH }, +#if defined(HAVE_OCSP) && defined(HAVE_OCSP_RESPONDER) + {"ocsp", no_argument, 0, WOLFCLU_OCSP }, +#endif {"base64", no_argument, 0, WOLFCLU_BASE64 }, {"help", no_argument, 0, WOLFCLU_HELP }, {"h", no_argument, 0, WOLFCLU_HELP }, @@ -319,6 +322,12 @@ int main(int argc, char** argv) ret = wolfCLU_DhParamSetup(argc, argv); break; +#if defined(HAVE_OCSP) && defined(HAVE_OCSP_RESPONDER) + case WOLFCLU_OCSP: + ret = wolfCLU_OcspSetup(argc, argv); + break; +#endif + case WOLFCLU_BASE64: ret = wolfCLU_Base64Setup(argc, argv); break; diff --git a/src/include.am b/src/include.am index 0f469427..d79caeea 100644 --- a/src/include.am +++ b/src/include.am @@ -46,6 +46,10 @@ wolfssl_SOURCES = src/clu_main.c \ src/server/clu_server_setup.c \ src/pkcs/clu_pkcs7.c \ src/pkcs/clu_pkcs8.c \ - src/tools/clu_base64.c + src/tools/clu_base64.c \ + src/tools/clu_http.c \ + src/tools/clu_scgi.c \ + src/tools/clu_pem_der.c \ + src/ocsp/clu_ocsp.c diff --git a/src/ocsp/clu_ocsp.c b/src/ocsp/clu_ocsp.c new file mode 100644 index 00000000..1fbe47fe --- /dev/null +++ b/src/ocsp/clu_ocsp.c @@ -0,0 +1,1335 @@ +/* clu_ocsp.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include + +#if defined(HAVE_OCSP) && defined(HAVE_OCSP_RESPONDER) + +#include + +enum { + WOLFCLU_OCSP_HELP = 2100, + WOLFCLU_OCSP_IGNORE_ERR, + WOLFCLU_OCSP_CAFILE, + WOLFCLU_OCSP_CAPATH, + WOLFCLU_OCSP_CASTORE, + WOLFCLU_OCSP_NO_CAFILE, + WOLFCLU_OCSP_NO_CAPATH, + WOLFCLU_OCSP_NO_CASTORE, + WOLFCLU_OCSP_TIMEOUT, + WOLFCLU_OCSP_RESP_NO_CERTS, + WOLFCLU_OCSP_MULTI, + WOLFCLU_OCSP_NO_CERTS, + WOLFCLU_OCSP_BADSIG, + WOLFCLU_OCSP_CA, + WOLFCLU_OCSP_NMIN, + WOLFCLU_OCSP_NREQUEST, + WOLFCLU_OCSP_REQIN, + WOLFCLU_OCSP_SIGNER, + WOLFCLU_OCSP_SIGN_OTHER, + WOLFCLU_OCSP_INDEX, + WOLFCLU_OCSP_NDAYS, + WOLFCLU_OCSP_RSIGNER, + WOLFCLU_OCSP_RKEY, + WOLFCLU_OCSP_PASSIN, + WOLFCLU_OCSP_ROTHER, + WOLFCLU_OCSP_RMD, + WOLFCLU_OCSP_RSIGOPT, + WOLFCLU_OCSP_HEADER, + WOLFCLU_OCSP_RCID, + WOLFCLU_OCSP_URL, + WOLFCLU_OCSP_HOST, + WOLFCLU_OCSP_PORT, + WOLFCLU_OCSP_PATH, + WOLFCLU_OCSP_PROXY, + WOLFCLU_OCSP_NO_PROXY, + WOLFCLU_OCSP_OUT, + WOLFCLU_OCSP_NOVERIFY, + WOLFCLU_OCSP_NONCE, + WOLFCLU_OCSP_NO_NONCE, + WOLFCLU_OCSP_NO_SIGNATURE_VERIFY, + WOLFCLU_OCSP_RESP_KEY_ID, + WOLFCLU_OCSP_NO_CERT_VERIFY, + WOLFCLU_OCSP_TEXT, + WOLFCLU_OCSP_REQ_TEXT, + WOLFCLU_OCSP_RESP_TEXT, + WOLFCLU_OCSP_NO_CHAIN, + WOLFCLU_OCSP_NO_CERT_CHECKS, + WOLFCLU_OCSP_NO_EXPLICIT, + WOLFCLU_OCSP_TRUST_OTHER, + WOLFCLU_OCSP_NO_INTERN, + WOLFCLU_OCSP_RESPIN, + WOLFCLU_OCSP_VAFILE, + WOLFCLU_OCSP_VERIFY_OTHER, + WOLFCLU_OCSP_CERT, + WOLFCLU_OCSP_SERIAL, + WOLFCLU_OCSP_VALIDITY_PERIOD, + WOLFCLU_OCSP_SIGNKEY, + WOLFCLU_OCSP_REQOUT, + WOLFCLU_OCSP_RESPOUT, + WOLFCLU_OCSP_ISSUER, + WOLFCLU_OCSP_STATUS_AGE, + WOLFCLU_OCSP_POLICY, + WOLFCLU_OCSP_PURPOSE, + WOLFCLU_OCSP_VERIFY_NAME, + WOLFCLU_OCSP_VERIFY_DEPTH, + WOLFCLU_OCSP_AUTH_LEVEL, + WOLFCLU_OCSP_ATTIME, + WOLFCLU_OCSP_VERIFY_HOSTNAME, + WOLFCLU_OCSP_VERIFY_EMAIL, + WOLFCLU_OCSP_VERIFY_IP, + WOLFCLU_OCSP_IGNORE_CRITICAL, + WOLFCLU_OCSP_ISSUER_CHECKS, + WOLFCLU_OCSP_CRL_CHECK, + WOLFCLU_OCSP_CRL_CHECK_ALL, + WOLFCLU_OCSP_POLICY_CHECK, + WOLFCLU_OCSP_EXPLICIT_POLICY, + WOLFCLU_OCSP_INHIBIT_ANY, + WOLFCLU_OCSP_INHIBIT_MAP, + WOLFCLU_OCSP_X509_STRICT, + WOLFCLU_OCSP_EXTENDED_CRL, + WOLFCLU_OCSP_USE_DELTAS, + WOLFCLU_OCSP_POLICY_PRINT, + WOLFCLU_OCSP_CHECK_SS_SIG, + WOLFCLU_OCSP_TRUSTED_FIRST, + WOLFCLU_OCSP_SUITEB_128_ONLY, + WOLFCLU_OCSP_SUITEB_128, + WOLFCLU_OCSP_SUITEB_192, + WOLFCLU_OCSP_PARTIAL_CHAIN, + WOLFCLU_OCSP_NO_ALT_CHAINS, + WOLFCLU_OCSP_NO_CHECK_TIME, + WOLFCLU_OCSP_ALLOW_PROXY_CERTS, + WOLFCLU_OCSP_PROVIDER_PATH, + WOLFCLU_OCSP_PROVIDER, + WOLFCLU_OCSP_PROPQUERY, + WOLFCLU_OCSP_SCGI, +}; + +typedef struct OcspClientConfig { + const char* caFile; + const char* issuer; + const char* url; + const char* cert; + int noNonce; +} OcspClientConfig; + +typedef struct OcspResponderConfig { + word16 port; + const char* indexFile; + const char* caFile; + const char* rsignerFile; + const char* rkeyFile; + int nrequest; + int scgiMode; +} OcspResponderConfig; + +static const struct option ocsp_options[] = { + /* General options */ + {"-help", no_argument, 0, WOLFCLU_OCSP_HELP }, + {"-ignore_err", no_argument, 0, WOLFCLU_OCSP_IGNORE_ERR }, + {"-CAfile", required_argument, 0, WOLFCLU_OCSP_CAFILE }, + {"-CApath", required_argument, 0, WOLFCLU_OCSP_CAPATH }, + {"-CAstore", required_argument, 0, WOLFCLU_OCSP_CASTORE }, + {"-no-CAfile", no_argument, 0, WOLFCLU_OCSP_NO_CAFILE }, + {"-no-CApath", no_argument, 0, WOLFCLU_OCSP_NO_CAPATH }, + {"-no-CAstore", no_argument, 0, WOLFCLU_OCSP_NO_CASTORE }, + + /* Responder options */ + {"-timeout", required_argument, 0, WOLFCLU_OCSP_TIMEOUT }, + {"-resp_no_certs", no_argument, 0, WOLFCLU_OCSP_RESP_NO_CERTS }, + {"-multi", required_argument, 0, WOLFCLU_OCSP_MULTI }, + {"-no_certs", no_argument, 0, WOLFCLU_OCSP_NO_CERTS }, + {"-badsig", no_argument, 0, WOLFCLU_OCSP_BADSIG }, + {"-CA", required_argument, 0, WOLFCLU_OCSP_CA }, + {"-nmin", required_argument, 0, WOLFCLU_OCSP_NMIN }, + {"-nrequest", required_argument, 0, WOLFCLU_OCSP_NREQUEST }, + {"-reqin", required_argument, 0, WOLFCLU_OCSP_REQIN }, + {"-signer", required_argument, 0, WOLFCLU_OCSP_SIGNER }, + {"-sign_other", required_argument, 0, WOLFCLU_OCSP_SIGN_OTHER }, + {"-index", required_argument, 0, WOLFCLU_OCSP_INDEX }, + {"-ndays", required_argument, 0, WOLFCLU_OCSP_NDAYS }, + {"-rsigner", required_argument, 0, WOLFCLU_OCSP_RSIGNER }, + {"-rkey", required_argument, 0, WOLFCLU_OCSP_RKEY }, + {"-passin", required_argument, 0, WOLFCLU_OCSP_PASSIN }, + {"-rother", required_argument, 0, WOLFCLU_OCSP_ROTHER }, + {"-rmd", required_argument, 0, WOLFCLU_OCSP_RMD }, + {"-rsigopt", required_argument, 0, WOLFCLU_OCSP_RSIGOPT }, + {"-header", required_argument, 0, WOLFCLU_OCSP_HEADER }, + {"-rcid", required_argument, 0, WOLFCLU_OCSP_RCID }, + + /* Client options */ + {"-url", required_argument, 0, WOLFCLU_OCSP_URL }, + {"-host", required_argument, 0, WOLFCLU_OCSP_HOST }, + {"-port", required_argument, 0, WOLFCLU_OCSP_PORT }, + {"-path", required_argument, 0, WOLFCLU_OCSP_PATH }, + {"-proxy", required_argument, 0, WOLFCLU_OCSP_PROXY }, + {"-no_proxy", required_argument, 0, WOLFCLU_OCSP_NO_PROXY }, + {"-out", required_argument, 0, WOLFCLU_OCSP_OUT }, + {"-noverify", no_argument, 0, WOLFCLU_OCSP_NOVERIFY }, + {"-nonce", no_argument, 0, WOLFCLU_OCSP_NONCE }, + {"-no_nonce", no_argument, 0, WOLFCLU_OCSP_NO_NONCE }, + {"-no_signature_verify", no_argument, 0, WOLFCLU_OCSP_NO_SIGNATURE_VERIFY}, + {"-resp_key_id", no_argument, 0, WOLFCLU_OCSP_RESP_KEY_ID }, + {"-no_cert_verify", no_argument, 0, WOLFCLU_OCSP_NO_CERT_VERIFY }, + {"-text", no_argument, 0, WOLFCLU_OCSP_TEXT }, + {"-req_text", no_argument, 0, WOLFCLU_OCSP_REQ_TEXT }, + {"-resp_text", no_argument, 0, WOLFCLU_OCSP_RESP_TEXT }, + {"-no_chain", no_argument, 0, WOLFCLU_OCSP_NO_CHAIN }, + {"-no_cert_checks", no_argument, 0, WOLFCLU_OCSP_NO_CERT_CHECKS }, + {"-no_explicit", no_argument, 0, WOLFCLU_OCSP_NO_EXPLICIT }, + {"-trust_other", no_argument, 0, WOLFCLU_OCSP_TRUST_OTHER }, + {"-no_intern", no_argument, 0, WOLFCLU_OCSP_NO_INTERN }, + {"-respin", required_argument, 0, WOLFCLU_OCSP_RESPIN }, + {"-VAfile", required_argument, 0, WOLFCLU_OCSP_VAFILE }, + {"-verify_other", required_argument, 0, WOLFCLU_OCSP_VERIFY_OTHER }, + {"-cert", required_argument, 0, WOLFCLU_OCSP_CERT }, + {"-serial", required_argument, 0, WOLFCLU_OCSP_SERIAL }, + {"-validity_period", required_argument, 0, WOLFCLU_OCSP_VALIDITY_PERIOD }, + {"-signkey", required_argument, 0, WOLFCLU_OCSP_SIGNKEY }, + {"-reqout", required_argument, 0, WOLFCLU_OCSP_REQOUT }, + {"-respout", required_argument, 0, WOLFCLU_OCSP_RESPOUT }, + {"-issuer", required_argument, 0, WOLFCLU_OCSP_ISSUER }, + {"-status_age", required_argument, 0, WOLFCLU_OCSP_STATUS_AGE }, + + /* Validation options */ + {"-policy", required_argument, 0, WOLFCLU_OCSP_POLICY }, + {"-purpose", required_argument, 0, WOLFCLU_OCSP_PURPOSE }, + {"-verify_name", required_argument, 0, WOLFCLU_OCSP_VERIFY_NAME }, + {"-verify_depth", required_argument, 0, WOLFCLU_OCSP_VERIFY_DEPTH }, + {"-auth_level", required_argument, 0, WOLFCLU_OCSP_AUTH_LEVEL }, + {"-attime", required_argument, 0, WOLFCLU_OCSP_ATTIME }, + {"-verify_hostname", required_argument, 0, WOLFCLU_OCSP_VERIFY_HOSTNAME }, + {"-verify_email", required_argument, 0, WOLFCLU_OCSP_VERIFY_EMAIL }, + {"-verify_ip", required_argument, 0, WOLFCLU_OCSP_VERIFY_IP }, + {"-ignore_critical", no_argument, 0, WOLFCLU_OCSP_IGNORE_CRITICAL }, + {"-issuer_checks", no_argument, 0, WOLFCLU_OCSP_ISSUER_CHECKS }, + {"-crl_check", no_argument, 0, WOLFCLU_OCSP_CRL_CHECK }, + {"-crl_check_all", no_argument, 0, WOLFCLU_OCSP_CRL_CHECK_ALL }, + {"-policy_check", no_argument, 0, WOLFCLU_OCSP_POLICY_CHECK }, + {"-explicit_policy", no_argument, 0, WOLFCLU_OCSP_EXPLICIT_POLICY }, + {"-inhibit_any", no_argument, 0, WOLFCLU_OCSP_INHIBIT_ANY }, + {"-inhibit_map", no_argument, 0, WOLFCLU_OCSP_INHIBIT_MAP }, + {"-x509_strict", no_argument, 0, WOLFCLU_OCSP_X509_STRICT }, + {"-extended_crl", no_argument, 0, WOLFCLU_OCSP_EXTENDED_CRL }, + {"-use_deltas", no_argument, 0, WOLFCLU_OCSP_USE_DELTAS }, + {"-policy_print", no_argument, 0, WOLFCLU_OCSP_POLICY_PRINT }, + {"-check_ss_sig", no_argument, 0, WOLFCLU_OCSP_CHECK_SS_SIG }, + {"-trusted_first", no_argument, 0, WOLFCLU_OCSP_TRUSTED_FIRST }, + {"-suiteB_128_only", no_argument, 0, WOLFCLU_OCSP_SUITEB_128_ONLY }, + {"-suiteB_128", no_argument, 0, WOLFCLU_OCSP_SUITEB_128 }, + {"-suiteB_192", no_argument, 0, WOLFCLU_OCSP_SUITEB_192 }, + {"-partial_chain", no_argument, 0, WOLFCLU_OCSP_PARTIAL_CHAIN }, + {"-no_alt_chains", no_argument, 0, WOLFCLU_OCSP_NO_ALT_CHAINS }, + {"-no_check_time", no_argument, 0, WOLFCLU_OCSP_NO_CHECK_TIME }, + {"-allow_proxy_certs", no_argument, 0, WOLFCLU_OCSP_ALLOW_PROXY_CERTS }, + + /* Provider options */ + {"-provider-path", required_argument, 0, WOLFCLU_OCSP_PROVIDER_PATH }, + {"-provider", required_argument, 0, WOLFCLU_OCSP_PROVIDER }, + {"-propquery", required_argument, 0, WOLFCLU_OCSP_PROPQUERY }, + + /* SCGI mode */ + {"-scgi", no_argument, 0, WOLFCLU_OCSP_SCGI }, + + {0, 0, 0, 0} /* terminal element */ +}; + +static void wolfCLU_OcspHelp(void) +{ + WOLFCLU_LOG(WOLFCLU_L0, "wolfssl ocsp [options]"); + WOLFCLU_LOG(WOLFCLU_L0, "OCSP utility - client and responder"); + WOLFCLU_LOG(WOLFCLU_L0, " "); + WOLFCLU_LOG(WOLFCLU_L0, "General options:"); + WOLFCLU_LOG(WOLFCLU_L0, " -help Display this summary"); + WOLFCLU_LOG(WOLFCLU_L0, " -ignore_err Ignore error on OCSP request or response"); + WOLFCLU_LOG(WOLFCLU_L0, " -CAfile file Trusted certificates file"); + WOLFCLU_LOG(WOLFCLU_L0, " -CApath dir Trusted certificates directory"); + WOLFCLU_LOG(WOLFCLU_L0, " "); + WOLFCLU_LOG(WOLFCLU_L0, "Client mode (specify -cert to use):"); + WOLFCLU_LOG(WOLFCLU_L0, " -url val Responder URL (overrides AIA in cert)"); + WOLFCLU_LOG(WOLFCLU_L0, " -host val TCP/IP hostname:port to connect to"); + WOLFCLU_LOG(WOLFCLU_L0, " -path val Path to use in OCSP request"); + WOLFCLU_LOG(WOLFCLU_L0, " -cert file Certificate to check"); + WOLFCLU_LOG(WOLFCLU_L0, " -issuer file Issuer certificate"); + WOLFCLU_LOG(WOLFCLU_L0, " -serial val Serial number to check"); + WOLFCLU_LOG(WOLFCLU_L0, " -nonce Add OCSP nonce to request"); + WOLFCLU_LOG(WOLFCLU_L0, " -no_nonce Don't add OCSP nonce to request"); + WOLFCLU_LOG(WOLFCLU_L0, " -out file Output filename"); + WOLFCLU_LOG(WOLFCLU_L0, " -text Print text form of request and response"); + WOLFCLU_LOG(WOLFCLU_L0, " "); + WOLFCLU_LOG(WOLFCLU_L0, "Responder mode (specify -port to use):"); + WOLFCLU_LOG(WOLFCLU_L0, " -port num Port to run responder on"); + WOLFCLU_LOG(WOLFCLU_L0, " -index file Certificate status index file"); + WOLFCLU_LOG(WOLFCLU_L0, " -rsigner file Responder certificate to sign responses"); + WOLFCLU_LOG(WOLFCLU_L0, " -rkey file Responder key to sign responses"); + WOLFCLU_LOG(WOLFCLU_L0, " -CA file CA certificate"); + WOLFCLU_LOG(WOLFCLU_L0, " -scgi Use SCGI protocol (for web server reverse proxy)"); + WOLFCLU_LOG(WOLFCLU_L0, " "); +} + +static int ocspClient(OcspClientConfig* config) +{ + WOLFSSL_CERT_MANAGER* cm = NULL; + byte* der = NULL; + int derSz; + int ocspFlags = 0; + int ret = WOLFCLU_SUCCESS; + + if (config->cert == NULL) { + wolfCLU_LogError("Client mode requires -cert"); + return WOLFCLU_FATAL_ERROR; + } + + if (config->issuer == NULL) { + wolfCLU_LogError("Client mode requires -issuer"); + return WOLFCLU_FATAL_ERROR; + } + + /* Read the certificate to check into a DER buffer */ + derSz = wolfCLU_ReadCertDer(config->cert, &der); + if (derSz <= 0) { + wolfCLU_LogError("Failed to read certificate %s", config->cert); + return WOLFCLU_FATAL_ERROR; + } + + cm = wolfSSL_CertManagerNew(); + if (cm == NULL) { + wolfCLU_LogError("Failed to create CertManager"); + XFREE(der, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + return WOLFCLU_FATAL_ERROR; + } + + /* Load trusted CA certificates and issuer */ + if (ret == WOLFCLU_SUCCESS && config->issuer != NULL) { + if (wolfSSL_CertManagerLoadCA(cm, config->issuer, NULL) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Unable to load issuer file %s", config->issuer); + ret = WOLFCLU_FATAL_ERROR; + } + } + + if (ret == WOLFCLU_SUCCESS && config->caFile != NULL) { + if (wolfSSL_CertManagerLoadCA(cm, config->caFile, NULL) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Unable to load CA file %s", config->caFile); + ret = WOLFCLU_FATAL_ERROR; + } + } + + /* Build OCSP flags */ + if (config->noNonce) + ocspFlags |= WOLFSSL_OCSP_NO_NONCE; + if (config->url != NULL) + ocspFlags |= WOLFSSL_OCSP_URL_OVERRIDE; + + /* Enable OCSP in the CertManager */ + if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_CertManagerEnableOCSP(cm, ocspFlags) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Failed to enable OCSP"); + ret = WOLFCLU_FATAL_ERROR; + } + } + + /* Set override URL if provided */ + if (ret == WOLFCLU_SUCCESS && config->url != NULL) { + if (wolfSSL_CertManagerSetOCSPOverrideURL(cm, config->url) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Failed to set OCSP override URL"); + ret = WOLFCLU_FATAL_ERROR; + } + } + + /* Perform the OCSP check – wolfSSL handles networking and HTTP */ + if (ret == WOLFCLU_SUCCESS) { + int ocspRet = wolfSSL_CertManagerCheckOCSP(cm, der, derSz); + if (ocspRet == WOLFSSL_SUCCESS) { + WOLFCLU_LOG(WOLFCLU_L0, "%s: good", config->cert); + } + else if (ocspRet == OCSP_CERT_REVOKED) { + /* Certificate is revoked - report it but return success. + * OpenSSL returns exit code 0 for revoked certs because the OCSP + * transaction itself succeeded. The revocation status is in output. */ + WOLFCLU_LOG(WOLFCLU_L0, "%s: revoked", config->cert); + /* ret remains WOLFCLU_SUCCESS */ + } + else { + /* Other OCSP errors (network, malformed response, etc.) */ + wolfCLU_LogError("OCSP check failed for %s (err %d: %s)", + config->cert, ocspRet, + wolfSSL_ERR_reason_error_string(ocspRet)); + ret = WOLFCLU_FATAL_ERROR; + } + } + + wolfSSL_CertManagerFree(cm); + XFREE(der, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + return ret; +} + +/* Index file entry structure */ +typedef struct IndexEntry { + char status; + time_t revocationTime; + char serial[64]; + struct IndexEntry* next; +} IndexEntry; + +/* Free index entries linked list */ +static void freeIndexEntries(IndexEntry* head) +{ + while (head) { + IndexEntry* next = head->next; + XFREE(head, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + head = next; + } +} + +/* Parse OpenSSL index.txt file */ +static IndexEntry* parseIndexFile(const char* filename) +{ + XFILE f = XBADFILE; + char line[1024]; + IndexEntry* head = NULL; + IndexEntry* tail = NULL; + IndexEntry* entry = NULL; + + if (filename == NULL) { + return NULL; + } + + f = XFOPEN(filename, "r"); + if (f == XBADFILE) { + wolfCLU_LogError("Error opening index file: %s", filename); + return NULL; + } + + while (XFGETS(line, sizeof(line), f) != NULL) { + char* p = line; + char* field; + int fieldNum = 0; + + /* Skip empty lines */ + if (line[0] == '\n' || line[0] == '\r' || line[0] == '\0') + continue; + + entry = (IndexEntry*)XMALLOC(sizeof(IndexEntry), HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (entry == NULL) { + goto cleanup; + } + XMEMSET(entry, 0, sizeof(IndexEntry)); + + /* Parse tab-separated fields */ + while ((field = XSTRSEP(&p, "\t")) != NULL && fieldNum < 6) { + switch (fieldNum) { + case 0: /* Status */ + entry->status = field[0]; + break; + case 2: /* Revocation time */ + if (field[0] != '\0') { + struct tm tm; + XMEMSET(&tm, 0, sizeof(tm)); + if (XSTRLEN(field) >= 12) { + int year = (field[0] - '0') * 10 + (field[1] - '0'); + tm.tm_year = (year < 50) ? (100 + year) : year; + tm.tm_mon = (field[2] - '0') * 10 + (field[3] - '0') - 1; + tm.tm_mday = (field[4] - '0') * 10 + (field[5] - '0'); + tm.tm_hour = (field[6] - '0') * 10 + (field[7] - '0'); + tm.tm_min = (field[8] - '0') * 10 + (field[9] - '0'); + tm.tm_sec = (field[10] - '0') * 10 + (field[11] - '0'); + entry->revocationTime = XMKTIME(&tm); + } + } + break; + case 3: /* Serial (hex) */ + XSTRNCPY(entry->serial, field, sizeof(entry->serial) - 1); + break; + } + fieldNum++; + } + + /* Validate entry */ + if (fieldNum < 4 || entry->serial[0] == '\0') { + XFREE(entry, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + entry = NULL; + continue; + } + + /* For revoked certificates, revocationTime must be valid */ + if (entry->status == 'R' && entry->revocationTime == (time_t)-1) { + wolfCLU_LogError("Invalid revocation time for serial %s", entry->serial); + XFREE(entry, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + entry = NULL; + continue; + } + + /* Add to list */ + entry->next = NULL; + if (tail) { + tail->next = entry; + tail = entry; + } + else { + head = tail = entry; + } + entry = NULL; + } + +cleanup: + if (f != XBADFILE) + XFCLOSE(f); + if (entry != NULL) + XFREE(entry, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + return head; +} + +static byte reqBuffer[16384]; +static byte respBuffer[16384]; + +/* Signal handling for graceful shutdown */ +#ifndef _WIN32 + #include + #include +#endif + +static volatile sig_atomic_t shutdownRequested = 0; + +#ifndef _WIN32 +/* Signal handler for SIGINT and SIGTERM - sets shutdown flag */ +static void ocspSignalHandler(int sig) +{ + int saved_errno = errno; + (void)sig; + shutdownRequested = 1; + errno = saved_errno; +} + +/* Setup signal handlers without SA_RESTART to allow accept() interruption */ +static void setupSignalHandlers(void) +{ + struct sigaction sa; + XMEMSET(&sa, 0, sizeof(sa)); + sa.sa_handler = ocspSignalHandler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; /* NO SA_RESTART - allow accept() to be interrupted */ + + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); +} +#endif + +enum TRANSPORT_TYPE { + TRANSPORT_NONE = 0, + TRANSPORT_HTTP, + TRANSPORT_SCGI +}; + +/* Read OCSP request from transport layer */ +static int transportReadRequest(SOCKET_T clientfd, enum TRANSPORT_TYPE transportType, const byte** ocspReq, int* ocspReqSz) +{ + if (transportType == TRANSPORT_HTTP) { + int recvLen; + + recvLen = wolfCLU_HttpServerRecv(clientfd, reqBuffer, sizeof(reqBuffer)); + if (recvLen <= 0) { + return WOLFCLU_FATAL_ERROR; + } + + if (wolfCLU_HttpServerParseRequest(reqBuffer, recvLen, ocspReq, ocspReqSz) != 0 || + *ocspReq == NULL || *ocspReqSz <= 0) { + wolfCLU_HttpServerSendError(clientfd, 400, "Bad Request"); + return WOLFCLU_FATAL_ERROR; + } + + return WOLFCLU_SUCCESS; + } + else if (transportType == TRANSPORT_SCGI) { + ScgiRequest scgiReq; + int ret = wolfCLU_ScgiReadRequest(clientfd, reqBuffer, sizeof(reqBuffer), &scgiReq); + if (ret != 0) { + return WOLFCLU_FATAL_ERROR; + } + + /* Validate request method */ + if (scgiReq.requestMethod == NULL || + XSTRCMP(scgiReq.requestMethod, "POST") != 0) { + wolfCLU_ScgiSendError(clientfd, 405, "Method Not Allowed"); + return WOLFCLU_FATAL_ERROR; + } + + /* Validate we have a body */ + if (scgiReq.body == NULL || scgiReq.bodyLen <= 0) { + wolfCLU_ScgiSendError(clientfd, 400, "Bad Request"); + return WOLFCLU_FATAL_ERROR; + } + + *ocspReq = scgiReq.body; + *ocspReqSz = scgiReq.bodyLen; + + return WOLFCLU_SUCCESS; + } + else { + return WOLFCLU_FATAL_ERROR; + } +} + +/* Send OCSP response via transport layer */ +static int transportSendResponse(SOCKET_T clientfd, enum TRANSPORT_TYPE transportType, const byte* respBuf, int respSz) +{ + if (transportType == TRANSPORT_HTTP) { + return wolfCLU_HttpServerSendOcspResponse(clientfd, respBuf, respSz); + } + else if (transportType == TRANSPORT_SCGI) { + return wolfCLU_ScgiSendResponse(clientfd, 200, "OK", + "application/ocsp-response", + respBuf, respSz); + } + else { + return WOLFCLU_FATAL_ERROR; + } +} + +/* Send error response via transport layer */ +static int transportSendError(SOCKET_T clientfd, enum TRANSPORT_TYPE transportType, int statusCode, const char* statusMsg) +{ + if (transportType == TRANSPORT_HTTP) { + wolfCLU_HttpServerSendError(clientfd, statusCode, statusMsg); + return WOLFCLU_SUCCESS; + } + else if (transportType == TRANSPORT_SCGI) { + return wolfCLU_ScgiSendError(clientfd, statusCode, statusMsg); + } + else { + return WOLFCLU_FATAL_ERROR; + } +} + +static int ocspResponder(OcspResponderConfig* config) +{ + OcspResponder* responder = NULL; + IndexEntry* indexEntries = NULL; + DecodedCert caCert; + SOCKET_T sockfd = INVALID_SOCKET; + SOCKET_T clientfd = INVALID_SOCKET; + int requestsProcessed = 0; + int ret = WOLFCLU_SUCCESS; + const char* caSubject; + word32 caSubjectSz; + byte* caCertDer = NULL; + word32 caCertDerSz = 0; + byte* signerCertDer = NULL; + word32 signerCertDerSz = 0; + byte* signerKeyDer = NULL; + word32 signerKeyDerSz = 0; + enum TRANSPORT_TYPE transportType = config->scgiMode ? TRANSPORT_SCGI : TRANSPORT_HTTP; + + XMEMSET(&caCert, 0, sizeof(caCert)); + + /* Validate required options */ + if (config->caFile == NULL) { + wolfCLU_LogError("Error: CA certificate required (-CA)"); + return WOLFCLU_FATAL_ERROR; + } + if (config->rsignerFile == NULL) { + wolfCLU_LogError("Error: Responder signer certificate required (-rsigner)"); + return WOLFCLU_FATAL_ERROR; + } + if (config->rkeyFile == NULL) { + wolfCLU_LogError("Error: Responder key required (-rkey)"); + return WOLFCLU_FATAL_ERROR; + } + if (config->port == 0) { + wolfCLU_LogError("Error: Port required (-port)"); + return WOLFCLU_FATAL_ERROR; + } + + /* Load CA certificate */ + if (wolfCLU_LoadCertDer(config->caFile, &caCertDer, &caCertDerSz) != 0) { + wolfCLU_LogError("Error loading CA certificate: %s", config->caFile); + ret = WOLFCLU_FATAL_ERROR; + goto cleanup; + } + + /* Load responder signer certificate */ + if (wolfCLU_LoadCertDer(config->rsignerFile, &signerCertDer, &signerCertDerSz) != 0) { + wolfCLU_LogError("Error loading signer certificate: %s", config->rsignerFile); + ret = WOLFCLU_FATAL_ERROR; + goto cleanup; + } + + /* Load responder signer key */ + if (wolfCLU_LoadKeyDer(config->rkeyFile, &signerKeyDer, &signerKeyDerSz) != 0) { + wolfCLU_LogError("Error loading signer key: %s", config->rkeyFile); + ret = WOLFCLU_FATAL_ERROR; + goto cleanup; + } + + /* Parse CA certificate */ + wc_InitDecodedCert(&caCert, caCertDer, caCertDerSz, NULL); + if (wc_ParseCert(&caCert, CERT_TYPE, 0, NULL) != 0) { + wolfCLU_LogError("Error parsing CA certificate"); + ret = WOLFCLU_FATAL_ERROR; + goto cleanup; + } + + caSubject = wc_GetDecodedCertSubject(&caCert, &caSubjectSz); + if (caSubject == NULL || caSubjectSz == 0) { + wolfCLU_LogError("Could not get CA subject"); + ret = WOLFCLU_FATAL_ERROR; + goto cleanup; + } + + /* Load index file if provided */ + if (config->indexFile) { + indexEntries = parseIndexFile(config->indexFile); + if (indexEntries == NULL) { + wolfCLU_LogError("Warning: Could not parse index file: %s", config->indexFile); + } + } + + /* Create OCSP responder */ + responder = wc_OcspResponder_new(NULL, 1); + if (responder == NULL) { + wolfCLU_LogError("Error creating OCSP responder"); + ret = WOLFCLU_FATAL_ERROR; + goto cleanup; + } + + /* Add signer to responder. When the signer cert is the CA itself, pass + * NULL for the issuer cert (direct CA signing). Only pass the CA cert as + * issuer when using an authorized responder (delegated signing). */ + { + const byte* issuerDer = NULL; + word32 issuerDerSz = 0; + if (signerCertDerSz != caCertDerSz || + XMEMCMP(signerCertDer, caCertDer, signerCertDerSz) != 0) { + issuerDer = caCertDer; + issuerDerSz = caCertDerSz; + } + if (wc_OcspResponder_AddSigner(responder, signerCertDer, signerCertDerSz, + signerKeyDer, signerKeyDerSz, issuerDer, issuerDerSz) != 0) { + wolfCLU_LogError("Error adding signer to responder"); + ret = WOLFCLU_FATAL_ERROR; + goto cleanup; + } + } + + /* Populate responder with certificate statuses from index */ + if (indexEntries != NULL) { + IndexEntry* entry; + for (entry = indexEntries; entry != NULL; entry = entry->next) { + byte serial[64]; + word32 serialLen = 0; + enum Ocsp_Cert_Status status; + time_t revTime = 0; + word32 i; + char* p = entry->serial; + + /* Convert hex string to bytes */ + serialLen = (word32)XSTRLEN(entry->serial) / 2; + if (serialLen == 0 || serialLen > sizeof(serial)) { + continue; + } + + for (i = 0; i < serialLen; i++) { + int high = (p[i*2] >= 'A') ? (p[i*2] - 'A' + 10) : + (p[i*2] >= 'a') ? (p[i*2] - 'a' + 10) : (p[i*2] - '0'); + int low = (p[i*2+1] >= 'A') ? (p[i*2+1] - 'A' + 10) : + (p[i*2+1] >= 'a') ? (p[i*2+1] - 'a' + 10) : (p[i*2+1] - '0'); + serial[i] = (byte)((high << 4) | low); + } + + /* Determine status */ + if (entry->status == 'V') { + status = CERT_GOOD; + } + else if (entry->status == 'R') { + status = CERT_REVOKED; + revTime = entry->revocationTime; + } + else { + status = CERT_UNKNOWN; + } + + /* Set validity period: only for CERT_GOOD, must be 0 for others */ + wc_OcspResponder_SetCertStatus(responder, caSubject, caSubjectSz, + serial, serialLen, status, revTime, + CRL_REASON_UNSPECIFIED, + (status == CERT_GOOD) ? 86400 : 0); + } + } + + /* Create and listen on server socket */ + sockfd = wolfCLU_HttpServerListen(&config->port); + if (sockfd == INVALID_SOCKET) { + wolfCLU_LogError("Failed to create server socket on port %d", config->port); + ret = WOLFCLU_FATAL_ERROR; + goto cleanup; + } + + /* Setup signal handlers for graceful shutdown */ + setupSignalHandlers(); + + WOLFCLU_LOG(WOLFCLU_L0, "OCSP responder%s listening on port %d", + (transportType == TRANSPORT_SCGI) ? " (SCGI mode)" : "", config->port); + + /* Main loop - exit on shutdown signal */ + while (!shutdownRequested && + (config->nrequest == 0 || requestsProcessed < config->nrequest)) { + const byte* ocspReq; + int ocspReqSz; + word32 respSz; + + /* Accept connection */ + clientfd = wolfCLU_ServerAccept(sockfd); + if (clientfd == INVALID_SOCKET) { + continue; + } + + /* Read request from transport layer */ + ret = transportReadRequest(clientfd, transportType, &ocspReq, &ocspReqSz); + if (ret != WOLFCLU_SUCCESS) { + break; + } + + /* Process OCSP request and generate response */ + respSz = sizeof(respBuffer); + ret = wc_OcspResponder_WriteResponse(responder, ocspReq, (word32)ocspReqSz, + respBuffer, &respSz); + + if (ret != 0) { + enum Ocsp_Response_Status errStatus; + + /* Map error to OCSP response status */ + switch (ret) { + case ASN_PARSE_E: + errStatus = OCSP_MALFORMED_REQUEST; + break; + case ASN_SIG_HASH_E: + errStatus = OCSP_INTERNAL_ERROR; + break; + case ASN_NO_SIGNER_E: + errStatus = OCSP_UNAUTHORIZED; + break; + case OCSP_CERT_UNKNOWN: + errStatus = OCSP_UNAUTHORIZED; + break; + default: + errStatus = OCSP_INTERNAL_ERROR; + break; + } + + /* Generate OCSP error response */ + respSz = sizeof(respBuffer); + ret = wc_OcspResponder_WriteErrorResponse(errStatus, respBuffer, &respSz); + + if (ret != 0) { + /* If we can't encode an error response, send HTTP/SCGI error */ + transportSendError(clientfd, transportType, 500, "Internal Server Error"); + break; + } + } + + /* Send response via transport layer */ + if (transportSendResponse(clientfd, transportType, respBuffer, (int)respSz) != 0) { + break; + } + + requestsProcessed++; + + /* Check if we've hit the request limit */ + if (config->nrequest > 0 && requestsProcessed >= config->nrequest) { + break; + } + + wolfCLU_ServerClose(clientfd); + clientfd = INVALID_SOCKET; + + if (config->nrequest > 0 && requestsProcessed >= config->nrequest) { + break; + } + } + + ret = WOLFCLU_SUCCESS; + +cleanup: + if (clientfd != INVALID_SOCKET) + wolfCLU_ServerClose(clientfd); + if (sockfd != INVALID_SOCKET) + wolfCLU_ServerClose(sockfd); + + wc_FreeDecodedCert(&caCert); + if (responder) + wc_OcspResponder_free(responder); + if (indexEntries) + freeIndexEntries(indexEntries); + if (caCertDer) + XFREE(caCertDer, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (signerCertDer) + XFREE(signerCertDer, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (signerKeyDer) + XFREE(signerKeyDer, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +} + +int wolfCLU_OcspSetup(int argc, char** argv) +{ + int ret = WOLFCLU_SUCCESS; + int option; + int longIndex = 1; + int isClientMode = 0; + int isResponderMode = 0; + OcspClientConfig clientConfig; + OcspResponderConfig responderConfig; + + XMEMSET(&clientConfig, 0, sizeof(clientConfig)); + XMEMSET(&responderConfig, 0, sizeof(responderConfig)); + + opterr = 0; + optind = 0; + + while ((option = wolfCLU_GetOpt(argc, argv, "", ocsp_options, &longIndex)) != -1) { + switch (option) { + case WOLFCLU_OCSP_HELP: + wolfCLU_OcspHelp(); + return WOLFCLU_SUCCESS; + + case WOLFCLU_OCSP_URL: + isClientMode = 1; + clientConfig.url = optarg; + break; + + case WOLFCLU_OCSP_CERT: + isClientMode = 1; + clientConfig.cert = optarg; + break; + + case WOLFCLU_OCSP_CAFILE: + clientConfig.caFile = optarg; + break; + + case WOLFCLU_OCSP_NO_NONCE: + clientConfig.noNonce = 1; + break; + + case WOLFCLU_OCSP_PORT: + isResponderMode = 1; + responderConfig.port = (word16)XATOI(optarg); + break; + + case WOLFCLU_OCSP_IGNORE_ERR: + wolfCLU_LogError("Option -ignore_err is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_CAPATH: + wolfCLU_LogError("Option -CApath is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_CASTORE: + wolfCLU_LogError("Option -CAstore is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_CAFILE: + wolfCLU_LogError("Option -no-CAfile is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_CAPATH: + wolfCLU_LogError("Option -no-CApath is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_CASTORE: + wolfCLU_LogError("Option -no-CAstore is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_TIMEOUT: + wolfCLU_LogError("Option -timeout is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_RESP_NO_CERTS: + wolfCLU_LogError("Option -resp_no_certs is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_MULTI: + wolfCLU_LogError("Option -multi is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_CERTS: + wolfCLU_LogError("Option -no_certs is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_BADSIG: + wolfCLU_LogError("Option -badsig is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_CA: + responderConfig.caFile = optarg; + break; + + case WOLFCLU_OCSP_NMIN: + wolfCLU_LogError("Option -nmin is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NREQUEST: + responderConfig.nrequest = XATOI(optarg); + break; + + case WOLFCLU_OCSP_REQIN: + wolfCLU_LogError("Option -reqin is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_SIGNER: + wolfCLU_LogError("Option -signer is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_SIGN_OTHER: + wolfCLU_LogError("Option -sign_other is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_INDEX: + responderConfig.indexFile = optarg; + break; + + case WOLFCLU_OCSP_NDAYS: + wolfCLU_LogError("Option -ndays is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_RSIGNER: + responderConfig.rsignerFile = optarg; + break; + + case WOLFCLU_OCSP_RKEY: + responderConfig.rkeyFile = optarg; + break; + + case WOLFCLU_OCSP_SCGI: + responderConfig.scgiMode = 1; + break; + + case WOLFCLU_OCSP_PASSIN: + wolfCLU_LogError("Option -passin is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_ROTHER: + wolfCLU_LogError("Option -rother is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_RMD: + wolfCLU_LogError("Option -rmd is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_RSIGOPT: + wolfCLU_LogError("Option -rsigopt is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_HEADER: + wolfCLU_LogError("Option -header is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_RCID: + wolfCLU_LogError("Option -rcid is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_HOST: + wolfCLU_LogError("Option -host is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_PATH: + wolfCLU_LogError("Option -path is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_PROXY: + wolfCLU_LogError("Option -proxy is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_PROXY: + wolfCLU_LogError("Option -no_proxy is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_OUT: + wolfCLU_LogError("Option -out is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NOVERIFY: + wolfCLU_LogError("Option -noverify is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NONCE: + wolfCLU_LogError("Option -nonce is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_SIGNATURE_VERIFY: + wolfCLU_LogError("Option -no_signature_verify is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_RESP_KEY_ID: + wolfCLU_LogError("Option -resp_key_id is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_CERT_VERIFY: + wolfCLU_LogError("Option -no_cert_verify is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_TEXT: + wolfCLU_LogError("Option -text is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_REQ_TEXT: + wolfCLU_LogError("Option -req_text is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_RESP_TEXT: + wolfCLU_LogError("Option -resp_text is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_CHAIN: + wolfCLU_LogError("Option -no_chain is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_CERT_CHECKS: + wolfCLU_LogError("Option -no_cert_checks is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_EXPLICIT: + wolfCLU_LogError("Option -no_explicit is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_TRUST_OTHER: + wolfCLU_LogError("Option -trust_other is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_INTERN: + wolfCLU_LogError("Option -no_intern is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_RESPIN: + wolfCLU_LogError("Option -respin is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_VAFILE: + wolfCLU_LogError("Option -VAfile is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_VERIFY_OTHER: + wolfCLU_LogError("Option -verify_other is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_SERIAL: + wolfCLU_LogError("Option -serial is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_VALIDITY_PERIOD: + wolfCLU_LogError("Option -validity_period is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_SIGNKEY: + wolfCLU_LogError("Option -signkey is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_REQOUT: + wolfCLU_LogError("Option -reqout is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_RESPOUT: + wolfCLU_LogError("Option -respout is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_ISSUER: + isClientMode = 1; + clientConfig.issuer = optarg; + break; + + case WOLFCLU_OCSP_STATUS_AGE: + wolfCLU_LogError("Option -status_age is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_POLICY: + wolfCLU_LogError("Option -policy is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_PURPOSE: + wolfCLU_LogError("Option -purpose is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_VERIFY_NAME: + wolfCLU_LogError("Option -verify_name is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_VERIFY_DEPTH: + wolfCLU_LogError("Option -verify_depth is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_AUTH_LEVEL: + wolfCLU_LogError("Option -auth_level is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_ATTIME: + wolfCLU_LogError("Option -attime is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_VERIFY_HOSTNAME: + wolfCLU_LogError("Option -verify_hostname is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_VERIFY_EMAIL: + wolfCLU_LogError("Option -verify_email is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_VERIFY_IP: + wolfCLU_LogError("Option -verify_ip is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_IGNORE_CRITICAL: + wolfCLU_LogError("Option -ignore_critical is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_ISSUER_CHECKS: + wolfCLU_LogError("Option -issuer_checks is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_CRL_CHECK: + wolfCLU_LogError("Option -crl_check is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_CRL_CHECK_ALL: + wolfCLU_LogError("Option -crl_check_all is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_POLICY_CHECK: + wolfCLU_LogError("Option -policy_check is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_EXPLICIT_POLICY: + wolfCLU_LogError("Option -explicit_policy is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_INHIBIT_ANY: + wolfCLU_LogError("Option -inhibit_any is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_INHIBIT_MAP: + wolfCLU_LogError("Option -inhibit_map is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_X509_STRICT: + wolfCLU_LogError("Option -x509_strict is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_EXTENDED_CRL: + wolfCLU_LogError("Option -extended_crl is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_USE_DELTAS: + wolfCLU_LogError("Option -use_deltas is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_POLICY_PRINT: + wolfCLU_LogError("Option -policy_print is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_CHECK_SS_SIG: + wolfCLU_LogError("Option -check_ss_sig is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_TRUSTED_FIRST: + wolfCLU_LogError("Option -trusted_first is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_SUITEB_128_ONLY: + wolfCLU_LogError("Option -suiteB_128_only is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_SUITEB_128: + wolfCLU_LogError("Option -suiteB_128 is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_SUITEB_192: + wolfCLU_LogError("Option -suiteB_192 is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_PARTIAL_CHAIN: + wolfCLU_LogError("Option -partial_chain is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_ALT_CHAINS: + wolfCLU_LogError("Option -no_alt_chains is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_NO_CHECK_TIME: + wolfCLU_LogError("Option -no_check_time is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_ALLOW_PROXY_CERTS: + wolfCLU_LogError("Option -allow_proxy_certs is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_PROVIDER_PATH: + wolfCLU_LogError("Option -provider-path is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_PROVIDER: + wolfCLU_LogError("Option -provider is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case WOLFCLU_OCSP_PROPQUERY: + wolfCLU_LogError("Option -propquery is not yet supported"); + return WOLFCLU_FATAL_ERROR; + + case ':': + case '?': + wolfCLU_LogError("Bad argument found"); + wolfCLU_OcspHelp(); + return WOLFCLU_FATAL_ERROR; + + default: + break; + } + } + + if (ret != WOLFCLU_SUCCESS) { + return ret; + } + + if (!(isClientMode ^ isResponderMode)) { + wolfCLU_LogError("Can't detect side (client vs responder) or multiple sides specified"); + wolfCLU_OcspHelp(); + ret = WOLFCLU_FATAL_ERROR; + } + else if (isClientMode) { + ret = ocspClient(&clientConfig); + } + else if (isResponderMode) { + ret = ocspResponder(&responderConfig); + } + else { + wolfCLU_LogError("Unexpected error"); + ret = WOLFCLU_FATAL_ERROR; + } + + WOLFCLU_LOG(WOLFCLU_L0, "wolfssl exiting gracefully"); + + return ret; +} + +#endif /* HAVE_OCSP && HAVE_OCSP_RESPONDER */ diff --git a/src/server/server.c b/src/server/server.c index b73db6a4..c326f55b 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -35,6 +35,8 @@ #undef TEST_OPENSSL_COEXIST /* can't use this option with this example */ #include /* name change portability layer */ +#include + #ifdef HAVE_ECC #include /* wc_ecc_fp_free */ #endif @@ -111,21 +113,6 @@ static const char kReplyMsg[] = "I hear you fa shizzle!"; static const char kReplyMsg[] = "I hear you fa shizzle!\n"; #endif -static const char kHttpServerMsg[] = - "HTTP/1.1 200 OK\r\n" - "Content-Type: text/html\r\n" - "Connection: close\r\n" - "Content-Length: 141\r\n" - "\r\n" - "\r\n" - "\r\n" - "Welcome to wolfSSL!\r\n" - "\r\n" - "\r\n" - "

wolfSSL has successfully performed handshake!

\r\n" - "\r\n" - "\r\n"; - /* Read needs to be largest of the client.c message strings (29) */ #define SRV_READ_SZ 32 @@ -3599,8 +3586,8 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) write_msg_sz = (int)XSTRLEN(kReplyMsg); } else { - write_msg = kHttpServerMsg; - write_msg_sz = (int)XSTRLEN(kHttpServerMsg); + write_msg = wolfCLU_GetDefaultHttpResponse(); + write_msg_sz = wolfCLU_GetDefaultHttpResponseLength(); } ServerWrite(ssl, write_msg, write_msg_sz); diff --git a/src/tools/clu_funcs.c b/src/tools/clu_funcs.c index bb1fbc5c..f52d467e 100644 --- a/src/tools/clu_funcs.c +++ b/src/tools/clu_funcs.c @@ -86,6 +86,9 @@ static const struct option crypt_algo_options[] = { WOLFCLU_LOG(WOLFCLU_L0, "enc / encrypt Encrypt a file or some user input"); WOLFCLU_LOG(WOLFCLU_L0, "hash Hash a file or input"); WOLFCLU_LOG(WOLFCLU_L0, "md5 Creates an MD5 hash"); +#if defined(HAVE_OCSP) && defined(HAVE_OCSP_RESPONDER) + WOLFCLU_LOG(WOLFCLU_L0, "ocsp OCSP utility (client and responder)"); +#endif WOLFCLU_LOG(WOLFCLU_L0, "pkey Used for key operations"); WOLFCLU_LOG(WOLFCLU_L0, "req Request for certificate generation"); WOLFCLU_LOG(WOLFCLU_L0, "-rsa Legacy RSA signing and signature verification"); diff --git a/src/tools/clu_http.c b/src/tools/clu_http.c new file mode 100644 index 00000000..b16841fb --- /dev/null +++ b/src/tools/clu_http.c @@ -0,0 +1,420 @@ +/* clu_http.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include + +#include + +/* Platform-specific socket includes */ +#ifdef _WIN32 + #include + #include + #define SOCKADDR_IN_T struct sockaddr_in + #define CLOSE_SOCKET(s) closesocket(s) +#else + #include + #include + #include + #include + #include + #define SOCKADDR_IN_T struct sockaddr_in + #define CLOSE_SOCKET(s) close(s) + #define SOCKET_ERROR (-1) +#endif + +/* Default HTTP GET request message */ +static const char kDefaultHttpGet[] = "GET /index.html HTTP/1.0\r\n\r\n"; + +/* Default HTTP 200 OK response with HTML */ +static const char kDefaultHttpResponse[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Connection: close\r\n" + "Content-Length: 141\r\n" + "\r\n" + "\r\n" + "\r\n" + "Welcome to wolfSSL!\r\n" + "\r\n" + "\r\n" + "

wolfSSL has successfully performed handshake!

\r\n" + "\r\n" + "\r\n"; + +/** + * @brief Get a simple HTTP GET request string + * @return pointer to static HTTP GET request string + */ +const char* wolfCLU_GetDefaultHttpGet(void) +{ + return kDefaultHttpGet; +} + +/** + * @brief Get the length of the default HTTP GET request (without null terminator) + * @return length of HTTP GET request + */ +int wolfCLU_GetDefaultHttpGetLength(void) +{ + return (int)(sizeof(kDefaultHttpGet) - 1); +} + +/** + * @brief Get a simple HTTP 200 OK response string with HTML content + * @return pointer to static HTTP response string + */ +const char* wolfCLU_GetDefaultHttpResponse(void) +{ + return kDefaultHttpResponse; +} + +/** + * @brief Get the length of the default HTTP response (without null terminator) + * @return length of HTTP response + */ +int wolfCLU_GetDefaultHttpResponseLength(void) +{ + return (int)(sizeof(kDefaultHttpResponse) - 1); +} + +/** + * @brief Build a custom HTTP GET request + * @param path the path to request (e.g., "/index.html") + * @param host optional host header value (can be NULL) + * @param buffer buffer to write the request to + * @param bufferSz size of the buffer + * @return number of bytes written to buffer, or negative on error + */ +int wolfCLU_BuildHttpGet(const char* path, const char* host, char* buffer, + int bufferSz) +{ + int sz = 0; + + if (path == NULL || buffer == NULL || bufferSz < 32) { + return -1; + } + + /* Build GET request */ + sz = XSNPRINTF(buffer, bufferSz, "GET %s HTTP/1.0\r\n", path); + if (sz < 0 || sz >= bufferSz) { + return -1; + } + + /* Add Host header if provided */ + if (host != NULL) { + int hostSz = XSNPRINTF(buffer + sz, bufferSz - sz, + "Host: %s\r\n", host); + if (hostSz < 0 || sz + hostSz >= bufferSz) { + return -1; + } + sz += hostSz; + } + + /* Add final CRLF */ + if (sz + 2 >= bufferSz) { + return -1; + } + buffer[sz++] = '\r'; + buffer[sz++] = '\n'; + buffer[sz] = '\0'; + + return sz; +} + +/** + * @brief Build a simple HTTP response + * @param statusCode HTTP status code (e.g., 200, 404) + * @param statusText HTTP status text (e.g., "OK", "Not Found") + * @param contentType MIME type (e.g., "text/html") + * @param body response body content + * @param buffer buffer to write the response to + * @param bufferSz size of the buffer + * @return number of bytes written to buffer, or negative on error + */ +int wolfCLU_BuildHttpResponse(int statusCode, const char* statusText, + const char* contentType, const char* body, + char* buffer, int bufferSz) +{ + int sz = 0; + int bodySz = 0; + + if (statusText == NULL || buffer == NULL || bufferSz < 64) { + return -1; + } + + if (body != NULL) { + bodySz = (int)XSTRLEN(body); + } + + /* Build status line */ + sz = XSNPRINTF(buffer, bufferSz, "HTTP/1.1 %d %s\r\n", + statusCode, statusText); + if (sz < 0 || sz >= bufferSz) { + return -1; + } + + /* Add Content-Type header */ + if (contentType != NULL) { + int ctSz = XSNPRINTF(buffer + sz, bufferSz - sz, + "Content-Type: %s\r\n", contentType); + if (ctSz < 0 || sz + ctSz >= bufferSz) { + return -1; + } + sz += ctSz; + } + + /* Add Connection header */ + if (sz + 22 >= bufferSz) { + return -1; + } + sz += XSNPRINTF(buffer + sz, bufferSz - sz, "Connection: close\r\n"); + + /* Add Content-Length header */ + if (sz + 30 >= bufferSz) { + return -1; + } + sz += XSNPRINTF(buffer + sz, bufferSz - sz, "Content-Length: %d\r\n", + bodySz); + + /* Add final CRLF before body */ + if (sz + 2 >= bufferSz) { + return -1; + } + buffer[sz++] = '\r'; + buffer[sz++] = '\n'; + + /* Add body if provided */ + if (body != NULL && bodySz > 0) { + if (sz + bodySz >= bufferSz) { + return -1; + } + XMEMCPY(buffer + sz, body, bodySz); + sz += bodySz; + buffer[sz] = '\0'; + } + + return sz; +} + +/** + * @brief Create and bind a server socket using tcp_listen + * @param port port number to bind to (pointer will be updated with actual port) + * @return socket descriptor on success, INVALID_SOCKET on error + */ +SOCKET_T wolfCLU_HttpServerListen(word16* port) +{ + SOCKET_T sockfd = INVALID_SOCKET; + + /* Use tcp_listen from wolfSSL test.h + * Parameters: + * sockfd - socket to create + * port - port to bind to (pointer) + * useAnyAddr - 1 to use INADDR_ANY, 0 for localhost + * udp - 0 for TCP + * sctp - 0 for non-SCTP + */ + tcp_listen(&sockfd, port, 1, 0, 0); + + return sockfd; +} + +/** + * @brief Accept a client connection + * @param serverfd server socket descriptor + * @return client socket descriptor on success, INVALID_SOCKET on error + */ +SOCKET_T wolfCLU_ServerAccept(SOCKET_T serverfd) +{ + SOCKADDR_IN_T clientAddr; + socklen_t clientLen = sizeof(clientAddr); + SOCKET_T clientfd; + + clientfd = accept(serverfd, (struct sockaddr*)&clientAddr, &clientLen); + return clientfd; +} + +/** + * @brief Receive a complete HTTP request + * @param clientfd client socket descriptor + * @param buffer buffer to store request + * @param bufferSz size of buffer + * @return number of bytes received, or negative on error + */ +int wolfCLU_HttpServerRecv(SOCKET_T clientfd, byte* buffer, int bufferSz) +{ + int totalLen = 0; + int contentLen = 0; + int headerSz = 0; + + while (totalLen < bufferSz - 1) { + int n = (int)recv(clientfd, (char*)buffer + totalLen, + (size_t)(bufferSz - 1 - totalLen), 0); + if (n <= 0) + break; + totalLen += n; + buffer[totalLen] = '\0'; + + /* Once we find end-of-headers, parse Content-Length */ + if (headerSz == 0) { + const char* hdrEnd = XSTRSTR((char*)buffer, "\r\n\r\n"); + if (hdrEnd != NULL) { + const char* cl; + headerSz = (int)(hdrEnd + 4 - (char*)buffer); + cl = XSTRSTR((char*)buffer, "Content-Length:"); + if (cl == NULL) + cl = XSTRSTR((char*)buffer, "content-length:"); + if (cl != NULL) + contentLen = XATOI(cl + 15); + } + } + /* Check if we have the full body */ + if (headerSz > 0 && totalLen >= headerSz + contentLen) + break; + } + return totalLen; +} + +/** + * @brief Send an HTTP response with OCSP content + * @param clientfd client socket descriptor + * @param body response body (OCSP response) + * @param bodySz size of response body + * @return 0 on success, negative on error + */ +int wolfCLU_HttpServerSendOcspResponse(SOCKET_T clientfd, const byte* body, + int bodySz) +{ + char header[512]; + int headerLen; + int sent; + + headerLen = XSNPRINTF(header, sizeof(header), + "HTTP/1.0 200 OK\r\n" + "Content-Type: application/ocsp-response\r\n" + "Content-Length: %d\r\n" + "Connection: close\r\n" + "\r\n", bodySz); + + if (headerLen < 0 || headerLen >= (int)sizeof(header)) { + return -1; + } + + /* Send header */ + sent = (int)send(clientfd, header, (size_t)headerLen, 0); + if (sent != headerLen) { + return -1; + } + + /* Send body */ + if (bodySz > 0) { + sent = (int)send(clientfd, (const char*)body, (size_t)bodySz, 0); + if (sent != bodySz) { + return -1; + } + } + + return 0; +} + +/** + * @brief Send an HTTP error response + * @param clientfd client socket descriptor + * @param statusCode HTTP status code + * @param statusMsg HTTP status message + * @return 0 on success, negative on error + */ +int wolfCLU_HttpServerSendError(SOCKET_T clientfd, int statusCode, + const char* statusMsg) +{ + char response[512]; + int len; + int msgLen = (int)XSTRLEN(statusMsg); + + len = XSNPRINTF(response, sizeof(response), + "HTTP/1.0 %d %s\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: %d\r\n" + "Connection: close\r\n" + "\r\n" + "%s", statusCode, statusMsg, msgLen, statusMsg); + + if (len < 0 || len >= (int)sizeof(response)) { + return -1; + } + + return (send(clientfd, response, (size_t)len, 0) == len) ? 0 : -1; +} + +/** + * @brief Close a socket + * @param sockfd socket descriptor to close + */ +void wolfCLU_ServerClose(SOCKET_T sockfd) +{ + if (sockfd != INVALID_SOCKET) { + CLOSE_SOCKET(sockfd); + } +} + +/** + * @brief Parse HTTP POST request to extract OCSP request body + * @param httpReq HTTP request buffer + * @param httpReqSz size of HTTP request + * @param body pointer to store body location (output) + * @param bodySz pointer to store body size (output) + * @return 0 on success, negative on error + */ +int wolfCLU_HttpServerParseRequest(const byte* httpReq, int httpReqSz, + const byte** body, int* bodySz) +{ + const char* contentLen; + const char* bodyStart; + + *body = NULL; + *bodySz = 0; + + /* Check for POST method */ + if (XSTRNCMP((char*)httpReq, "POST ", 5) != 0) { + return -1; + } + + /* Find Content-Length */ + contentLen = XSTRSTR((char*)httpReq, "Content-Length:"); + if (contentLen == NULL) { + contentLen = XSTRSTR((char*)httpReq, "content-length:"); + } + if (contentLen) { + *bodySz = XATOI(contentLen + 15); + } + + /* Find body (after \r\n\r\n) */ + bodyStart = XSTRSTR((char*)httpReq, "\r\n\r\n"); + if (bodyStart) { + *body = (const byte*)(bodyStart + 4); + /* Use Content-Length if available, otherwise use remaining data */ + if (*bodySz == 0) { + *bodySz = httpReqSz - (int)(*body - httpReq); + } + return 0; + } + + return -1; +} diff --git a/src/tools/clu_pem_der.c b/src/tools/clu_pem_der.c new file mode 100644 index 00000000..93a54bff --- /dev/null +++ b/src/tools/clu_pem_der.c @@ -0,0 +1,134 @@ +/* clu_pem_der.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include + +/** + * @brief Load file and convert to DER format + * @param filename input file path + * @param der pointer to store DER buffer (caller must XFREE) + * @param derSz pointer to store DER size + * @param pemType PEM type (CERT_TYPE, PRIVATEKEY_TYPE, etc.) + * @return 0 on success, negative on error + */ +static int loadFileToDer(const char* filename, byte** der, word32* derSz, int pemType) +{ + WOLFSSL_BIO* bio = NULL; + byte* buf = NULL; + word32 bufSz = 0; + int isPem = 0; + int ret; + DerBuffer* pDer = NULL; + + if (filename == NULL || der == NULL || derSz == NULL) + return -1; + + bio = wolfSSL_BIO_new_file(filename, "rb"); + if (bio == NULL) { + wolfCLU_LogError("Unable to open file %s", filename); + return -1; + } + + bufSz = wolfSSL_BIO_get_len(bio); + if (bufSz <= 0) { + wolfCLU_LogError("Empty or unreadable file %s", filename); + wolfSSL_BIO_free(bio); + return -1; + } + + buf = (byte*)XMALLOC(bufSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (buf == NULL) { + wolfSSL_BIO_free(bio); + return -1; + } + + if (wolfSSL_BIO_read(bio, buf, bufSz) != (int)bufSz) { + wolfCLU_LogError("Failed to read file %s", filename); + XFREE(buf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + wolfSSL_BIO_free(bio); + return -1; + } + wolfSSL_BIO_free(bio); + + /* Check if PEM format */ + isPem = (XSTRSTR((char*)buf, "-----BEGIN") != NULL) ? 1 : 0; + + if (isPem) { + ret = wc_PemToDer(buf, bufSz, pemType, &pDer, NULL, NULL, NULL); + if (ret == 0 && pDer != NULL) { + *der = (byte*)XMALLOC(pDer->length, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (*der != NULL) { + XMEMCPY(*der, pDer->buffer, pDer->length); + *derSz = pDer->length; + } + wc_FreeDer(&pDer); + } + XFREE(buf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + return (*der != NULL) ? 0 : -1; + } + else { + /* Already DER format */ + *der = buf; + *derSz = bufSz; + return 0; + } +} + +/** + * @brief Load certificate file in DER format (handles PEM conversion) + * @param filename certificate file path + * @param der pointer to store DER buffer (caller must XFREE) + * @param derSz pointer to store DER size + * @return 0 on success, negative on error + */ +int wolfCLU_LoadCertDer(const char* filename, byte** der, word32* derSz) +{ + return loadFileToDer(filename, der, derSz, CERT_TYPE); +} + +/** + * @brief Load private key file in DER format (handles PEM conversion) + * @param filename key file path + * @param der pointer to store DER buffer (caller must XFREE) + * @param derSz pointer to store DER size + * @return 0 on success, negative on error + */ +int wolfCLU_LoadKeyDer(const char* filename, byte** der, word32* derSz) +{ + return loadFileToDer(filename, der, derSz, PRIVATEKEY_TYPE); +} + +/** + * @brief Load certificate file and return DER size + * @param filename certificate file path + * @param outDer pointer to store DER buffer (caller must XFREE) + * @return DER size on success, negative on error + */ +int wolfCLU_ReadCertDer(const char* filename, byte** outDer) +{ + word32 derSz = 0; + int ret = wolfCLU_LoadCertDer(filename, outDer, &derSz); + if (ret != 0) { + return ret; + } + return (int)derSz; +} diff --git a/src/tools/clu_scgi.c b/src/tools/clu_scgi.c new file mode 100644 index 00000000..a2fc62e6 --- /dev/null +++ b/src/tools/clu_scgi.c @@ -0,0 +1,277 @@ +/* clu_scgi.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * SCGI (Simple Common Gateway Interface) Protocol Implementation + * + * Following the specification at https://python.ca/scgi/protocol.txt + * + * SCGI is a protocol for communication between web servers and application + * servers. It's simpler than FastCGI but provides similar functionality. + * + * Wire Format: + * ------------ + * Request: :, + * + * Where: + * : ASCII decimal number of bytes in headers section + * : : Literal colon separator + * : Null-separated key-value pairs (key\0value\0...) + * , : Literal comma separator (end of headers) + * : Raw body bytes + * + * Example Request: + * 70:CONTENT_LENGTH\027\0SCGI\01\0REQUEST_METHOD\0POST\0REQUEST_URI\0/deepthought\0,What is the answer to life? + * + * Response Format: + * --------------- + * Standard CGI response with Status and headers: + * Status: 200 OK\r\n + * Content-Type: application/ocsp-response\r\n + * \r\n + * + */ + +#include +#include + +/* Platform-specific includes for sockets */ +#ifdef _WIN32 + #include + #include +#else + #include + #include + #include + #include + #include +#endif + +/* Read exactly n bytes from socket, handling partial reads */ +static int readExactly(SOCKET_T sockfd, byte* buffer, int n) +{ + int totalRead = 0; + + while (totalRead < n) { + int ret = (int)recv(sockfd, (char*)buffer + totalRead, n - totalRead, 0); + if (ret <= 0) { + return -1; + } + totalRead += ret; + } + return totalRead; +} + +/* Parse netstring length (read until ':' and convert to integer) */ +static int parseNetstringLength(SOCKET_T sockfd, int* length) +{ + char lenBuf[16]; + int i = 0; + + *length = 0; + + while (i < (int)sizeof(lenBuf) - 1) { + int ret = (int)recv(sockfd, &lenBuf[i], 1, 0); + if (ret <= 0) { + return -1; + } + if (lenBuf[i] == ':') { + lenBuf[i] = '\0'; + *length = XATOI(lenBuf); + return 0; + } + i++; + } + + return -1; +} + +/* Parse SCGI headers (null-separated key-value pairs) */ +static int parseHeaders(const byte* headers, int headerLen, ScgiRequest* req) +{ + int pos = 0; + + req->contentLength = -1; + req->requestMethod = NULL; + req->requestUri = NULL; + + while (pos < headerLen) { + const char* key = (const char*)(headers + pos); + int keyLen = (int)XSTRLEN(key); + const char* value; + int valueLen; + + if (pos + keyLen >= headerLen) { + break; + } + + pos += keyLen + 1; + value = (const char*)(headers + pos); + valueLen = (int)XSTRLEN(value); + + if (pos + valueLen >= headerLen) { + break; + } + + if (XSTRCMP(key, "CONTENT_LENGTH") == 0) { + req->contentLength = XATOI(value); + } + else if (XSTRCMP(key, "REQUEST_METHOD") == 0) { + req->requestMethod = value; + } + else if (XSTRCMP(key, "REQUEST_URI") == 0) { + req->requestUri = value; + } + else { + WOLFCLU_LOG(WOLFCLU_L2, "Got unsupported SCGI header"); + } + + pos += valueLen + 1; + } + + return 0; +} + +/** + * @brief Read and parse an SCGI request from socket + * @param sockfd socket descriptor + * @param buffer buffer to store the complete request + * @param bufferSz size of buffer + * @param req output structure to store parsed request + * @return 0 on success, negative on error + */ +int wolfCLU_ScgiReadRequest(SOCKET_T sockfd, byte* buffer, int bufferSz, + ScgiRequest* req) +{ + int headerLen; + int pos = 0; + byte comma; + + XMEMSET(req, 0, sizeof(ScgiRequest)); + + if (parseNetstringLength(sockfd, &headerLen) != 0) { + WOLFCLU_LOG(WOLFCLU_E0, "Failed to parse SCGI netstring length"); + return -1; + } + + if (headerLen <= 0 || headerLen >= bufferSz) { + WOLFCLU_LOG(WOLFCLU_E0, "Invalid SCGI header length: %d", headerLen); + return -1; + } + + if (readExactly(sockfd, buffer, headerLen) != headerLen) { + WOLFCLU_LOG(WOLFCLU_E0, "Failed to read SCGI headers"); + return -1; + } + pos = headerLen; + + if (readExactly(sockfd, &comma, 1) != 1 || comma != ',') { + WOLFCLU_LOG(WOLFCLU_E0, "Invalid SCGI netstring terminator"); + return -1; + } + + if (parseHeaders(buffer, headerLen, req) != 0) { + WOLFCLU_LOG(WOLFCLU_E0, "Failed to parse SCGI headers"); + return -1; + } + + if (req->contentLength < 0 || pos + req->contentLength > bufferSz) { + WOLFCLU_LOG(WOLFCLU_E0, "Invalid SCGI content length: %d", + req->contentLength); + return -1; + } + + if (req->contentLength > 0) { + if (readExactly(sockfd, buffer + pos, req->contentLength) != + req->contentLength) { + WOLFCLU_LOG(WOLFCLU_E0, "Failed to read SCGI body"); + return -1; + } + req->body = buffer + pos; + req->bodyLen = req->contentLength; + } + else { + req->body = NULL; + req->bodyLen = 0; + } + + return 0; +} + +/** + * @brief Send SCGI response with status and body + * @param sockfd socket descriptor + * @param statusCode HTTP status code + * @param statusText HTTP status text + * @param contentType MIME type + * @param body response body + * @param bodyLen size of response body + * @return 0 on success, negative on error + */ +int wolfCLU_ScgiSendResponse(SOCKET_T sockfd, int statusCode, + const char* statusText, const char* contentType, + const byte* body, int bodyLen) +{ + char header[512]; + int headerLen; + int sent; + + headerLen = XSNPRINTF(header, sizeof(header), + "Status: %d %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %d\r\n" + "\r\n", + statusCode, statusText, contentType, bodyLen); + + if (headerLen < 0 || headerLen >= (int)sizeof(header)) { + return -1; + } + + sent = (int)send(sockfd, header, headerLen, 0); + if (sent != headerLen) { + return -1; + } + + if (bodyLen > 0 && body != NULL) { + sent = (int)send(sockfd, (const char*)body, bodyLen, 0); + if (sent != bodyLen) { + return -1; + } + } + + return 0; +} + +/** + * @brief Send SCGI error response + * @param sockfd socket descriptor + * @param statusCode HTTP status code + * @param statusText HTTP status text (also used as body) + * @return 0 on success, negative on error + */ +int wolfCLU_ScgiSendError(SOCKET_T sockfd, int statusCode, + const char* statusText) +{ + return wolfCLU_ScgiSendResponse(sockfd, statusCode, statusText, + "text/plain", + (const byte*)statusText, + (int)XSTRLEN(statusText)); +} diff --git a/tests/ocsp-scgi/include.am b/tests/ocsp-scgi/include.am new file mode 100644 index 00000000..16f38f88 --- /dev/null +++ b/tests/ocsp-scgi/include.am @@ -0,0 +1,11 @@ +# vim:ft=automake +# included from Top Level Makefile.am +# All paths should be given relative to the root + +# SCGI tests are run as part of make check +# Tests will be skipped if nginx or openssl are not available +dist_noinst_SCRIPTS += tests/ocsp-scgi/ocsp-scgi-test.sh + +EXTRA_DIST += \ + tests/ocsp-scgi/scgi_params \ + tests/ocsp-scgi/README.md diff --git a/tests/ocsp-scgi/ocsp-scgi-test.sh b/tests/ocsp-scgi/ocsp-scgi-test.sh new file mode 100755 index 00000000..b8a92cad --- /dev/null +++ b/tests/ocsp-scgi/ocsp-scgi-test.sh @@ -0,0 +1,493 @@ +#!/bin/bash + +# OCSP SCGI Integration Tests +# Tests wolfCLU OCSP SCGI mode with nginx proxying +# +# Usage: ocsp-scgi-test.sh [--keep-temp] +# +# Options: +# --keep-temp Don't delete temporary directory on exit (useful for debugging) +# +# Exit codes: +# 0 - All tests passed +# 77 - Tests skipped (missing dependencies) +# 99 - Tests failed + +set -e + +EXIT_SUCCESS=0 +EXIT_FAILURE=99 +EXIT_SKIP=77 + +# Track if tests failed (used in cleanup to print logs) +TESTS_FAILED=0 + +# Parse command line options +KEEP_TEMP=0 +if [ "$1" = "--keep-temp" ]; then + KEEP_TEMP=1 +fi + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +WOLFCLU_BIN="$REPO_ROOT/wolfssl" +CERTS_DIR="$REPO_ROOT/certs" + +if ! $WOLFCLU_BIN ocsp -help &> /dev/null; then + echo "ocsp not supported, skipping test" + exit 77 +fi + +# Create temporary directory for test files +TEMP_DIR=$(mktemp -d -t wolfclu-ocsp-scgi-XXXXXX) +TEST_DIR="$TEMP_DIR" +LOG_DIR="$TEMP_DIR/logs" + +# Cleanup function +cleanup() { + echo "Cleaning up..." + + # Kill wolfCLU SCGI responder + if [ -n "$WOLFCLU_PID" ] && kill -0 "$WOLFCLU_PID" 2>/dev/null; then + echo "Stopping wolfCLU SCGI responder (PID: $WOLFCLU_PID)..." + kill "$WOLFCLU_PID" 2>/dev/null || true + wait "$WOLFCLU_PID" 2>/dev/null || true + fi + + # Stop nginx + if [ -n "$NGINX_PID" ] && kill -0 "$NGINX_PID" 2>/dev/null; then + echo "Stopping nginx (PID: $NGINX_PID)..." + kill "$NGINX_PID" 2>/dev/null || true + wait "$NGINX_PID" 2>/dev/null || true + fi + + # Print all logs if tests failed + if [ "$TESTS_FAILED" = "1" ] && [ -d "$TEMP_DIR" ]; then + echo "" + echo "======================================" + echo "Tests failed - dumping all logs:" + echo "======================================" + + # Find all .log files in temp directory + while IFS= read -r -d '' logfile; do + if [ -s "$logfile" ]; then # Only show non-empty log files + echo "" + echo "--- $logfile ---" + cat "$logfile" + fi + done < <(find "$TEMP_DIR" -type f -name "*.log" -print0 2>/dev/null) + + echo "" + echo "======================================" + fi + + # Clean up temporary directory + if [ "$KEEP_TEMP" = "1" ]; then + echo "" + echo "======================================" + echo "Temporary directory preserved:" + echo "$TEMP_DIR" + echo "======================================" + echo "Contents:" + ls -lh "$TEMP_DIR" + if [ -d "$LOG_DIR" ]; then + echo "" + echo "Logs:" + ls -lh "$LOG_DIR" + fi + else + rm -rf "$TEMP_DIR" + fi +} + +trap cleanup EXIT INT TERM + +# Check prerequisites +echo "======================================" +echo "OCSP SCGI Integration Tests" +echo "======================================" + +# Check for nginx +if ! command -v nginx &> /dev/null; then + echo "nginx not found - skipping OCSP SCGI tests" + echo "Install nginx to run these tests: sudo apt-get install nginx" + exit $EXIT_SKIP +fi + +# Check for openssl +if ! command -v openssl &> /dev/null; then + echo "openssl not found - skipping OCSP SCGI tests" + exit $EXIT_SKIP +fi + +# Check for wolfCLU binary +if [ ! -x "$WOLFCLU_BIN" ]; then + echo "wolfCLU binary not found or not executable: $WOLFCLU_BIN" + echo "Build wolfCLU first: make" + exit $EXIT_SKIP +fi + +# Check for certificates +if [ ! -d "$CERTS_DIR" ]; then + echo "Certificates directory not found: $CERTS_DIR" + exit $EXIT_SKIP +fi + +echo "Prerequisites check passed" +echo "wolfCLU: $WOLFCLU_BIN" +echo "Certificates: $CERTS_DIR" +echo "Temp directory: $TEMP_DIR" +echo "Logs: $LOG_DIR" +if [ "$KEEP_TEMP" = "1" ]; then + echo "Keep temp: YES (will preserve on exit)" +fi +echo "" + +# Create log directory +mkdir -p "$LOG_DIR" + +# Create nginx temporary directories +mkdir -p "$TEMP_DIR/nginx_client_body" +mkdir -p "$TEMP_DIR/nginx_proxy" +mkdir -p "$TEMP_DIR/nginx_fastcgi" +mkdir -p "$TEMP_DIR/nginx_uwsgi" +mkdir -p "$TEMP_DIR/nginx_scgi" + +# Generate nginx configuration with proper temp directory paths +cat > "$TEMP_DIR/nginx.conf" < "$TEST_DIR/index.txt" + ;; + "revoked") + printf "R\t991231235959Z\t200101000000Z\t01\tunknown\t/C=US/ST=Montana/L=Bozeman/O=wolfSSL/OU=Support/CN=www.wolfssl.com/emailAddress=info@wolfssl.com\n" > "$TEST_DIR/index.txt" + ;; + "empty") + touch "$TEST_DIR/index.txt" + ;; + *) + echo "Unknown mode: $mode" + return 1 + ;; + esac + + echo "unique_subject = no" > "$TEST_DIR/index.txt.attr" +} + +# Test helper function +# Usage: run_test [rsigner] [rkey] +run_test() { + local test_name="$1" + local index_setup="$2" + local expected_status="$3" + local rsigner="${4:-$CERTS_DIR/ca-cert.pem}" + local rkey="${5:-$CERTS_DIR/ca-key.pem}" + + echo "" + echo "======================================" + echo "Test: $test_name" + echo "======================================" + + # Setup index file + echo "Setting up index file..." + setup_index "$index_setup" + + # Restart wolfCLU SCGI responder with new index + if [ -n "$WOLFCLU_PID" ] && kill -0 "$WOLFCLU_PID" 2>/dev/null; then + echo "Restarting wolfCLU SCGI responder..." + kill "$WOLFCLU_PID" + wait "$WOLFCLU_PID" 2>/dev/null || true + fi + + # Start wolfCLU SCGI responder + echo "Starting wolfCLU SCGI responder (rsigner: $(basename "$rsigner"))..." + "$WOLFCLU_BIN" ocsp -scgi \ + -port 6961 \ + -index "$TEST_DIR/index.txt" \ + -rsigner "$rsigner" \ + -rkey "$rkey" \ + -CA "$CERTS_DIR/ca-cert.pem" \ + > "$LOG_DIR/wolfclu-scgi.log" 2>&1 & + WOLFCLU_PID=$! + + # Wait for responder to start + sleep 0.5 + + if ! kill -0 "$WOLFCLU_PID" 2>/dev/null; then + echo "ERROR: wolfCLU SCGI responder failed to start" + cat "$LOG_DIR/wolfclu-scgi.log" + return $EXIT_FAILURE + fi + + echo "wolfCLU SCGI responder started (PID: $WOLFCLU_PID)" + + # Make OCSP request via nginx + echo "Making OCSP request..." + + # Send OCSP request via HTTP to nginx (which forwards via SCGI to wolfCLU) + # openssl ocsp handles the entire HTTP transaction + OCSP_OUTPUT=$(openssl ocsp \ + -issuer "$CERTS_DIR/ca-cert.pem" \ + -cert "$CERTS_DIR/server-cert.pem" \ + -CAfile "$CERTS_DIR/ca-cert.pem" \ + -url http://localhost:8080/ocsp 2>&1) + + OCSP_EXIT_CODE=$? + + echo "$OCSP_OUTPUT" + + # Check if the request was successful + if [ $OCSP_EXIT_CODE -eq 0 ]; then + if echo "$OCSP_OUTPUT" | grep -q "$expected_status"; then + echo "✓ Test PASSED: Found expected status '$expected_status'" + return $EXIT_SUCCESS + else + echo "✗ Test FAILED: Expected '$expected_status' but got different status" + return $EXIT_FAILURE + fi + else + echo "✗ Test FAILED: OCSP request failed with exit code $OCSP_EXIT_CODE" + return $EXIT_FAILURE + fi +} + +# Start nginx +echo "Starting nginx..." + +nginx -c "$TEMP_DIR/nginx.conf" > "$LOG_DIR/nginx-startup.log" 2>&1 & +NGINX_PID=$! +sleep 0.5 + +if ! kill -0 "$NGINX_PID" 2>/dev/null; then + echo "ERROR: nginx failed to start" + cat "$LOG_DIR/nginx-startup.log" + exit $EXIT_FAILURE +fi + +echo "nginx started (PID: $NGINX_PID)" + +# Run tests +FAILED_TESTS=0 +PASSED_TESTS=0 + +# Test 1: Valid certificate +if run_test "Valid certificate (good status)" "valid" "good"; then + PASSED_TESTS=$((PASSED_TESTS + 1)) +else + FAILED_TESTS=$((FAILED_TESTS + 1)) +fi + +# Test 2: Revoked certificate +if run_test "Revoked certificate" "revoked" "revoked"; then + PASSED_TESTS=$((PASSED_TESTS + 1)) +else + FAILED_TESTS=$((FAILED_TESTS + 1)) +fi + +# Test 3: Valid certificate after revoked (stateless verification) +if run_test "Valid certificate again (stateless)" "valid" "good"; then + PASSED_TESTS=$((PASSED_TESTS + 1)) +else + FAILED_TESTS=$((FAILED_TESTS + 1)) +fi + +# Test 4: Multiple requests to same responder +echo "" +echo "======================================" +echo "Test: Multiple sequential requests" +echo "======================================" + +MULTI_REQUEST_SUCCESS=1 +for i in 1 2 3; do + echo "Request $i of 3..." + + # Send OCSP request via openssl (handles HTTP internally) + if openssl ocsp \ + -issuer "$CERTS_DIR/ca-cert.pem" \ + -cert "$CERTS_DIR/server-cert.pem" \ + -CAfile "$CERTS_DIR/ca-cert.pem" \ + -url http://localhost:8080/ocsp > /dev/null 2>&1; then + echo "✓ Request $i successful" + else + echo "✗ Request $i failed" + MULTI_REQUEST_SUCCESS=0 + break + fi +done + +if [ "$MULTI_REQUEST_SUCCESS" = "1" ]; then + echo "✓ Test PASSED: All 3 requests successful" + PASSED_TESTS=$((PASSED_TESTS + 1)) +else + echo "✗ Test FAILED: Not all requests successful" + FAILED_TESTS=$((FAILED_TESTS + 1)) +fi + +# --- Tests with authorized/delegated OCSP responder --- + +# Test 5: Valid certificate with authorized responder +if run_test "Valid certificate with authorized responder" "valid" "good" \ + "$CERTS_DIR/ocsp-responder-cert.pem" "$CERTS_DIR/ocsp-responder-key.pem"; then + PASSED_TESTS=$((PASSED_TESTS + 1)) +else + FAILED_TESTS=$((FAILED_TESTS + 1)) +fi + +# Test 6: Revoked certificate with authorized responder +if run_test "Revoked certificate with authorized responder" "revoked" "revoked" \ + "$CERTS_DIR/ocsp-responder-cert.pem" "$CERTS_DIR/ocsp-responder-key.pem"; then + PASSED_TESTS=$((PASSED_TESTS + 1)) +else + FAILED_TESTS=$((FAILED_TESTS + 1)) +fi + +# Test 7: Valid certificate after revoked with authorized responder (stateless) +if run_test "Valid certificate again with authorized responder (stateless)" "valid" "good" \ + "$CERTS_DIR/ocsp-responder-cert.pem" "$CERTS_DIR/ocsp-responder-key.pem"; then + PASSED_TESTS=$((PASSED_TESTS + 1)) +else + FAILED_TESTS=$((FAILED_TESTS + 1)) +fi + +# Test 8: Multiple requests with authorized responder +echo "" +echo "======================================" +echo "Test: Multiple sequential requests with authorized responder" +echo "======================================" + +setup_index "valid" + +# Restart with authorized responder +if [ -n "$WOLFCLU_PID" ] && kill -0 "$WOLFCLU_PID" 2>/dev/null; then + kill "$WOLFCLU_PID" + wait "$WOLFCLU_PID" 2>/dev/null || true +fi + +"$WOLFCLU_BIN" ocsp -scgi \ + -port 6961 \ + -index "$TEST_DIR/index.txt" \ + -rsigner "$CERTS_DIR/ocsp-responder-cert.pem" \ + -rkey "$CERTS_DIR/ocsp-responder-key.pem" \ + -CA "$CERTS_DIR/ca-cert.pem" \ + > "$LOG_DIR/wolfclu-scgi.log" 2>&1 & +WOLFCLU_PID=$! +sleep 0.5 + +if ! kill -0 "$WOLFCLU_PID" 2>/dev/null; then + echo "ERROR: wolfCLU SCGI responder failed to start" + cat "$LOG_DIR/wolfclu-scgi.log" + FAILED_TESTS=$((FAILED_TESTS + 1)) +else + MULTI_REQUEST_SUCCESS=1 + for i in 1 2 3; do + echo "Request $i of 3..." + if openssl ocsp \ + -issuer "$CERTS_DIR/ca-cert.pem" \ + -cert "$CERTS_DIR/server-cert.pem" \ + -CAfile "$CERTS_DIR/ca-cert.pem" \ + -url http://localhost:8080/ocsp > /dev/null 2>&1; then + echo "✓ Request $i successful" + else + echo "✗ Request $i failed" + MULTI_REQUEST_SUCCESS=0 + break + fi + done + + if [ "$MULTI_REQUEST_SUCCESS" = "1" ]; then + echo "✓ Test PASSED: All 3 requests successful" + PASSED_TESTS=$((PASSED_TESTS + 1)) + else + echo "✗ Test FAILED: Not all requests successful" + FAILED_TESTS=$((FAILED_TESTS + 1)) + fi +fi + +# Stop the last responder and verify graceful shutdown +echo "" +echo "======================================" +echo "Verifying graceful shutdown" +echo "======================================" + +if [ -n "$WOLFCLU_PID" ] && kill -0 "$WOLFCLU_PID" 2>/dev/null; then + echo "Stopping wolfCLU SCGI responder..." + kill "$WOLFCLU_PID" 2>/dev/null || true + wait "$WOLFCLU_PID" 2>/dev/null || true + WOLFCLU_PID="" +fi + +# Check for graceful exit message in logs +if [ -f "$LOG_DIR/wolfclu-scgi.log" ]; then + if grep -q "wolfssl exiting gracefully" "$LOG_DIR/wolfclu-scgi.log"; then + echo "✓ Found graceful exit message in wolfclu-scgi.log" + else + echo "✗ ERROR: No 'wolfssl exiting gracefully' message found in wolfclu-scgi.log" + echo " The responder did not shut down gracefully" + FAILED_TESTS=$((FAILED_TESTS + 1)) + fi +else + echo "✗ ERROR: wolfclu-scgi.log not found" + FAILED_TESTS=$((FAILED_TESTS + 1)) +fi + +# Summary +echo "" +echo "======================================" +echo "Test Summary" +echo "======================================" +echo "Passed: $PASSED_TESTS" +echo "Failed: $FAILED_TESTS" +echo "======================================" + +if [ "$FAILED_TESTS" -gt 0 ]; then + TESTS_FAILED=1 + echo "Some tests failed. Check logs above." + exit $EXIT_FAILURE +else + echo "All tests passed!" + exit $EXIT_SUCCESS +fi diff --git a/tests/ocsp-scgi/scgi_params b/tests/ocsp-scgi/scgi_params new file mode 100644 index 00000000..a37ab500 --- /dev/null +++ b/tests/ocsp-scgi/scgi_params @@ -0,0 +1,17 @@ +scgi_param REQUEST_METHOD $request_method; +scgi_param REQUEST_URI $request_uri; +scgi_param QUERY_STRING $query_string; +scgi_param CONTENT_TYPE $content_type; +scgi_param CONTENT_LENGTH $content_length; + +scgi_param SCRIPT_NAME $fastcgi_script_name; +scgi_param REQUEST_URI $request_uri; +scgi_param DOCUMENT_URI $document_uri; +scgi_param DOCUMENT_ROOT $document_root; +scgi_param SERVER_PROTOCOL $server_protocol; +scgi_param HTTPS $https if_not_empty; + +scgi_param REMOTE_ADDR $remote_addr; +scgi_param REMOTE_PORT $remote_port; +scgi_param SERVER_PORT $server_port; +scgi_param SERVER_NAME $server_name; diff --git a/tests/ocsp/include.am b/tests/ocsp/include.am new file mode 100644 index 00000000..f6da6716 --- /dev/null +++ b/tests/ocsp/include.am @@ -0,0 +1,6 @@ +# vim:ft=automake +# included from top level Makefile.am +# All paths should be given relative to root directory + +# Only register the consolidated test - interop test is a helper +dist_noinst_SCRIPTS+=tests/ocsp/ocsp-test.sh diff --git a/tests/ocsp/ocsp-interop-test.sh b/tests/ocsp/ocsp-interop-test.sh new file mode 100755 index 00000000..49c8a75a --- /dev/null +++ b/tests/ocsp/ocsp-interop-test.sh @@ -0,0 +1,520 @@ +#!/bin/bash + +# Generic OCSP interoperability test +# Uses environment variables to determine which binaries to use: +# OCSP_CLIENT - binary to use for OCSP client (wolfclu or openssl) +# OCSP_RESPONDER - binary to use for OCSP responder (wolfclu or openssl) +# KEEP_TEST_DIR - if set to 1, preserve test directory for debugging +# +# Test coverage: +# - Positive tests: Valid certificate checks with various options +# - Negative tests: Revoked certificates, missing parameters, invalid files +# - Return code compatibility: Verifies wolfssl ocsp is compatible with openssl ocsp +# +# Exit codes: +# 0 - All tests passed +# 77 - Test skipped (filesystem disabled, OCSP not supported, etc.) +# 99 - Test failed + +if [ ! -d ./certs/ ]; then + echo "certs directory not found, skipping test" + exit 77 +fi + +# Skip test if filesystem disabled +FILESYSTEM=`cat config.log | grep "disable\-filesystem"` +if [ "$FILESYSTEM" != "" ]; then + echo "Filesystem disabled, skipping test" + exit 77 +fi + +# Determine client and responder binaries +if [ -z "$OCSP_CLIENT" ]; then + echo "Client not specified" + exit 99 +fi + +if [ -z "$OCSP_RESPONDER" ]; then + echo "Responder not specified" + exit 99 +fi + +echo "Testing OCSP interop: $OCSP_CLIENT (client) vs $OCSP_RESPONDER (responder)" + +if ! $OCSP_CLIENT ocsp -help &> /dev/null; then + echo "ocsp not supported on client side, skipping test" + exit 77 +fi +if ! $OCSP_RESPONDER ocsp -help &> /dev/null; then + echo "ocsp not supported on responder side, skipping test" + exit 77 +fi + +# Create a temporary directory for test files +TEST_DIR=$(mktemp -d) +if [ $? != 0 ]; then + echo "Failed to create temp directory" + exit 99 +fi + +cleanup() { + EXIT_CODE=$? + + # Print logs on error + if [ $EXIT_CODE != 0 ] && [ $EXIT_CODE != 77 ]; then + echo "====================================" + echo "Test failed with exit code: $EXIT_CODE" + echo "====================================" + + for logfile in "$TEST_DIR"/*.log; do + if [ -f "$logfile" ]; then + echo "$(basename "$logfile"):" + cat "$logfile" + echo "------------------------------------" + fi + done + fi + + # Kill the OCSP responder if still running + if [ ! -z "$RESPONDER_PID" ]; then + kill $RESPONDER_PID 2>/dev/null + wait $RESPONDER_PID 2>/dev/null + fi + + # Remove test directory unless KEEP_TEST_DIR is set + if [ "$KEEP_TEST_DIR" = "1" ]; then + echo "Test directory preserved: $TEST_DIR" + else + rm -rf "$TEST_DIR" + fi +} + +trap cleanup EXIT + +# Create an OCSP index file for the test +# Format: statusexpirationrevocationserialfilenameDN +# V = valid, R = revoked, E = expired +# Use printf to ensure proper tab separators +printf "V\t991231235959Z\t\t01\tunknown\t/C=US/ST=Montana/L=Bozeman/O=wolfSSL/OU=Support/CN=www.wolfssl.com/emailAddress=info@wolfssl.com\n" > "$TEST_DIR/index.txt" + +OCSP_PORT=6960 + +# Start OCSP responder in background +$OCSP_RESPONDER ocsp -port $OCSP_PORT \ + -index "$TEST_DIR/index.txt" \ + -CA certs/ca-cert.pem \ + -rsigner certs/ca-cert.pem \ + -rkey certs/ca-key.pem \ + -nrequest 10 \ + > "$TEST_DIR/ocsp-responder.log" 2>&1 & +RESPONDER_PID=$! + +# Wait for responder to start +sleep 0.5 + +# Check if responder is still running +if ! kill -0 $RESPONDER_PID 2>/dev/null; then + echo "OCSP responder failed to start" + exit 99 +fi + +echo "OCSP responder started on port $OCSP_PORT (PID: $RESPONDER_PID)" + +# Run client tests +# Test 1: Basic OCSP check with CA file and explicit URL +echo "Test 1: OCSP check with -CAfile and -url" + +$OCSP_CLIENT ocsp \ + -issuer certs/ca-cert.pem \ + -cert certs/server-cert.pem \ + -CAfile certs/ca-cert.pem \ + -url http://127.0.0.1:$OCSP_PORT \ + > "$TEST_DIR/test1.log" 2>&1 + +RESULT=$? +if [ $RESULT != 0 ]; then + echo "Test 1 failed: $OCSP_CLIENT OCSP check returned $RESULT" + exit 99 +fi + +# Verify the output contains success indicator +grep -q "good" "$TEST_DIR/test1.log" +if [ $? != 0 ]; then + echo "Test 1 failed: expected success indicator in output" + exit 99 +fi + +echo "Test 1 passed" + +# Test 2: OCSP check with -no_nonce +echo "Test 2: OCSP check with -no_nonce" + +$OCSP_CLIENT ocsp \ + -issuer certs/ca-cert.pem \ + -cert certs/server-cert.pem \ + -CAfile certs/ca-cert.pem \ + -url http://127.0.0.1:$OCSP_PORT \ + -no_nonce \ + > "$TEST_DIR/test2.log" 2>&1 + +RESULT=$? +if [ $RESULT != 0 ]; then + echo "Test 2 failed: $OCSP_CLIENT OCSP check with -no_nonce returned $RESULT" + exit 99 +fi + +grep -q "good" "$TEST_DIR/test2.log" +if [ $? != 0 ]; then + echo "Test 2 failed: expected success indicator in output" + exit 99 +fi + +echo "Test 2 passed" + +# Test 3: OCSP check for revoked certificate +echo "Test 3: OCSP check for revoked certificate (should show revoked status)" + +# Note: OpenSSL OCSP returns exit code 0 even for revoked certificates, because +# the OCSP transaction itself succeeded. The revocation status is in the output. +# wolfssl OCSP responder currently has a limitation generating revoked responses. + + +# Update index.txt to include the revoked certificate (serial 02) +printf "V\t991231235959Z\t\t01\tunknown\t/C=US/ST=Montana/L=Bozeman/O=wolfSSL/OU=Support/CN=www.wolfssl.com/emailAddress=info@wolfssl.com\n" > "$TEST_DIR/index.txt" +printf "R\t991231235959Z\t240101000000Z\t02\tunknown\t/C=US/ST=Montana/L=Bozeman/O=wolfSSL_revoked/OU=Support_revoked/CN=www.wolfssl.com/emailAddress=info@wolfssl.com\n" >> "$TEST_DIR/index.txt" + +# Restart responder with new index +if [ ! -z "$RESPONDER_PID" ]; then + kill $RESPONDER_PID 2>/dev/null + wait $RESPONDER_PID 2>/dev/null +fi + +$OCSP_RESPONDER ocsp -port $OCSP_PORT \ + -index "$TEST_DIR/index.txt" \ + -CA certs/ca-cert.pem \ + -rsigner certs/ca-cert.pem \ + -rkey certs/ca-key.pem \ + -nrequest 10 \ + > "$TEST_DIR/ocsp-responder2.log" 2>&1 & +RESPONDER_PID=$! + +sleep 0.5 + +if ! kill -0 $RESPONDER_PID 2>/dev/null; then + echo "Test 3 failed: OCSP responder failed to restart" + exit 99 +fi + +# Check the revoked certificate +$OCSP_CLIENT ocsp \ + -issuer certs/ca-cert.pem \ + -cert certs/server-revoked-cert.pem \ + -CAfile certs/ca-cert.pem \ + -url http://127.0.0.1:$OCSP_PORT \ + > "$TEST_DIR/test3.log" 2>&1 + +RESULT=$? + +# OpenSSL returns 0 (success) even for revoked certs - the status is in output +# Check the output for revoked status indicator +if grep -qi "revoked" "$TEST_DIR/test3.log"; then + # Found revoked status - this is correct + echo "Test 3 passed" +else + # Didn't find any revoked indicator + echo "Test 3 failed: expected revoked status indicator in output" + cat "$TEST_DIR/test3.log" + exit 99 +fi + +# Test 4: Missing required parameter (-cert without -issuer) +echo "Test 4: Missing required parameter (no issuer)" + +$OCSP_CLIENT ocsp \ + -cert certs/server-cert.pem \ + -CAfile certs/ca-cert.pem \ + -url http://127.0.0.1:$OCSP_PORT \ + > "$TEST_DIR/test4.log" 2>&1 + +RESULT=$? +if [ $RESULT = 0 ]; then + echo "Test 4 failed: $OCSP_CLIENT should have failed without -issuer" + exit 99 +fi + +# Check for error message about missing issuer +grep -qi "issuer" "$TEST_DIR/test4.log" +if [ $? != 0 ]; then + echo "Test 4 failed: expected error about missing issuer" + exit 99 +fi + +echo "Test 4 passed" + +# Test 5: Missing required parameter (-issuer without -cert) +echo "Test 5: Missing required parameter (no cert)" + +$OCSP_CLIENT ocsp \ + -issuer certs/ca-cert.pem \ + -CAfile certs/ca-cert.pem \ + -url http://127.0.0.1:$OCSP_PORT \ + > "$TEST_DIR/test5.log" 2>&1 + +RESULT=$? +if [ $RESULT = 0 ]; then + echo "Test 5 failed: $OCSP_CLIENT should have failed without -cert" + exit 99 +fi + +# Check for error message about missing cert or help output +# OpenSSL shows help usage, wolfssl shows an error +grep -qi "cert\|help\|usage" "$TEST_DIR/test5.log" +if [ $? != 0 ]; then + echo "Test 5 failed: expected error about missing cert or help output" + exit 99 +fi + +echo "Test 5 passed" + +# Test 6: Invalid certificate file +echo "Test 6: Invalid certificate file" + +$OCSP_CLIENT ocsp \ + -issuer certs/ca-cert.pem \ + -cert /nonexistent/file.pem \ + -CAfile certs/ca-cert.pem \ + -url http://127.0.0.1:$OCSP_PORT \ + > "$TEST_DIR/test6.log" 2>&1 + +RESULT=$? +if [ $RESULT = 0 ]; then + echo "Test 6 failed: $OCSP_CLIENT should have failed with invalid cert file" + exit 99 +fi + +# Check for error message +grep -qi "fail\|error\|not found\|unable" "$TEST_DIR/test6.log" +if [ $? != 0 ]; then + echo "Test 6 failed: expected error message about invalid file" + exit 99 +fi + +echo "Test 6 passed" + +# Test 7: Invalid issuer certificate file +echo "Test 7: Invalid issuer certificate file" + +$OCSP_CLIENT ocsp \ + -issuer /nonexistent/issuer.pem \ + -cert certs/server-cert.pem \ + -CAfile certs/ca-cert.pem \ + -url http://127.0.0.1:$OCSP_PORT \ + > "$TEST_DIR/test7.log" 2>&1 + +RESULT=$? +if [ $RESULT = 0 ]; then + echo "Test 7 failed: $OCSP_CLIENT should have failed with invalid issuer file" + exit 99 +fi + +# Check for error message +grep -qi "fail\|error\|unable\|issuer" "$TEST_DIR/test7.log" +if [ $? != 0 ]; then + echo "Test 7 failed: expected error message about invalid issuer file" + exit 99 +fi + +echo "Test 7 passed" + +# --- Tests with delegated OCSP responder (ocsp-responder-cert.pem as -rsigner) --- + +# Kill current responder and restart with delegated responder cert +if [ ! -z "$RESPONDER_PID" ]; then + kill $RESPONDER_PID 2>/dev/null + wait $RESPONDER_PID 2>/dev/null + RESPONDER_PID="" +fi + +# Reset index to valid-only for delegated responder tests +printf "V\t991231235959Z\t\t01\tunknown\t/C=US/ST=Montana/L=Bozeman/O=wolfSSL/OU=Support/CN=www.wolfssl.com/emailAddress=info@wolfssl.com\n" > "$TEST_DIR/index.txt" + +$OCSP_RESPONDER ocsp -port $OCSP_PORT \ + -index "$TEST_DIR/index.txt" \ + -CA certs/ca-cert.pem \ + -rsigner certs/ocsp-responder-cert.pem \ + -rkey certs/ocsp-responder-key.pem \ + -nrequest 10 \ + > "$TEST_DIR/ocsp-responder-deleg.log" 2>&1 & +RESPONDER_PID=$! + +sleep 0.5 + +if ! kill -0 $RESPONDER_PID 2>/dev/null; then + echo "Delegated OCSP responder failed to start" + exit 99 +fi + +echo "Delegated OCSP responder started on port $OCSP_PORT (PID: $RESPONDER_PID)" + +# Test 8: Basic OCSP check with delegated responder +echo "Test 8: OCSP check with delegated responder (-rsigner ocsp-responder-cert.pem)" + +$OCSP_CLIENT ocsp \ + -issuer certs/ca-cert.pem \ + -cert certs/server-cert.pem \ + -CAfile certs/ca-cert.pem \ + -url http://127.0.0.1:$OCSP_PORT \ + > "$TEST_DIR/test8.log" 2>&1 + +RESULT=$? +if [ $RESULT != 0 ]; then + echo "Test 8 failed: $OCSP_CLIENT OCSP check with delegated responder returned $RESULT" + exit 99 +fi + +grep -q "good" "$TEST_DIR/test8.log" +if [ $? != 0 ]; then + echo "Test 8 failed: expected success indicator in output" + exit 99 +fi + +echo "Test 8 passed" + +# Test 9: OCSP check with delegated responder and -no_nonce +echo "Test 9: OCSP check with delegated responder and -no_nonce" + +$OCSP_CLIENT ocsp \ + -issuer certs/ca-cert.pem \ + -cert certs/server-cert.pem \ + -CAfile certs/ca-cert.pem \ + -url http://127.0.0.1:$OCSP_PORT \ + -no_nonce \ + > "$TEST_DIR/test9.log" 2>&1 + +RESULT=$? +if [ $RESULT != 0 ]; then + echo "Test 9 failed: $OCSP_CLIENT OCSP check with delegated responder and -no_nonce returned $RESULT" + exit 99 +fi + +grep -q "good" "$TEST_DIR/test9.log" +if [ $? != 0 ]; then + echo "Test 9 failed: expected success indicator in output" + exit 99 +fi + +echo "Test 9 passed" + +# Test 10: Revoked cert check with delegated responder +echo "Test 10: OCSP revoked cert check with delegated responder" + +# Update index to include revoked certificate +printf "V\t991231235959Z\t\t01\tunknown\t/C=US/ST=Montana/L=Bozeman/O=wolfSSL/OU=Support/CN=www.wolfssl.com/emailAddress=info@wolfssl.com\n" > "$TEST_DIR/index.txt" +printf "R\t991231235959Z\t240101000000Z\t02\tunknown\t/C=US/ST=Montana/L=Bozeman/O=wolfSSL_revoked/OU=Support_revoked/CN=www.wolfssl.com/emailAddress=info@wolfssl.com\n" >> "$TEST_DIR/index.txt" + +# Restart delegated responder with updated index +if [ ! -z "$RESPONDER_PID" ]; then + kill $RESPONDER_PID 2>/dev/null + wait $RESPONDER_PID 2>/dev/null +fi + +$OCSP_RESPONDER ocsp -port $OCSP_PORT \ + -index "$TEST_DIR/index.txt" \ + -CA certs/ca-cert.pem \ + -rsigner certs/ocsp-responder-cert.pem \ + -rkey certs/ocsp-responder-key.pem \ + -nrequest 10 \ + > "$TEST_DIR/ocsp-responder-deleg2.log" 2>&1 & +RESPONDER_PID=$! + +sleep 0.5 + +if ! kill -0 $RESPONDER_PID 2>/dev/null; then + echo "Test 10 failed: delegated OCSP responder failed to restart" + exit 99 +fi + +$OCSP_CLIENT ocsp \ + -issuer certs/ca-cert.pem \ + -cert certs/server-revoked-cert.pem \ + -CAfile certs/ca-cert.pem \ + -url http://127.0.0.1:$OCSP_PORT \ + > "$TEST_DIR/test10.log" 2>&1 + +if grep -qi "revoked" "$TEST_DIR/test10.log"; then + echo "Test 10 passed" +else + echo "Test 10 failed: expected revoked status indicator in output" + cat "$TEST_DIR/test10.log" + exit 99 +fi + +# Test 11: Unreachable OCSP responder +echo "Test 11: Unreachable OCSP responder" + +# Kill the responder temporarily +if [ ! -z "$RESPONDER_PID" ]; then + kill $RESPONDER_PID 2>/dev/null + wait $RESPONDER_PID 2>/dev/null + RESPONDER_PID="" +fi + +$OCSP_CLIENT ocsp \ + -issuer certs/ca-cert.pem \ + -cert certs/server-cert.pem \ + -CAfile certs/ca-cert.pem \ + -url http://127.0.0.1:$OCSP_PORT \ + > "$TEST_DIR/test11.log" 2>&1 + +RESULT=$? +if [ $RESULT = 0 ]; then + echo "Test 11 failed: $OCSP_CLIENT should have failed with unreachable responder" + exit 99 +fi + +# Check for connection/network error +grep -qi "fail\|error\|connect\|timeout\|refused" "$TEST_DIR/test11.log" +if [ $? != 0 ]; then + echo "Test 11 failed: expected connection error message" + exit 99 +fi + +echo "Test 11 passed" + +# Verify graceful exit messages in responder logs (for wolfCLU responders only) +if [ "$OCSP_RESPONDER" = "./wolfssl" ]; then + echo "" + echo "Verifying graceful shutdown messages..." + + # Check each responder log file for the graceful exit message + MISSING_LOGS="" + LOG_COUNT=0 + + for logfile in "$TEST_DIR"/ocsp-responder*.log; do + if [ -f "$logfile" ]; then + LOG_COUNT=$((LOG_COUNT + 1)) + if grep -q "wolfssl exiting gracefully" "$logfile"; then + echo "✓ Found graceful exit message in $(basename "$logfile")" + else + echo "✗ Missing graceful exit message in $(basename "$logfile")" + MISSING_LOGS="$MISSING_LOGS $(basename "$logfile")" + fi + fi + done + + if [ $LOG_COUNT -eq 0 ]; then + echo "ERROR: No responder log files found" + exit 99 + fi + + if [ -n "$MISSING_LOGS" ]; then + echo "" + echo "ERROR: The following responder logs are missing graceful exit messages:" + echo "$MISSING_LOGS" + echo "All responders must shut down gracefully" + exit 99 + fi +fi + +echo "All OCSP interop tests passed" +exit 0 diff --git a/tests/ocsp/ocsp-test.sh b/tests/ocsp/ocsp-test.sh new file mode 100755 index 00000000..d34e77c4 --- /dev/null +++ b/tests/ocsp/ocsp-test.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# Consolidated OCSP interoperability test +# Runs all test combinations in series to avoid port conflicts + +# Exit 77 to indicate test was skipped +# Exit 99 to indicate test failed +# Exit 0 to indicate test passed + +echo "======================================" +echo "OCSP Interoperability Test Suite" +echo "======================================" + +if ! ./wolfssl ocsp -help &> /dev/null; then + echo "ocsp not supported, skipping test" + exit 77 +fi + +# Track overall results +TOTAL=0 +PASSED=0 +SKIPPED=0 +FAILED=0 + +run_test() { + local client=$1 + local responder=$2 + local test_name="$client-$responder" + + echo "" + echo "Running: $test_name" + echo "--------------------------------------" + + TOTAL=$((TOTAL + 1)) + + export OCSP_CLIENT="$client" + export OCSP_RESPONDER="$responder" + + "$(dirname "$0")/ocsp-interop-test.sh" + local result=$? + + if [ $result -eq 0 ]; then + echo "✓ $test_name: PASSED" + PASSED=$((PASSED + 1)) + elif [ $result -eq 77 ]; then + echo "⊘ $test_name: SKIPPED" + SKIPPED=$((SKIPPED + 1)) + else + echo "✗ $test_name: FAILED (exit $result)" + FAILED=$((FAILED + 1)) + fi +} + +# Run all test combinations in series +run_test "./wolfssl" "openssl" +run_test "openssl" "./wolfssl" +run_test "./wolfssl" "./wolfssl" +# Running this config too to make sure the script works +run_test "openssl" "openssl" + +# Print summary +echo "" +echo "======================================" +echo "Test Summary" +echo "======================================" +echo "Total: $TOTAL" +echo "Passed: $PASSED" +echo "Skipped: $SKIPPED" +echo "Failed: $FAILED" +echo "======================================" + +# Return appropriate exit code +if [ $FAILED -gt 0 ]; then + exit 99 +elif [ $PASSED -eq 0 ]; then + # All tests were skipped + exit 77 +else + exit 0 +fi + + diff --git a/tests/x509/x509-req-test.sh b/tests/x509/x509-req-test.sh index 4ba35832..d0d8742f 100755 --- a/tests/x509/x509-req-test.sh +++ b/tests/x509/x509-req-test.sh @@ -330,6 +330,7 @@ if [ $? -eq 0 ]; then fi fi rm -f tmp.cert +rm -f tmp.csr echo "Done" exit 0 diff --git a/wolfCLU.vcxproj b/wolfCLU.vcxproj index cd213b9b..a6017fa3 100644 --- a/wolfCLU.vcxproj +++ b/wolfCLU.vcxproj @@ -159,6 +159,7 @@ + @@ -174,7 +175,10 @@ + + + diff --git a/wolfclu/client.h b/wolfclu/client.h index 05034d83..64b16fec 100644 --- a/wolfclu/client.h +++ b/wolfclu/client.h @@ -24,7 +24,6 @@ #define WOLFSSL_CLIENT_H #define NO_MAIN_DRIVER -#define WOLFSSL_THREAD #ifndef WOLFCLU_NO_FILESYSTEM THREAD_RETURN WOLFSSL_THREAD client_test(void* args); diff --git a/wolfclu/clu_header_main.h b/wolfclu/clu_header_main.h index 863c43a3..e95e2c64 100644 --- a/wolfclu/clu_header_main.h +++ b/wolfclu/clu_header_main.h @@ -595,6 +595,205 @@ int wolfCLU_Base64Setup(int argc, char** argv); int wolfCLU_DhParamSetup(int argc, char** argv); +/** + * @brief function to handle OCSP commands (client and responder) + */ +int wolfCLU_OcspSetup(int argc, char** argv); + +/** + * @brief Get a simple HTTP GET request string + * @return pointer to static HTTP GET request string + */ +const char* wolfCLU_GetDefaultHttpGet(void); + +/** + * @brief Get the length of the default HTTP GET request (without null terminator) + * @return length of HTTP GET request + */ +int wolfCLU_GetDefaultHttpGetLength(void); + +/** + * @brief Get a simple HTTP 200 OK response string with HTML content + * @return pointer to static HTTP response string + */ +const char* wolfCLU_GetDefaultHttpResponse(void); + +/** + * @brief Get the length of the default HTTP response (without null terminator) + * @return length of HTTP response + */ +int wolfCLU_GetDefaultHttpResponseLength(void); + +/** + * @brief Build a custom HTTP GET request + * @param path the path to request (e.g., "/index.html") + * @param host optional host header value (can be NULL) + * @param buffer buffer to write the request to + * @param bufferSz size of the buffer + * @return number of bytes written to buffer, or negative on error + */ +int wolfCLU_BuildHttpGet(const char* path, const char* host, char* buffer, + int bufferSz); + +/** + * @brief Build a simple HTTP response + * @param statusCode HTTP status code (e.g., 200, 404) + * @param statusText HTTP status text (e.g., "OK", "Not Found") + * @param contentType MIME type (e.g., "text/html") + * @param body response body content + * @param buffer buffer to write the response to + * @param bufferSz size of the buffer + * @return number of bytes written to buffer, or negative on error + */ +int wolfCLU_BuildHttpResponse(int statusCode, const char* statusText, + const char* contentType, const char* body, + char* buffer, int bufferSz); + +/* Platform-specific socket type */ +#ifdef _WIN32 + #ifndef SOCKET_T + #define SOCKET_T SOCKET + #endif +#else + #ifndef SOCKET_T + #define SOCKET_T int + #endif + #ifndef INVALID_SOCKET + #define INVALID_SOCKET (-1) + #endif +#endif + +/** + * @brief Create and bind a server socket using tcp_listen + * @param port pointer to port number (will be updated with actual port) + * @return socket descriptor on success, INVALID_SOCKET on error + */ +SOCKET_T wolfCLU_HttpServerListen(word16* port); + +/** + * @brief Accept a client connection + * @param serverfd server socket descriptor + * @return client socket descriptor on success, INVALID_SOCKET on error + */ +SOCKET_T wolfCLU_ServerAccept(SOCKET_T serverfd); + +/** + * @brief Receive a complete HTTP request + * @param clientfd client socket descriptor + * @param buffer buffer to store request + * @param bufferSz size of buffer + * @return number of bytes received, or negative on error + */ +int wolfCLU_HttpServerRecv(SOCKET_T clientfd, byte* buffer, int bufferSz); + +/** + * @brief Send an HTTP response with OCSP content + * @param clientfd client socket descriptor + * @param body response body (OCSP response) + * @param bodySz size of response body + * @return 0 on success, negative on error + */ +int wolfCLU_HttpServerSendOcspResponse(SOCKET_T clientfd, const byte* body, + int bodySz); + +/** + * @brief Send an HTTP error response + * @param clientfd client socket descriptor + * @param statusCode HTTP status code + * @param statusMsg HTTP status message + * @return 0 on success, negative on error + */ +int wolfCLU_HttpServerSendError(SOCKET_T clientfd, int statusCode, + const char* statusMsg); + +/** + * @brief Close a socket + * @param sockfd socket descriptor to close + */ +void wolfCLU_ServerClose(SOCKET_T sockfd); + +/** + * @brief Parse HTTP POST request to extract OCSP request body + * @param httpReq HTTP request buffer + * @param httpReqSz size of HTTP request + * @param body pointer to store body location (output) + * @param bodySz pointer to store body size (output) + * @return 0 on success, negative on error + */ +int wolfCLU_HttpServerParseRequest(const byte* httpReq, int httpReqSz, + const byte** body, int* bodySz); + +/* SCGI protocol functions */ +/** + * @brief Read and parse an SCGI request from socket + * @param sockfd socket descriptor + * @param buffer buffer to store the complete request + * @param bufferSz size of buffer + * @param req output structure to store parsed request (defined in clu_scgi.h) + * @return 0 on success, negative on error + */ +struct ScgiRequest { + const byte* body; + int bodyLen; + int contentLength; + const char* requestMethod; + const char* requestUri; +}; +typedef struct ScgiRequest ScgiRequest; +int wolfCLU_ScgiReadRequest(SOCKET_T sockfd, byte* buffer, int bufferSz, + ScgiRequest* req); + +/** + * @brief Send SCGI response with status and body + * @param sockfd socket descriptor + * @param statusCode HTTP status code + * @param statusText HTTP status text + * @param contentType MIME type + * @param body response body + * @param bodyLen size of response body + * @return 0 on success, negative on error + */ +int wolfCLU_ScgiSendResponse(SOCKET_T sockfd, int statusCode, + const char* statusText, const char* contentType, + const byte* body, int bodyLen); + +/** + * @brief Send SCGI error response + * @param sockfd socket descriptor + * @param statusCode HTTP status code + * @param statusText HTTP status text (also used as body) + * @return 0 on success, negative on error + */ +int wolfCLU_ScgiSendError(SOCKET_T sockfd, int statusCode, + const char* statusText); + +/** + * @brief Load certificate file in DER format (handles PEM conversion) + * @param filename certificate file path + * @param der pointer to store DER buffer (caller must XFREE) + * @param derSz pointer to store DER size + * @return 0 on success, negative on error + */ +int wolfCLU_LoadCertDer(const char* filename, byte** der, word32* derSz); + +/** + * @brief Load private key file in DER format (handles PEM conversion) + * @param filename key file path + * @param der pointer to store DER buffer (caller must XFREE) + * @param derSz pointer to store DER size + * @return 0 on success, negative on error + */ +int wolfCLU_LoadKeyDer(const char* filename, byte** der, word32* derSz); + +/** + * @brief Load certificate file and return DER size + * @param filename certificate file path + * @param outDer pointer to store DER buffer (caller must XFREE) + * @return DER size on success, negative on error + */ +int wolfCLU_ReadCertDer(const char* filename, byte** outDer); + + /** * @brief function to prompt user for password from stdin */ diff --git a/wolfclu/clu_optargs.h b/wolfclu/clu_optargs.h index 13981b50..b4bc91e0 100644 --- a/wolfclu/clu_optargs.h +++ b/wolfclu/clu_optargs.h @@ -48,6 +48,7 @@ enum { WOLFCLU_CA, WOLFCLU_DSA, WOLFCLU_DH, + WOLFCLU_OCSP, WOLFCLU_VERSION, WOLFCLU_CONNECT,