From 993f2268c568d2cce7ccc077cb819dd5fc5b31d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Negru=C8=9Biu?= Date: Sat, 3 Aug 2024 08:50:18 +0300 Subject: [PATCH 1/8] Fixed test tool command line --- src/nscurl/NScurl.vcxproj.user | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nscurl/NScurl.vcxproj.user b/src/nscurl/NScurl.vcxproj.user index 8dbca8f..dae4a15 100644 --- a/src/nscurl/NScurl.vcxproj.user +++ b/src/nscurl/NScurl.vcxproj.user @@ -1,22 +1,22 @@  - $(SolutionDir)tests\NScurl-Debug-$(PlatformNsis)-unicode.exe + $(SolutionDir)tests\NScurl-Debug-$(NsisPlatform)-unicode.exe WindowsLocalDebugger /dll "$(TargetPath)" - $(SolutionDir)tests\NScurl-Debug-$(PlatformNsis)-unicode.exe + $(SolutionDir)tests\NScurl-Debug-$(NsisPlatform)-unicode.exe WindowsLocalDebugger /dll "$(TargetPath)" - $(SolutionDir)tests\NScurl-Debug-$(PlatformNsis)-unicode.exe + $(SolutionDir)tests\NScurl-Debug-$(NsisPlatform)-unicode.exe WindowsLocalDebugger /dll "$(TargetPath)" - $(SolutionDir)tests\NScurl-Debug-$(PlatformNsis)-unicode.exe + $(SolutionDir)tests\NScurl-Debug-$(NsisPlatform)-unicode.exe WindowsLocalDebugger /dll "$(TargetPath)" From 5cfb3504107933d1d80d764d8416c7275dff3d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Negru=C8=9Biu?= Date: Sat, 3 Aug 2024 20:20:09 +0300 Subject: [PATCH 2/8] GH-28: Refactored `/CACERT` parameter. Added `/CASTORE` parameter * `/CACERT` parameter has new arguments `builtin|none|`. For backward compatibility `/CACERT ""` is equivalent to `/CACERT none` * Added `/CASTORE true|false`. True by default * By default, Windows' native CA store is used for validation in addition to the built-in `cacert.pem` * By default `CURLSSLOPT_NO_PARTIALCHAIN` is set to invalidate incomplete chains --- src/nscurl/NScurl.readme.md | 42 +++++++++++++++++++-------- src/nscurl/curl.c | 58 +++++++++++++++++++++++++++---------- src/nscurl/curl.h | 9 ++++-- src/nscurl/utils.h | 8 +++-- 4 files changed, 85 insertions(+), 32 deletions(-) diff --git a/src/nscurl/NScurl.readme.md b/src/nscurl/NScurl.readme.md index 42bcd81..71ced62 100644 --- a/src/nscurl/NScurl.readme.md +++ b/src/nscurl/NScurl.readme.md @@ -355,30 +355,48 @@ Be aware that during the transfer, the _content length_ indicates the compressed ### /CACERT ```nsis -/CACERT "path\to\cacert.pem" -/CACERT "" +/CACERT builtin|none|""| ``` -Validate webserver identity using a custom `cacert.pem` certificate database. -By default, a built-in `cacert.pem` is extracted and used at runtime. -`/CACERT ""` disables SSL validation (aka _insecure transfer_). + +Specify a `cacert.pem` database to be used for SSL certificate validation. + +Parameter | Details +:------------- | :--------------------------------------- +`builtin` | Use a built-in `cacert.pem` database, embedded into `NScurl.dll` at build time
This is the default option +`none` or `""` | Disable `cacert.pem` database usage +`` | Use an external `cacert.pem` file > [!caution] -> The embedded `cacert.pem` can become outdated. -> That would lead to legitimate websites failing the SSL validation. -> The `libcurl project` maintains an online [cacert.pem](https://curl.haxx.se/docs/caextract.html) database that is generally considered trusted. +> The built-in `cacert.pem` can become outdated. +> That could lead to legitimate webservers failing the SSL validation. +> `libcurl project` maintains an online [cacert.pem](https://curl.haxx.se/docs/caextract.html) database that is generally considered trusted. > Feel free to embed the latest version into your installer and feed it to `NScurl` +> [!caution] +> If all certificate sources are empty (e.g. `/CACERT none /CASTORE false` and no `/CERT` arguments), SSL certificate validation is disabled. `NScurl` will connect to any server, including untrusted ones (aka _insecure transfers_) + +### /CASTORE +```nsis +/CASTORE true|false +``` +Specify that Windows' native CA store should be used for SSL certificate validation. +This option is __enabled__ by default. +When enabled, the native CA store is used __in addition__ to the other trusted certificate sources ([/CACERT](#cacert) and [/CERT](#cert)) + ### /CERT ``` /CERT "sha1 thumbprint" ``` -Specify an additional trusted certificate (e.g. `/CERT 917e732d330f9a12404f73d8bea36948b929dffc`) -Trusted certificates are used for SSL validation in addition to the `cacert.pem` database. +Specify an additional __trusted certificate__ (e.g. `/CERT 917e732d330f9a12404f73d8bea36948b929dffc`). +When specified, trusted certificates are used for SSL certificate validation __in addition__ to other trusted certificate sources ([/CACERT](#cacert) and [/CASTORE](#castore)). Trusted certificates can reference any certificate in the chain (end-entity cert, intermediate cert, root cert). Multiple `/CERT` parameters are allowed. -> [!tip] -> `cacert.pem` database can be disabled (`/CACERT ""`) leaving the `/CERT` trusted certificates in charge with the SSL validation (aka _certificate pinning_) +Example: +```nsis +# Certificate pinning (accepts only 1111.. and 2222.. certificates) +NScurl::http GET ${url} ${file} /CACERT none /CASTORE false /CERT 1111111111111111111111111111111111111111 /CERT 2222222222222222222222222222222222222222 /END +``` ### /DEPEND ``` diff --git a/src/nscurl/curl.c b/src/nscurl/curl.c index 3005a9f..ae4c8cd 100644 --- a/src/nscurl/curl.c +++ b/src/nscurl/curl.c @@ -4,7 +4,7 @@ #include "main.h" #include "curl.h" -#include +#include "openssl/ssl.h" #include "crypto.h" @@ -469,10 +469,28 @@ ULONG CurlParseRequestParam( _In_ ULONG iParamIndex, _In_ LPTSTR pszParam, _In_ } } else if (lstrcmpi( pszParam, _T( "/CACERT" ) ) == 0) { if (popstring( pszParam ) == NOERROR) { /// pszParam may be empty ("") - LPTSTR path = MyCanonicalizePath(pszParam); - MyFree( pReq->pszCacert ); - pReq->pszCacert = MyStrDup( eT2A, path ); - MyFree(path); + if (lstrcmpi(pszParam, _T("builtin")) == 0) { + MyFree(pReq->pszCacert); + pReq->pszCacert = CACERT_BUILTIN; + } else if (lstrcmpi(pszParam, _T("none")) == 0 || lstrcmp(pszParam, _T("")) == 0) { + MyFree(pReq->pszCacert); + pReq->pszCacert = CACERT_NONE; + } else { + LPTSTR path = MyCanonicalizePath(pszParam); + MyFree(pReq->pszCacert); + pReq->pszCacert = MyStrDup(eT2A, path); + MyFree(path); + } + } + } else if (lstrcmpi( pszParam, _T( "/CASTORE" ) ) == 0) { + if (popstring( pszParam ) == NOERROR) { + if (lstrcmpi(pszParam, _T("true")) == 0) { + pReq->bCastore = TRUE; + } else if (lstrcmpi(pszParam, _T("false")) == 0) { + pReq->bCastore = FALSE; + } else { + err = ERROR_INVALID_PARAMETER; + } } } else if (lstrcmpi( pszParam, _T( "/CERT" ) ) == 0) { if (popstring( pszParam ) == NOERROR && *pszParam) { @@ -1031,29 +1049,39 @@ void CurlTransfer( _In_ PCURL_REQUEST pReq ) curl_easy_setopt(curl, CURLOPT_SSL_ENABLE_ALPN, 0L); /// Disable ALPN. No negotiation for HTTP2 takes place /// SSL - if (!StringIsEmpty(pReq->pszCacert) || pReq->pCertList) { + if (pReq->pszCacert != CACERT_NONE || pReq->bCastore || pReq->pCertList) { + // SSL validation enabled curl_easy_setopt( curl, CURLOPT_SSL_VERIFYPEER, TRUE ); /// Verify SSL certificate curl_easy_setopt( curl, CURLOPT_SSL_VERIFYHOST, 2 ); /// Validate host name + // cacert.pem - if (pReq->pszCacert == NULL) { + if (pReq->pszCacert == CACERT_BUILTIN) { struct curl_blob cacert; - CurlBuiltinCacert( &cacert ); - assert( cacert.data ); - assert( cacert.len > 0 ); - curl_easy_setopt( curl, CURLOPT_CAINFO_BLOB, &cacert ); /// Embedded cacert.pem - } else if (!StringIsEmpty( pReq->pszCacert )) { - curl_easy_setopt( curl, CURLOPT_CAINFO, pReq->pszCacert ); /// Custom cacert.pem - assert( MyFileExistsA( pReq->pszCacert ) ); + CurlBuiltinCacert(&cacert); + assert(cacert.data); + assert(cacert.len > 0); + curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &cacert); /// Embedded cacert.pem + } else if (pReq->pszCacert == CACERT_NONE) { + curl_easy_setopt(curl, CURLOPT_CAINFO, NULL); /// No cacert.pem } else { - curl_easy_setopt( curl, CURLOPT_CAINFO, NULL ); /// No cacert.pem + assert(pReq->pszCacert&& lstrlenA(pReq->pszCacert) > 0); + assert(MyFileExistsA(pReq->pszCacert)); + curl_easy_setopt(curl, CURLOPT_CAINFO, pReq->pszCacert); /// Custom cacert.pem } + // Additional trusted certificates if (pReq->pCertList) { // SSL callback curl_easy_setopt( curl, CURLOPT_SSL_CTX_FUNCTION, CurlSSLCallback ); curl_easy_setopt( curl, CURLOPT_SSL_CTX_DATA, pReq ); } + + ULONG sslopt = CURLSSLOPT_NO_PARTIALCHAIN; // full chains only + if (pReq->bCastore) + sslopt |= CURLSSLOPT_NATIVE_CA; + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, sslopt); + } else { // SSL validation disabled curl_easy_setopt( curl, CURLOPT_SSL_VERIFYPEER, FALSE ); diff --git a/src/nscurl/curl.h b/src/nscurl/curl.h index 98690e7..935cacd 100644 --- a/src/nscurl/curl.h +++ b/src/nscurl/curl.h @@ -14,6 +14,9 @@ /// \brief Filename reserved for in-memory transfers. #define FILENAME_MEMORY _T("Memory") +#define CACERT_BUILTIN NULL +#define CACERT_NONE ((LPCSTR)1) + //+ struct CURL_REQUEST typedef struct _CURL_REQUEST { LPCSTR pszURL; @@ -37,8 +40,9 @@ typedef struct _CURL_REQUEST { BOOLEAN bMarkOfTheWeb : 1; BOOLEAN bHttp11 : 1; BOOLEAN bEncoding : 1; - LPCSTR pszCacert; /// can be NULL. If valid and empty ("") no cacert.pem is used - struct curl_slist *pCertList; /// can be NULL. If pszCacert=="" and pCertList==NULL, the SSL validation is turned off + BOOLEAN bCastore : 1; /// Use native CA store (CURLSSLOPT_NATIVE_CA) + LPCSTR pszCacert; /// can be CACERT_BUILTIN(NULL), CACERT_NONE, or a file path + struct curl_slist *pCertList; /// can be NULL LPCTSTR pszDebugFile; /// can be NULL ULONG iConnectTimeout; /// can be 0. Connecting timeout ULONG iCompleteTimeout; /// can be 0. Complete (connect + transfer) timeout @@ -96,6 +100,7 @@ static void CurlRequestInit( _Inout_ PCURL_REQUEST pReq ) { if (!pReq) return; ZeroMemory( pReq, sizeof( *pReq ) ); pReq->Runtime.iRootCertFlags = (ULONG)-1; // Uninitialized + pReq->bCastore = TRUE; } //+ CurlRequestDestroy diff --git a/src/nscurl/utils.h b/src/nscurl/utils.h index eb44b54..a8d771c 100644 --- a/src/nscurl/utils.h +++ b/src/nscurl/utils.h @@ -70,9 +70,11 @@ static LPVOID MyAlloc( _In_ ULONG iSize ) { //+ MyFree #define MyFree(_ptr) { \ if ( _ptr ) { \ - g_MemStats.FreeBytes += GlobalSize((HGLOBAL)(_ptr)); \ - g_MemStats.FreeCalls++; \ - GlobalFree((HGLOBAL)(_ptr)); \ + if ((ULONG_PTR)(_ptr) > 0x1000) { \ + g_MemStats.FreeBytes += GlobalSize((HGLOBAL)(_ptr)); \ + g_MemStats.FreeCalls++; \ + GlobalFree((HGLOBAL)(_ptr)); \ + } \ (_ptr) = NULL; \ }} From 9052213a3f3600627ac2a34d7e41ae749d725050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Negru=C8=9Biu?= Date: Sat, 3 Aug 2024 20:20:42 +0300 Subject: [PATCH 3/8] GH-28: Added self-signed certificate tests --- tests/NScurl-Debug.nsi | 133 ++++++++++++++++++++++++++++++++++++ tests/NScurl-Test.nsi | 120 ++++++++++++++++++++++++++++++++ tests/badssl-selfsigned.crt | 21 ++++++ 3 files changed, 274 insertions(+) create mode 100644 tests/badssl-selfsigned.crt diff --git a/tests/NScurl-Debug.nsi b/tests/NScurl-Debug.nsi index 633b7dd..5d87613 100644 --- a/tests/NScurl-Debug.nsi +++ b/tests/NScurl-Debug.nsi @@ -64,6 +64,9 @@ RequestExecutionLevel user ; Don't require UAC elevation ShowInstDetails show ManifestDPIAware true +Var /global g_testCount +Var /global g_testFails + !macro STACK_VERIFY_START Push "MyStackTop" ; Mark the top of the stack !macroend @@ -81,6 +84,8 @@ Function .onInit ; Initializations InitPluginsDir + StrCpy $g_testCount 0 + StrCpy $g_testFails 0 ; Language selection !define MUI_LANGDLL_ALLLANGUAGES @@ -658,6 +663,129 @@ Section "Big file (10GB)" SectionEnd +SectionGroup /e "Tests" + +!macro CERT_TEST url file cacert castore cert errortype errorcode + StrCpy $R0 '${file}' + ${If} `${cacert}` == "" + StrCpy $R0 '$R0_default' + ${ElseIf} `${cacert}` == "none" + ${OrIf} `${cacert}` == "builtin" + StrCpy $R0 '$R0_${cacert}' + ${Else} + StrCpy $R0 '$R0_file' + ${EndIf} + ${If} `${castore}` == "" + StrCpy $R0 '$R0_default' + ${Else} + StrCpy $R0 '$R0_${castore}' + ${EndIf} + ${If} `${cert}` == "" + StrCpy $R0 '$R0_nocert' + ${Else} + StrCpy $R0 '$R0_${cert}' + ${EndIf} + + DetailPrint 'NScurl::http "${url}" "$R0.html"' + !insertmacro STACK_VERIFY_START + Push "/END" + ${If} `${cacert}` != "" + Push `${cacert}` + Push /CACERT + ${EndIf} + ${If} `${castore}` != "" + Push `${castore}` + Push /CASTORE + ${EndIf} + ${If} `${cert}` != "" + Push `${cert}` + Push /CERT + ${EndIf} + Push "$R0.debug.txt" + Push "nodata" + Push /DEBUG + Push "test" + Push /TAG + Push "@ID@" + Push /RETURN + Push "$R0.html" + Push "${url}" + Push "GET" + CallInstDLL $DLL http + Pop $0 + + Push "@Error@" + Push $0 + Push /ID + CallInstDLL $DLL query + Pop $1 + ${If} $1 != "OK" + DetailPrint "Status: $1" + ${EndIf} + + Push "@ErrorType@" + Push $0 + Push /ID + CallInstDLL $DLL query + Pop $1 + + Push "@ErrorCode@" + Push $0 + Push /ID + CallInstDLL $DLL query + Pop $2 + + IntOp $g_testCount $g_testCount + 1 + ${If} $1 == ${errortype} + ${AndIf} $2 = ${errorcode} + DetailPrint "(OK) $1 $2" + ${Else} + IntOp $g_testFails $g_testFails + 1 + DetailPrint "----- FAIL ----- $1 $2 => expected ${errortype} ${errorcode}" + ${EndIf} + !insertmacro STACK_VERIFY_END +!macroend + +Section "Self-signed certificate" + SectionIn ${INSTTYPE_MOST} + DetailPrint '=====[ ${__SECTION__} ]===============================' + + !define /redef LINK 'https://self-signed.badssl.com' + !define /redef FILE '$EXEDIR\_testcert' + + ; Valid to: ‎Sunday, ‎May ‎17, ‎2026 8:59:33 PM + ${IfNot} ${FileExists} "$PLUGINSDIR\badssl-selfsigned.crt" + File "/oname=$PLUGINSDIR\badssl-selfsigned.crt" "badssl-selfsigned.crt" + ${EndIf} + !define /ifndef SELFSIGNED_CERT '9dff24e1dbeec15f90751e7af364d417d65cb8cd' ; `badssl-selfsigned.crt` thumbprint + + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' '' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' '' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' '$PLUGINSDIR\badssl-selfsigned.crt' '' '' 'http' 200 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'true' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'false' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' 'curl' 0x3c + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' 'http' 200 ; SSL validation disabled + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '1111111111111111111111111111111111111111' 'curl' 0x3c ; CURLE_PEER_FAILED_VERIFICATION + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '1111111111111111111111111111111111111111' 'curl' 0x23 ; CURLE_SSL_CONNECT_ERROR + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' ${SELFSIGNED_CERT} 'http' 200 + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' ${SELFSIGNED_CERT} 'http' 200 + + Push /REMOVE + Push "test" + Push /TAG + CallInstDLL $DLL cancel ; no return + +SectionEnd + +SectionGroupEnd + + SectionGroup /e "Errors" Section "httpbin.org/get/status/40x" @@ -1334,3 +1462,8 @@ SectionEnd SectionGroupEnd ; Extra + + +Section -Final + DetailPrint 'Total tests: $g_testCount, Failed tests: $g_testFails' +SectionEnd diff --git a/tests/NScurl-Test.nsi b/tests/NScurl-Test.nsi index 9a8dfc5..0972df0 100644 --- a/tests/NScurl-Test.nsi +++ b/tests/NScurl-Test.nsi @@ -63,6 +63,9 @@ RequestExecutionLevel user ; Don't require UAC elevation ShowInstDetails show ManifestDPIAware true +Var /global g_testCount +Var /global g_testFails + #---------------------------------------------------------------# # .onInit # #---------------------------------------------------------------# @@ -70,6 +73,8 @@ Function .onInit ; Initializations InitPluginsDir + StrCpy $g_testCount 0 + StrCpy $g_testFails 0 ; Language selection !define MUI_LANGDLL_ALLLANGUAGES @@ -403,6 +408,116 @@ Section "Big file (10GB)" SectionEnd +SectionGroup /e "Tests" + +!macro CERT_TEST url file cacert castore cert errortype errorcode + StrCpy $R0 '${file}' + ${If} `${cacert}` == "" + StrCpy $R0 '$R0_default' + ${ElseIf} `${cacert}` == "none" + ${OrIf} `${cacert}` == "builtin" + StrCpy $R0 '$R0_${cacert}' + ${Else} + StrCpy $R0 '$R0_file' + ${EndIf} + ${If} `${castore}` == "" + StrCpy $R0 '$R0_default' + ${Else} + StrCpy $R0 '$R0_${castore}' + ${EndIf} + ${If} `${cert}` == "" + StrCpy $R0 '$R0_nocert' + ${Else} + StrCpy $R0 '$R0_${cert}' + ${EndIf} + + DetailPrint 'NScurl::http "${url}" "$R0.html"' + + Push "/END" + ${If} `${cacert}` != "" + Push `${cacert}` + Push /CACERT + ${EndIf} + ${If} `${castore}` != "" + Push `${castore}` + Push /CASTORE + ${EndIf} + ${If} `${cert}` != "" + Push `${cert}` + Push /CERT + ${EndIf} + Push "$R0.debug.txt" + Push "nodata" + Push /DEBUG + Push "test" + Push /TAG + Push "@ID@" + Push /RETURN + Push "$R0.html" + Push "${url}" + Push "GET" + CallInstDLL "$PLUGINSDIR\NScurl.dll" http + Pop $0 + + NScurl::query /ID $0 "@Error@" + Pop $1 + ${If} $1 != "OK" + DetailPrint "Status: $1" + ${EndIf} + + NScurl::query /ID $0 "@ErrorType@" + Pop $1 + + NScurl::query /ID $0 "@ErrorCode@" + Pop $2 + + IntOp $g_testCount $g_testCount + 1 + ${If} $1 == ${errortype} + ${AndIf} $2 = ${errorcode} + DetailPrint "(OK) $1 $2" + ${Else} + IntOp $g_testFails $g_testFails + 1 + DetailPrint "----- FAIL ----- $1 $2 => expected ${errortype} ${errorcode}" + ${EndIf} +!macroend + +Section "Self-signed certificate" + SectionIn ${INSTTYPE_MOST} + DetailPrint '=====[ ${__SECTION__} ]===============================' + + !define /redef LINK 'https://self-signed.badssl.com' + !define /redef FILE '$EXEDIR\_testcert' + + ; Valid to: ‎Sunday, ‎May ‎17, ‎2026 8:59:33 PM + ${IfNot} ${FileExists} "$PLUGINSDIR\badssl-selfsigned.crt" + File "/oname=$PLUGINSDIR\badssl-selfsigned.crt" "badssl-selfsigned.crt" + ${EndIf} + !define /ifndef SELFSIGNED_CERT '9dff24e1dbeec15f90751e7af364d417d65cb8cd' ; `badssl-selfsigned.crt` thumbprint + + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' '' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' '' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' '$PLUGINSDIR\badssl-selfsigned.crt' '' '' 'http' 200 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'true' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'false' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' 'curl' 0x3c + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' 'http' 200 ; SSL validation disabled + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '1111111111111111111111111111111111111111' 'curl' 0x3c ; CURLE_PEER_FAILED_VERIFICATION + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '1111111111111111111111111111111111111111' 'curl' 0x23 ; CURLE_SSL_CONNECT_ERROR + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' ${SELFSIGNED_CERT} 'http' 200 + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' ${SELFSIGNED_CERT} 'http' 200 + + NScurl::cancel /TAG "test" /REMOVE + +SectionEnd + +SectionGroupEnd + + SectionGroup /e "Errors" Section "httpbin.org/get/status/40x" @@ -789,3 +904,8 @@ SectionEnd SectionGroupEnd ; Extra + + +Section -Final + DetailPrint 'Total tests: $g_testCount, Failed tests: $g_testFails' +SectionEnd diff --git a/tests/badssl-selfsigned.crt b/tests/badssl-selfsigned.crt new file mode 100644 index 0000000..45099f6 --- /dev/null +++ b/tests/badssl-selfsigned.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDeTCCAmGgAwIBAgIJANuSS2L+9oTlMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0y +NDA1MTcxNzU5MzNaFw0yNjA1MTcxNzU5MzNaMGIxCzAJBgNVBAYTAlVTMRMwEQYD +VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK +DAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2 +PmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW +hyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A +xPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve +ww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY +QCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T +BAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI +hvcNAQELBQADggEBAH1tiJTqI9nW4Vr3q6joNV7+hNKS2OtgqBxQhMVWWWr4mRDf +ayfr4eAJkiHv8/Fvb6WqbGmzClCVNVOrfTzHeLsfROLLmlkYqXSST76XryQR6hyt +4qWqGd4M+MUNf7ty3zcVF0Yt2vqHzp4y8m+mE5nSqRarAGvDNJv+I6e4Edw19u1j +ddjiqyutdMsJkgvfNvSLQA8u7SAVjnhnoC6n2jm2wdFbrB+9rnrGje+Q8r1ERFyj +SG26SdQCiaG5QBCuDhrtLSR1N90URYCY0H6Z57sWcTKEusb95Pz6cBTLGuiNDKJq +juBzebaanR+LTh++Bleb9I0HxFFCTwlQhxo/bfY= +-----END CERTIFICATE----- From 7e3d3fb79bbd365f0345c4875c6e774b61aee01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Negru=C8=9Biu?= Date: Sat, 3 Aug 2024 22:46:30 +0300 Subject: [PATCH 4/8] GH-28: Collect and export openssl x509 certificate validation errors * SSL callback function is always set (regardless whether `/CERT` has been used) * `@ErrorType@` query keyword may return `x509`, in which case `@ErrorCode@` returns an X509_V_xxx error code * Added "Expired certificate", "Wrong host", "Untrusted Root" tests --- src/nscurl/NScurl.readme.md | 16 +++-- src/nscurl/curl.c | 138 +++++++++++++++++++++--------------- src/nscurl/curl.h | 3 + tests/NScurl-Debug.nsi | 99 +++++++++++++++++++++----- tests/NScurl-Test.nsi | 91 +++++++++++++++++++----- 5 files changed, 250 insertions(+), 97 deletions(-) diff --git a/src/nscurl/NScurl.readme.md b/src/nscurl/NScurl.readme.md index 71ced62..cb0c134 100644 --- a/src/nscurl/NScurl.readme.md +++ b/src/nscurl/NScurl.readme.md @@ -373,7 +373,8 @@ Parameter | Details > Feel free to embed the latest version into your installer and feed it to `NScurl` > [!caution] -> If all certificate sources are empty (e.g. `/CACERT none /CASTORE false` and no `/CERT` arguments), SSL certificate validation is disabled. `NScurl` will connect to any server, including untrusted ones (aka _insecure transfers_) +> If all certificate sources are empty (e.g. `/CACERT none /CASTORE false` and no `/CERT` arguments), SSL certificate validation is disabled. `NScurl` would connect to any server, including untrusted ones (aka _insecure transfers_). +> By default, both the built-in `cacert.pem` and the __native CA store__ are used for validation. ### /CASTORE ```nsis @@ -710,14 +711,21 @@ Failed transfers return various error messages (e.g `0x2a "Callback aborted"`, e ### @ERRORCODE@ The numeric _transfer status_ code. -It can be either an HTTP status code (i.e. 200, 206, 404), a libcurl error code (7, 10), or a Win32 error code (i.e. 0x2a) +A value of `0` indicates success. See [@ERRORTYPE@](#errortype) for details. ### @ERRORTYPE@ -Returns `win32`, `curl` or `http` error type. +The _transfer status_ error type. + +Type | Meaning | Docs +:--------- | :----------------------------- | :----- +`win32` | Win32 error code | `ERROR_SUCCESS`(0) indicates success
https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes +`x509` | `openssl/x509` certificate error | `X509_V_OK`(0) indicates success
https://github.com/openssl/openssl/blob/ca1d2db291530a827555b40974ed81efb91c2d19/include/openssl/x509_vfy.h.in#L206 +`curl` | `libcurl` error code | `CURLE_OK`(0) indicates success
https://curl.se/libcurl/c/libcurl-errors.html +`HTTP` | HTTP status code | `2xx` indicates success
https://en.wikipedia.org/wiki/List_of_HTTP_status_codes ### @CANCELLED@ Indicates whether the transfer was cancelled by the user. -Returns boolean values `0` or `1` +Returns boolean value `0` or `1` ******************************************************************************* diff --git a/src/nscurl/curl.c b/src/nscurl/curl.c index ae4c8cd..c1b7fa0 100644 --- a/src/nscurl/curl.c +++ b/src/nscurl/curl.c @@ -84,6 +84,11 @@ void CurlRequestFormatError( _In_ PCURL_REQUEST pReq, _In_opt_ LPTSTR pszError, if (pbSuccess) *pbSuccess = FALSE; if (pszError) _sntprintf( pszError, iErrorLen, _T( "0x%lx \"%s\"" ), pReq->Error.iWin32, pReq->Error.pszWin32 ); if (piErrorCode) *piErrorCode = pReq->Error.iWin32; + } else if (pReq->Error.iX509 != X509_V_OK) { + // openssl/x509 error + if (pbSuccess) *pbSuccess = FALSE; + if (pszError) _sntprintf( pszError, iErrorLen, _T( "%d \"%hs\"" ), pReq->Error.iX509, pReq->Error.pszX509 ); + if (piErrorCode) *piErrorCode = (ULONG)pReq->Error.iX509; } else if (pReq->Error.iCurl != CURLE_OK) { // CURL error if (pbSuccess) *pbSuccess = FALSE; @@ -146,6 +151,8 @@ LPCSTR CurlRequestErrorType( _In_ PCURL_REQUEST pReq ) if (pReq) { if (pReq->Error.iWin32 != ERROR_SUCCESS) { return "win32"; + } else if (pReq->Error.iX509 != X509_V_OK) { + return "x509"; } else if (pReq->Error.iCurl != CURLE_OK) { return "curl"; } else if (pReq->Error.iHttp >= 0) { @@ -162,6 +169,8 @@ ULONG CurlRequestErrorCode( _In_ PCURL_REQUEST pReq ) if (pReq) { if (pReq->Error.iWin32 != ERROR_SUCCESS) { return pReq->Error.iWin32; + } else if (pReq->Error.iX509 != X509_V_OK) { + return pReq->Error.iX509; } else if (pReq->Error.iCurl != CURLE_OK) { return pReq->Error.iCurl; } else if (pReq->Error.iHttp > 0) { @@ -544,64 +553,83 @@ ULONG CurlParseRequestParam( _In_ ULONG iParamIndex, _In_ LPTSTR pszParam, _In_ //++ OpenSSLVerifyCallback int OpenSSLVerifyCallback( int preverify_ok, X509_STORE_CTX *x509_ctx ) { - //? This function gets called to evaluate server's SSL certificate chain - //? The calls are made sequentially for each certificate in the chain - //? This usually happens three times per connection: #2(root certificate), #1(intermediate certificate), #0(user certificate) - - //? Our logic: - //? * We return TRUE for every certificate, to get a chance to inspect the next in chain - //? * We return the final value when we reach the last certificate (depth 0) - //? If we're dealing with a TRUSTED certificate we force a positive response - //? Otherwise we return whatever verdict OpenSSL has already assigned to the chain + // This function gets called to evaluate server's SSL certificate chain + // The calls are made sequentially for each certificate in the chain + // This usually happens three times per connection: #2(root certificate), #1(intermediate certificate), #0(peer certificate) - UCHAR Thumbprint[20]; /// sha1 - char szThumbprint[41]; - struct curl_slist *p; + int ret = preverify_ok; SSL *ssl = X509_STORE_CTX_get_ex_data( x509_ctx, SSL_get_ex_data_X509_STORE_CTX_idx() ); - SSL_CTX *ssl_ctx = SSL_get_SSL_CTX( ssl ); - PCURL_REQUEST pReq = (PCURL_REQUEST)SSL_CTX_get_app_data( ssl_ctx ); - - X509 *cert = X509_STORE_CTX_get_current_cert( x509_ctx ); /// Current certificate in chain - int err = X509_STORE_CTX_get_error( x509_ctx ); /// Current OpenSSL certificate validation error - int depth = X509_STORE_CTX_get_error_depth( x509_ctx ); /// Certificate index/depth in the chain. Starts with root certificate (e.g. #2), ends with user certificate (#0) + SSL_CTX *sslctx = SSL_get_SSL_CTX( ssl ); + PCURL_REQUEST pReq = (PCURL_REQUEST)SSL_CTX_get_app_data( sslctx ); - //x X509_NAME_oneline( X509_get_subject_name( cert ), szSubject, ARRAYSIZE( szSubject ) ); - //x X509_NAME_oneline( X509_get_issuer_name( cert ), szIssuer, ARRAYSIZE( szIssuer ) ); - - DBG_UNREFERENCED_LOCAL_VARIABLE(err); - - // Extract certificate SHA1 fingerprint - X509_digest( cert, EVP_sha1(), Thumbprint, NULL ); - MyFormatBinaryHexA( Thumbprint, sizeof( Thumbprint ), szThumbprint, sizeof( szThumbprint ) ); + if (pReq->pCertList) + { + // Our logic: + // * We return TRUE for every certificate, to get a chance to inspect the next in chain + // * We return the final value when we reach the last certificate (depth 0) + // If we're dealing with a TRUSTED certificate we force a positive response + // Otherwise we return whatever verdict OpenSSL has already assigned to the chain + + X509* cert = X509_STORE_CTX_get_current_cert(x509_ctx); // Current certificate in the chain + int err = X509_STORE_CTX_get_error(x509_ctx); // Current OpenSSL certificate validation error + int depth = X509_STORE_CTX_get_error_depth(x509_ctx); // Certificate index/depth in the chain. Starts with root certificate (e.g. #2), ends with peer certificate (#0) + + //x X509_NAME_oneline( X509_get_subject_name( cert ), szSubject, ARRAYSIZE( szSubject ) ); + //x X509_NAME_oneline( X509_get_issuer_name( cert ), szIssuer, ARRAYSIZE( szIssuer ) ); + + // Extract certificate SHA1 fingerprint + UCHAR Thumbprint[20]; // sha1 + char szThumbprint[41]; + X509_digest(cert, EVP_sha1(), Thumbprint, NULL); + MyFormatBinaryHexA(Thumbprint, sizeof(Thumbprint), szThumbprint, sizeof(szThumbprint)); + + // Verify against our trusted certificate list + struct curl_slist* p; + for (p = pReq->pCertList; p; p = p->next) + { + if (lstrcmpiA(p->data, szThumbprint) == 0) + { - // Verify against our trusted certificate list - for (p = pReq->pCertList; p; p = p->next) { - if (lstrcmpiA( p->data, szThumbprint ) == 0) { + pReq->Runtime.bTrustedCert = TRUE; + X509_STORE_CTX_set_error(x509_ctx, X509_V_OK); + break; + } + } - pReq->Runtime.bTrustedCert = TRUE; - X509_STORE_CTX_set_error( x509_ctx, X509_V_OK ); - break; + // Verdict + if (depth > 0) + { + TRACE(_T("Certificate( #%d, \"%hs\", PreVerify{OK:%hs, Err:%d} ) = %hs, Response{OK:%hs, Err:%d}\n"), depth, szThumbprint, preverify_ok ? "TRUE" : "FALS", err, p ? "TRUSTED" : "UNKNOWN", "TRUE", err); + ret = TRUE; + } + else + { + if (pReq->Runtime.bTrustedCert) + { + // We've found at least one TRUSTED certificate + // Clear all errors, return a positive verdict + X509_STORE_CTX_set_error(x509_ctx, X509_V_OK); + TRACE(_T("Certificate( #%d, \"%hs\", PreVerify{OK:%hs, Err:%d} ) = %hs, Response{OK:%hs, Err:%d}\n"), depth, szThumbprint, preverify_ok ? "TRUE" : "FALS", err, p ? "TRUSTED" : "UNKNOWN", "TRUE", X509_V_OK); + ret = TRUE; + } else { + // We haven't found any TRUSTED certificate + // Return whatever verdict already made by OpenSSL + TRACE(_T("Certificate( #%d, \"%hs\", PreVerify{OK:%hs, Err:%d} ) = %hs, Response{OK:%hs, Err:%d}\n"), depth, szThumbprint, preverify_ok ? "TRUE" : "FALS", err, p ? "TRUSTED" : "UNKNOWN", preverify_ok ? "TRUE" : "FALS", err); + } } } - // Verdict - if (depth > 0) { - TRACE( _T( "Certificate( #%d, \"%hs\", PreVerify{OK:%hs, Err:%d} ) = %hs, Response{OK:%hs, Err:%d}\n" ), depth, szThumbprint, preverify_ok ? "TRUE" : "FALS", err, p ? "TRUSTED" : "UNKNOWN", "TRUE", err ); - return TRUE; - } else { - if (pReq->Runtime.bTrustedCert) { - /// We've found at least one TRUSTED certificate - /// Clear all errors, return a positive verdict - X509_STORE_CTX_set_error( x509_ctx, X509_V_OK ); - TRACE( _T( "Certificate( #%d, \"%hs\", PreVerify{OK:%hs, Err:%d} ) = %hs, Response{OK:%hs, Err:%d}\n" ), depth, szThumbprint, preverify_ok ? "TRUE" : "FALS", err, p ? "TRUSTED" : "UNKNOWN", "TRUE", X509_V_OK ); - return TRUE; - } - /// We haven't found any TRUSTED certificate - /// Return whatever verdict already made by OpenSSL - TRACE( _T( "Certificate( #%d, \"%hs\", PreVerify{OK:%hs, Err:%d} ) = %hs, Response{OK:%hs, Err:%d}\n" ), depth, szThumbprint, preverify_ok ? "TRUE" : "FALS", err, p ? "TRUSTED" : "UNKNOWN", preverify_ok ? "TRUE" : "FALS", err ); - return preverify_ok; + // Remember the last x509 error + int ex509 = X509_STORE_CTX_get_error(x509_ctx); + if (ex509 != X509_V_OK && ex509 != pReq->Error.iX509) + { + pReq->Error.iX509 = ex509; + MyFree(pReq->Error.pszX509); + pReq->Error.pszX509 = MyStrDup(eA2A, X509_verify_cert_error_string(ex509)); } + + return ret; } @@ -1070,12 +1098,9 @@ void CurlTransfer( _In_ PCURL_REQUEST pReq ) curl_easy_setopt(curl, CURLOPT_CAINFO, pReq->pszCacert); /// Custom cacert.pem } - // Additional trusted certificates - if (pReq->pCertList) { - // SSL callback - curl_easy_setopt( curl, CURLOPT_SSL_CTX_FUNCTION, CurlSSLCallback ); - curl_easy_setopt( curl, CURLOPT_SSL_CTX_DATA, pReq ); - } + // SSL callback + curl_easy_setopt( curl, CURLOPT_SSL_CTX_FUNCTION, CurlSSLCallback ); + curl_easy_setopt( curl, CURLOPT_SSL_CTX_DATA, pReq ); ULONG sslopt = CURLSSLOPT_NO_PARTIALCHAIN; // full chains only if (pReq->bCastore) @@ -1277,8 +1302,9 @@ void CurlTransfer( _In_ PCURL_REQUEST pReq ) // Finished? CurlRequestFormatError( pReq, NULL, 0, &bSuccess, &e ); - TRACE(_T("curl_easy_perform() = {win32:0x%lx \"%s\", curl:%u \"%hs\", http:%u \"%hs\"}, re/connect:%lus/%lus, insist:%s\n"), + TRACE(_T("curl_easy_perform() = {win32:0x%lx \"%s\", x509:%d \"%hs\", curl:%u \"%hs\", http:%u \"%hs\"}, re/connect:%lus/%lus, insist:%s\n"), pReq->Error.iWin32, pReq->Error.pszWin32 ? pReq->Error.pszWin32 : _T(""), + pReq->Error.iX509, pReq->Error.pszX509 ? pReq->Error.pszX509 : "", pReq->Error.iCurl, pReq->Error.pszCurl ? pReq->Error.pszCurl : "", pReq->Error.iHttp, pReq->Error.pszHttp ? pReq->Error.pszHttp : "", (GetTickCount() - iConnectionTimeStart) / 1000, @@ -1314,7 +1340,7 @@ void CurlTransfer( _In_ PCURL_REQUEST pReq ) ((pReq->iCompleteTimeout > 0) && (pReq->Runtime.iTimeElapsed >= pReq->iCompleteTimeout))) /// Enforce "Complete" timeout { // NOTE: Don't overwrite previous error codes - if (pReq->Error.iWin32 == ERROR_SUCCESS && pReq->Error.iCurl == CURLE_OK && pReq->Error.iHttp == 0) { + if (pReq->Error.iWin32 == ERROR_SUCCESS && pReq->Error.iX509 == X509_V_OK && pReq->Error.iCurl == CURLE_OK && pReq->Error.iHttp == 0) { MyFree( pReq->Error.pszWin32 ); pReq->Error.iWin32 = ERROR_TIMEOUT; pReq->Error.pszWin32 = MyFormatError( pReq->Error.iWin32 ); diff --git a/src/nscurl/curl.h b/src/nscurl/curl.h index 935cacd..26b62e8 100644 --- a/src/nscurl/curl.h +++ b/src/nscurl/curl.h @@ -90,6 +90,8 @@ typedef struct _CURL_REQUEST { LPCTSTR pszWin32; CURLcode iCurl; LPCSTR pszCurl; + int iX509; + LPCSTR pszX509; int iHttp; LPCSTR pszHttp; } Error; @@ -138,6 +140,7 @@ static void CurlRequestDestroy( _Inout_ PCURL_REQUEST pReq ) { MyFree( pReq->Runtime.pszServerIP ); MyFree( pReq->Error.pszWin32 ); MyFree( pReq->Error.pszCurl ); + MyFree( pReq->Error.pszX509 ); MyFree( pReq->Error.pszHttp ); ZeroMemory( pReq, sizeof( *pReq ) ); } diff --git a/tests/NScurl-Debug.nsi b/tests/NScurl-Debug.nsi index 5d87613..61b37ca 100644 --- a/tests/NScurl-Debug.nsi +++ b/tests/NScurl-Debug.nsi @@ -683,7 +683,8 @@ SectionGroup /e "Tests" ${If} `${cert}` == "" StrCpy $R0 '$R0_nocert' ${Else} - StrCpy $R0 '$R0_${cert}' + StrCpy $0 `${cert}` 8 + StrCpy $R0 '$R0_$0' ${EndIf} DetailPrint 'NScurl::http "${url}" "$R0.html"' @@ -738,43 +739,105 @@ SectionGroup /e "Tests" IntOp $g_testCount $g_testCount + 1 ${If} $1 == ${errortype} ${AndIf} $2 = ${errorcode} - DetailPrint "(OK) $1 $2" + DetailPrint "[ OK ] $1/$2" ${Else} IntOp $g_testFails $g_testFails + 1 - DetailPrint "----- FAIL ----- $1 $2 => expected ${errortype} ${errorcode}" + DetailPrint "----- FAIL ----- $1/$2 => expected ${errortype}/${errorcode}" ${EndIf} !insertmacro STACK_VERIFY_END !macroend +Section "Expired certificate" + SectionIn ${INSTTYPE_MOST} + DetailPrint '=====[ ${__SECTION__} ]===============================' + + !define /redef LINK 'https://expired.badssl.com' + !define /redef FILE '$EXEDIR\_test_expired' + + !define /ifndef X509_V_ERR_CERT_HAS_EXPIRED 10 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' x509 ${X509_V_ERR_CERT_HAS_EXPIRED} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' x509 ${X509_V_ERR_CERT_HAS_EXPIRED} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' http 200 ; SSL validation disabled + + Push /REMOVE + Push "test" + Push /TAG + CallInstDLL $DLL cancel ; no return +SectionEnd + +Section "Wrong host" + SectionIn ${INSTTYPE_MOST} + DetailPrint '=====[ ${__SECTION__} ]===============================' + + !define /redef LINK 'https://wrong.host.badssl.com' + !define /redef FILE '$EXEDIR\_test_wronghost' + + !define /ifndef CURLE_PEER_FAILED_VERIFICATION 60 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' curl ${CURLE_PEER_FAILED_VERIFICATION} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' curl ${CURLE_PEER_FAILED_VERIFICATION} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' http 200 ; SSL validation disabled + + Push /REMOVE + Push "test" + Push /TAG + CallInstDLL $DLL cancel ; no return +SectionEnd + +Section "Untrusted root" + SectionIn ${INSTTYPE_MOST} + DetailPrint '=====[ ${__SECTION__} ]===============================' + + !define /redef LINK 'https://untrusted-root.badssl.com' + !define /redef FILE '$EXEDIR\_test_untrustroot' + + !define /ifndef UNTRUSTED_CERT '7890C8934D5869B25D2F8D0D646F9A5D7385BA85' + !define /ifndef X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN 19 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' x509 ${X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN} + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' ${UNTRUSTED_CERT} http 200 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' x509 ${X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' http 200 ; SSL validation disabled + + Push /REMOVE + Push "test" + Push /TAG + CallInstDLL $DLL cancel ; no return +SectionEnd + Section "Self-signed certificate" SectionIn ${INSTTYPE_MOST} DetailPrint '=====[ ${__SECTION__} ]===============================' !define /redef LINK 'https://self-signed.badssl.com' - !define /redef FILE '$EXEDIR\_testcert' + !define /redef FILE '$EXEDIR\_test_selfsigned' ; Valid to: ‎Sunday, ‎May ‎17, ‎2026 8:59:33 PM ${IfNot} ${FileExists} "$PLUGINSDIR\badssl-selfsigned.crt" File "/oname=$PLUGINSDIR\badssl-selfsigned.crt" "badssl-selfsigned.crt" ${EndIf} - !define /ifndef SELFSIGNED_CERT '9dff24e1dbeec15f90751e7af364d417d65cb8cd' ; `badssl-selfsigned.crt` thumbprint + !define /ifndef SELFSIGNED_CERT '9dff24e1dbeec15f90751e7af364d417d65cb8cd' ; `badssl-selfsigned.crt` thumbprint + + !define /ifndef X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT 18 - !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' 'curl' 0x3c - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' '' '' 'curl' 0x3c - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' '' '' 'curl' 0x3c - !insertmacro CERT_TEST '${LINK}' '${FILE}' '$PLUGINSDIR\badssl-selfsigned.crt' '' '' 'http' 200 + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' '$PLUGINSDIR\badssl-selfsigned.crt' '' '' http 200 - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'true' '' 'curl' 0x3c - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'false' '' 'curl' 0x3c - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'true' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'false' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' 'http' 200 ; SSL validation disabled + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' http 200 ; SSL validation disabled - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '1111111111111111111111111111111111111111' 'curl' 0x3c ; CURLE_PEER_FAILED_VERIFICATION - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '1111111111111111111111111111111111111111' 'curl' 0x23 ; CURLE_SSL_CONNECT_ERROR + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '1111111111111111111111111111111111111111' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '1111111111111111111111111111111111111111' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' ${SELFSIGNED_CERT} 'http' 200 - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' ${SELFSIGNED_CERT} 'http' 200 + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' ${SELFSIGNED_CERT} http 200 + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' ${SELFSIGNED_CERT} http 200 Push /REMOVE Push "test" @@ -1465,5 +1528,5 @@ SectionGroupEnd ; Extra Section -Final - DetailPrint 'Total tests: $g_testCount, Failed tests: $g_testFails' + DetailPrint '[ TESTS ] Total: $g_testCount, Failed: $g_testFails' SectionEnd diff --git a/tests/NScurl-Test.nsi b/tests/NScurl-Test.nsi index 0972df0..dab6333 100644 --- a/tests/NScurl-Test.nsi +++ b/tests/NScurl-Test.nsi @@ -428,7 +428,8 @@ SectionGroup /e "Tests" ${If} `${cert}` == "" StrCpy $R0 '$R0_nocert' ${Else} - StrCpy $R0 '$R0_${cert}' + StrCpy $0 `${cert}` 8 + StrCpy $R0 '$R0_$0' ${EndIf} DetailPrint 'NScurl::http "${url}" "$R0.html"' @@ -474,45 +475,97 @@ SectionGroup /e "Tests" IntOp $g_testCount $g_testCount + 1 ${If} $1 == ${errortype} ${AndIf} $2 = ${errorcode} - DetailPrint "(OK) $1 $2" + DetailPrint "[ OK ] $1/$2" ${Else} IntOp $g_testFails $g_testFails + 1 - DetailPrint "----- FAIL ----- $1 $2 => expected ${errortype} ${errorcode}" + DetailPrint "----- FAIL ----- $1/$2 => expected ${errortype}/${errorcode}" ${EndIf} !macroend +Section "Expired certificate" + SectionIn ${INSTTYPE_MOST} + DetailPrint '=====[ ${__SECTION__} ]===============================' + + !define /redef LINK 'https://expired.badssl.com' + !define /redef FILE '$EXEDIR\_test_expired' + + !define /ifndef X509_V_ERR_CERT_HAS_EXPIRED 10 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' x509 ${X509_V_ERR_CERT_HAS_EXPIRED} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' x509 ${X509_V_ERR_CERT_HAS_EXPIRED} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' http 200 ; SSL validation disabled + + NScurl::cancel /TAG "test" /REMOVE +SectionEnd + +Section "Wrong host" + SectionIn ${INSTTYPE_MOST} + DetailPrint '=====[ ${__SECTION__} ]===============================' + + !define /redef LINK 'https://wrong.host.badssl.com' + !define /redef FILE '$EXEDIR\_test_wronghost' + + !define /ifndef CURLE_PEER_FAILED_VERIFICATION 60 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' curl ${CURLE_PEER_FAILED_VERIFICATION} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' curl ${CURLE_PEER_FAILED_VERIFICATION} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' http 200 ; SSL validation disabled + + NScurl::cancel /TAG "test" /REMOVE +SectionEnd + +Section "Untrusted root" + SectionIn ${INSTTYPE_MOST} + DetailPrint '=====[ ${__SECTION__} ]===============================' + + !define /redef LINK 'https://untrusted-root.badssl.com' + !define /redef FILE '$EXEDIR\_test_untrustroot' + + !define /ifndef UNTRUSTED_CERT '7890C8934D5869B25D2F8D0D646F9A5D7385BA85' + !define /ifndef X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN 19 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' x509 ${X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN} + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' ${UNTRUSTED_CERT} http 200 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' x509 ${X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' http 200 ; SSL validation disabled + + NScurl::cancel /TAG "test" /REMOVE +SectionEnd + Section "Self-signed certificate" SectionIn ${INSTTYPE_MOST} DetailPrint '=====[ ${__SECTION__} ]===============================' !define /redef LINK 'https://self-signed.badssl.com' - !define /redef FILE '$EXEDIR\_testcert' + !define /redef FILE '$EXEDIR\_test_selfsigned' ; Valid to: ‎Sunday, ‎May ‎17, ‎2026 8:59:33 PM ${IfNot} ${FileExists} "$PLUGINSDIR\badssl-selfsigned.crt" File "/oname=$PLUGINSDIR\badssl-selfsigned.crt" "badssl-selfsigned.crt" ${EndIf} - !define /ifndef SELFSIGNED_CERT '9dff24e1dbeec15f90751e7af364d417d65cb8cd' ; `badssl-selfsigned.crt` thumbprint + !define /ifndef SELFSIGNED_CERT '9dff24e1dbeec15f90751e7af364d417d65cb8cd' ; `badssl-selfsigned.crt` thumbprint - !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' 'curl' 0x3c - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' '' '' 'curl' 0x3c - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' '' '' 'curl' 0x3c - !insertmacro CERT_TEST '${LINK}' '${FILE}' '$PLUGINSDIR\badssl-selfsigned.crt' '' '' 'http' 200 + !define /ifndef X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT 18 - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'true' '' 'curl' 0x3c - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'false' '' 'curl' 0x3c - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' 'curl' 0x3c + !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' '$PLUGINSDIR\badssl-selfsigned.crt' '' '' http 200 - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' 'http' 200 ; SSL validation disabled + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'true' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'false' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '1111111111111111111111111111111111111111' 'curl' 0x3c ; CURLE_PEER_FAILED_VERIFICATION - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '1111111111111111111111111111111111111111' 'curl' 0x23 ; CURLE_SSL_CONNECT_ERROR + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '' http 200 ; SSL validation disabled - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' ${SELFSIGNED_CERT} 'http' 200 - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' ${SELFSIGNED_CERT} 'http' 200 + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '1111111111111111111111111111111111111111' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '1111111111111111111111111111111111111111' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} - NScurl::cancel /TAG "test" /REMOVE + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' ${SELFSIGNED_CERT} http 200 + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' ${SELFSIGNED_CERT} http 200 + NScurl::cancel /TAG "test" /REMOVE SectionEnd SectionGroupEnd @@ -907,5 +960,5 @@ SectionGroupEnd ; Extra Section -Final - DetailPrint 'Total tests: $g_testCount, Failed tests: $g_testFails' + DetailPrint '[ TESTS ] Total: $g_testCount, Failed: $g_testFails' SectionEnd From 467b272d435455fafa7aa2c105ea0110d9980c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Negru=C8=9Biu?= Date: Sun, 4 Aug 2024 11:20:42 +0300 Subject: [PATCH 5/8] GH-28: SSL validation errors break the `/INSIST` loop --- src/nscurl/curl.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/nscurl/curl.c b/src/nscurl/curl.c index c1b7fa0..3cd271c 100644 --- a/src/nscurl/curl.c +++ b/src/nscurl/curl.c @@ -1320,6 +1320,8 @@ void CurlTransfer( _In_ PCURL_REQUEST pReq ) break; /// Canceled if (pReq->Error.iHttp > 0 && (pReq->Error.iHttp < 200 || pReq->Error.iHttp >= 300)) break; /// HTTP error + if (pReq->Error.iCurl == CURLE_PEER_FAILED_VERIFICATION || pReq->Error.iX509 != X509_V_OK) + break; /// SSL error // Cancel? if (CurlRequestGetAbortFlag( pReq ) != FALSE) { From fbead20fdc1661e875f5a10a0ac2bef9e1a1db7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Negru=C8=9Biu?= Date: Sun, 4 Aug 2024 11:21:31 +0300 Subject: [PATCH 6/8] GH-28: Extended `/CERT sha1|pem` to accept `pem` option in addition to `sha1` --- src/nscurl/NScurl.readme.md | 43 +++++++++++++++++++++++---- src/nscurl/curl.c | 58 +++++++++++++++++++++++++++++++++---- src/nscurl/curl.h | 4 ++- 3 files changed, 93 insertions(+), 12 deletions(-) diff --git a/src/nscurl/NScurl.readme.md b/src/nscurl/NScurl.readme.md index cb0c134..2e8307c 100644 --- a/src/nscurl/NScurl.readme.md +++ b/src/nscurl/NScurl.readme.md @@ -386,17 +386,50 @@ When enabled, the native CA store is used __in addition__ to the other trusted c ### /CERT ``` -/CERT "sha1 thumbprint" +/CERT sha1|pem ``` -Specify an additional __trusted certificate__ (e.g. `/CERT 917e732d330f9a12404f73d8bea36948b929dffc`). -When specified, trusted certificates are used for SSL certificate validation __in addition__ to other trusted certificate sources ([/CACERT](#cacert) and [/CASTORE](#castore)). -Trusted certificates can reference any certificate in the chain (end-entity cert, intermediate cert, root cert). +Specify additional trusted certificates to be used for SSL certificate validation __in addition__ to other certificate sources ([/CACERT](#cacert) and [/CASTORE](#castore)). Multiple `/CERT` parameters are allowed. -Example: +Parameter | Details +:---------------- | :--------------------- +sha1 | `sha1` certificate thumbprint. The thumbprint can reference any certificate in the chain (the root, intermediate certificates, end-entity certificate) +pem | `pem` blob containing one or more trusted root certificates
NOTE: Limited in size to `${NSIS_MAX_STRLEN}` + +Examples: ```nsis # Certificate pinning (accepts only 1111.. and 2222.. certificates) NScurl::http GET ${url} ${file} /CACERT none /CASTORE false /CERT 1111111111111111111111111111111111111111 /CERT 2222222222222222222222222222222222222222 /END +Pop $0 +``` + +```nsis +; Trust self-signed certificate +!define BADSSL_SELFSIGNED_CRT \ +"-----BEGIN CERTIFICATE-----$\n\ +MIIDeTCCAmGgAwIBAgIJANuSS2L+9oTlMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV$\n\ +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp$\n\ +c2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0y$\n\ +NDA1MTcxNzU5MzNaFw0yNjA1MTcxNzU5MzNaMGIxCzAJBgNVBAYTAlVTMRMwEQYD$\n\ +VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK$\n\ +DAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB$\n\ +BQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2$\n\ +PmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW$\n\ +hyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A$\n\ +xPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve$\n\ +ww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY$\n\ +QCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T$\n\ +BAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI$\n\ +hvcNAQELBQADggEBAH1tiJTqI9nW4Vr3q6joNV7+hNKS2OtgqBxQhMVWWWr4mRDf$\n\ +ayfr4eAJkiHv8/Fvb6WqbGmzClCVNVOrfTzHeLsfROLLmlkYqXSST76XryQR6hyt$\n\ +4qWqGd4M+MUNf7ty3zcVF0Yt2vqHzp4y8m+mE5nSqRarAGvDNJv+I6e4Edw19u1j$\n\ +ddjiqyutdMsJkgvfNvSLQA8u7SAVjnhnoC6n2jm2wdFbrB+9rnrGje+Q8r1ERFyj$\n\ +SG26SdQCiaG5QBCuDhrtLSR1N90URYCY0H6Z57sWcTKEusb95Pz6cBTLGuiNDKJq$\n\ +juBzebaanR+LTh++Bleb9I0HxFFCTwlQhxo/bfY=$\n\ +-----END CERTIFICATE-----" + +NScurl::http GET "https://self-signed.badssl.com" "${file}" /CERT '${BADSSL_SELFSIGNED_CRT}' /END +Pop $0 ``` ### /DEPEND diff --git a/src/nscurl/curl.c b/src/nscurl/curl.c index 3cd271c..040ebe7 100644 --- a/src/nscurl/curl.c +++ b/src/nscurl/curl.c @@ -504,16 +504,25 @@ ULONG CurlParseRequestParam( _In_ ULONG iParamIndex, _In_ LPTSTR pszParam, _In_ } else if (lstrcmpi( pszParam, _T( "/CERT" ) ) == 0) { if (popstring( pszParam ) == NOERROR && *pszParam) { /// Validate SHA1 hash - if (lstrlen( pszParam ) == 40) { + int len = lstrlen(pszParam); + if (len == 40) { int i; for (i = 0; isxdigit( pszParam[i] ); i++); if (i == 40) { + // /CERT sha1 LPSTR psz = MyStrDup( eT2A, pszParam ); if (psz) { pReq->pCertList = curl_slist_append( pReq->pCertList, psz ); MyFree( psz ); } } + } else if (len > 64 && _tcsstr(pszParam, _T("-----BEGIN CERTIFICATE-----"))) { + // /CERT pem_blob + LPSTR psz = MyStrDup(eT2A, pszParam); + if (psz) { + pReq->pPemList = curl_slist_append(pReq->pPemList, psz); + MyFree(psz); + } } } } else if (lstrcmpi( pszParam, _T( "/DEBUG" ) ) == 0) { @@ -633,14 +642,51 @@ int OpenSSLVerifyCallback( int preverify_ok, X509_STORE_CTX *x509_ctx ) } +CURLcode CurlSSLInit(PCURL_REQUEST req, SSL_CTX* sslctx) +{ + // Add all `/CERT ...` certificates to the SSL_CTX store + const struct curl_slist* pem; + for (pem = req->pPemList; pem; pem = pem->next) + { + BIO* bio = BIO_new_mem_buf(pem->data, -1); + if (bio) + { + X509_STORE* store = SSL_CTX_get_cert_store(sslctx); + + // read certificates one by one + X509* cert; + while ((cert = PEM_read_bio_X509(bio, NULL, NULL, NULL)) != NULL) + { + if (X509_STORE_add_cert(store, cert) != 0) + { + // todo: warning + } + X509_free(cert); + } + + BIO_free(bio); + } + } + + return CURLE_OK; +} + + //++ CurlSSLCallback //? This callback function gets called by libcurl just before the initialization of an SSL connection -CURLcode CurlSSLCallback( CURL *curl, void *ssl_ctx, void *userptr ) +CURLcode CurlSSLCallback( CURL *curl, void *ssl_ctx, void * userdata) { - SSL_CTX *pssl = ssl_ctx; - SSL_CTX_set_app_data( pssl, userptr ); - SSL_CTX_set_verify( pssl, SSL_VERIFY_PEER, OpenSSLVerifyCallback); - UNREFERENCED_PARAMETER( curl ); + SSL_CTX *sslctx = ssl_ctx; + + // Import `/CERT pem` certificates + PCURL_REQUEST req = userdata; + CurlSSLInit(req, sslctx); + + // Additional callback to validate `/CERT sha1` certificates + SSL_CTX_set_app_data(sslctx, userdata); + SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, OpenSSLVerifyCallback); + + UNREFERENCED_PARAMETER( curl ); return CURLE_OK; } diff --git a/src/nscurl/curl.h b/src/nscurl/curl.h index 26b62e8..0284990 100644 --- a/src/nscurl/curl.h +++ b/src/nscurl/curl.h @@ -42,7 +42,8 @@ typedef struct _CURL_REQUEST { BOOLEAN bEncoding : 1; BOOLEAN bCastore : 1; /// Use native CA store (CURLSSLOPT_NATIVE_CA) LPCSTR pszCacert; /// can be CACERT_BUILTIN(NULL), CACERT_NONE, or a file path - struct curl_slist *pCertList; /// can be NULL + struct curl_slist *pCertList; /// List of sha1 certificate thumprints. can be NULL + struct curl_slist *pPemList; /// List of pem blobs. can be NULL LPCTSTR pszDebugFile; /// can be NULL ULONG iConnectTimeout; /// can be 0. Connecting timeout ULONG iCompleteTimeout; /// can be 0. Complete (connect + transfer) timeout @@ -122,6 +123,7 @@ static void CurlRequestDestroy( _Inout_ PCURL_REQUEST pReq ) { MyFree( pReq->pszAgent ); MyFree( pReq->pszReferrer ); curl_slist_free_all( pReq->pCertList ); + curl_slist_free_all( pReq->pPemList ); MyFree( pReq->pszCacert ); MyFree( pReq->pszDebugFile ); MyFree( pReq->pszTag ); From b48a788f9636731504eb27a62ebe07d08583a30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Negru=C8=9Biu?= Date: Sun, 4 Aug 2024 11:22:52 +0300 Subject: [PATCH 7/8] GH-28: Certificate tests independent of external .crt file --- tests/NScurl-Debug.nsi | 44 +++++++++++++++++++++++++++++-------- tests/NScurl-Test.nsi | 44 +++++++++++++++++++++++++++++-------- tests/badssl-selfsigned.crt | 21 ------------------ 3 files changed, 70 insertions(+), 39 deletions(-) delete mode 100644 tests/badssl-selfsigned.crt diff --git a/tests/NScurl-Debug.nsi b/tests/NScurl-Debug.nsi index 61b37ca..2c358bf 100644 --- a/tests/NScurl-Debug.nsi +++ b/tests/NScurl-Debug.nsi @@ -665,6 +665,33 @@ SectionEnd SectionGroup /e "Tests" +; Valid to: ‎Sunday, ‎May ‎17, ‎2026 8:59:33 PM +!define BADSSL_SELFSIGNED_CRT \ +"-----BEGIN CERTIFICATE-----$\n\ +MIIDeTCCAmGgAwIBAgIJANuSS2L+9oTlMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV$\n\ +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp$\n\ +c2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0y$\n\ +NDA1MTcxNzU5MzNaFw0yNjA1MTcxNzU5MzNaMGIxCzAJBgNVBAYTAlVTMRMwEQYD$\n\ +VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK$\n\ +DAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB$\n\ +BQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2$\n\ +PmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW$\n\ +hyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A$\n\ +xPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve$\n\ +ww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY$\n\ +QCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T$\n\ +BAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI$\n\ +hvcNAQELBQADggEBAH1tiJTqI9nW4Vr3q6joNV7+hNKS2OtgqBxQhMVWWWr4mRDf$\n\ +ayfr4eAJkiHv8/Fvb6WqbGmzClCVNVOrfTzHeLsfROLLmlkYqXSST76XryQR6hyt$\n\ +4qWqGd4M+MUNf7ty3zcVF0Yt2vqHzp4y8m+mE5nSqRarAGvDNJv+I6e4Edw19u1j$\n\ +ddjiqyutdMsJkgvfNvSLQA8u7SAVjnhnoC6n2jm2wdFbrB+9rnrGje+Q8r1ERFyj$\n\ +SG26SdQCiaG5QBCuDhrtLSR1N90URYCY0H6Z57sWcTKEusb95Pz6cBTLGuiNDKJq$\n\ +juBzebaanR+LTh++Bleb9I0HxFFCTwlQhxo/bfY=$\n\ +-----END CERTIFICATE-----" + +!define BADSSL_SELFSIGNED_THUMBPRINT '9dff24e1dbeec15f90751e7af364d417d65cb8cd' + + !macro CERT_TEST url file cacert castore cert errortype errorcode StrCpy $R0 '${file}' ${If} `${cacert}` == "" @@ -702,6 +729,10 @@ SectionGroup /e "Tests" Push `${cert}` Push /CERT ${EndIf} + Push /CANCEL + Push /INSIST + Push 60s + Push /TIMEOUT ; badssl.com can be laggy sometimes Push "$R0.debug.txt" Push "nodata" Push /DEBUG @@ -814,18 +845,13 @@ Section "Self-signed certificate" !define /redef LINK 'https://self-signed.badssl.com' !define /redef FILE '$EXEDIR\_test_selfsigned' - ; Valid to: ‎Sunday, ‎May ‎17, ‎2026 8:59:33 PM - ${IfNot} ${FileExists} "$PLUGINSDIR\badssl-selfsigned.crt" - File "/oname=$PLUGINSDIR\badssl-selfsigned.crt" "badssl-selfsigned.crt" - ${EndIf} - !define /ifndef SELFSIGNED_CERT '9dff24e1dbeec15f90751e7af364d417d65cb8cd' ; `badssl-selfsigned.crt` thumbprint - !define /ifndef X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT 18 !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} - !insertmacro CERT_TEST '${LINK}' '${FILE}' '$PLUGINSDIR\badssl-selfsigned.crt' '' '' http 200 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '${BADSSL_SELFSIGNED_CRT}' http 200 !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'true' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'false' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} @@ -836,8 +862,8 @@ Section "Self-signed certificate" !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '1111111111111111111111111111111111111111' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '1111111111111111111111111111111111111111' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' ${SELFSIGNED_CERT} http 200 - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' ${SELFSIGNED_CERT} http 200 + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' ${BADSSL_SELFSIGNED_THUMBPRINT} http 200 + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' ${BADSSL_SELFSIGNED_THUMBPRINT} http 200 Push /REMOVE Push "test" diff --git a/tests/NScurl-Test.nsi b/tests/NScurl-Test.nsi index dab6333..eac57f5 100644 --- a/tests/NScurl-Test.nsi +++ b/tests/NScurl-Test.nsi @@ -410,6 +410,33 @@ SectionEnd SectionGroup /e "Tests" +; Valid to: ‎Sunday, ‎May ‎17, ‎2026 8:59:33 PM +!define BADSSL_SELFSIGNED_CRT \ +"-----BEGIN CERTIFICATE-----$\n\ +MIIDeTCCAmGgAwIBAgIJANuSS2L+9oTlMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV$\n\ +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp$\n\ +c2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0y$\n\ +NDA1MTcxNzU5MzNaFw0yNjA1MTcxNzU5MzNaMGIxCzAJBgNVBAYTAlVTMRMwEQYD$\n\ +VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK$\n\ +DAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB$\n\ +BQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2$\n\ +PmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW$\n\ +hyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A$\n\ +xPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve$\n\ +ww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY$\n\ +QCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T$\n\ +BAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI$\n\ +hvcNAQELBQADggEBAH1tiJTqI9nW4Vr3q6joNV7+hNKS2OtgqBxQhMVWWWr4mRDf$\n\ +ayfr4eAJkiHv8/Fvb6WqbGmzClCVNVOrfTzHeLsfROLLmlkYqXSST76XryQR6hyt$\n\ +4qWqGd4M+MUNf7ty3zcVF0Yt2vqHzp4y8m+mE5nSqRarAGvDNJv+I6e4Edw19u1j$\n\ +ddjiqyutdMsJkgvfNvSLQA8u7SAVjnhnoC6n2jm2wdFbrB+9rnrGje+Q8r1ERFyj$\n\ +SG26SdQCiaG5QBCuDhrtLSR1N90URYCY0H6Z57sWcTKEusb95Pz6cBTLGuiNDKJq$\n\ +juBzebaanR+LTh++Bleb9I0HxFFCTwlQhxo/bfY=$\n\ +-----END CERTIFICATE-----" + +!define BADSSL_SELFSIGNED_THUMBPRINT '9dff24e1dbeec15f90751e7af364d417d65cb8cd' + + !macro CERT_TEST url file cacert castore cert errortype errorcode StrCpy $R0 '${file}' ${If} `${cacert}` == "" @@ -447,6 +474,10 @@ SectionGroup /e "Tests" Push `${cert}` Push /CERT ${EndIf} + Push /CANCEL + Push /INSIST + Push 60s + Push /TIMEOUT ; badssl.com can be laggy sometimes Push "$R0.debug.txt" Push "nodata" Push /DEBUG @@ -540,18 +571,13 @@ Section "Self-signed certificate" !define /redef LINK 'https://self-signed.badssl.com' !define /redef FILE '$EXEDIR\_test_selfsigned' - ; Valid to: ‎Sunday, ‎May ‎17, ‎2026 8:59:33 PM - ${IfNot} ${FileExists} "$PLUGINSDIR\badssl-selfsigned.crt" - File "/oname=$PLUGINSDIR\badssl-selfsigned.crt" "badssl-selfsigned.crt" - ${EndIf} - !define /ifndef SELFSIGNED_CERT '9dff24e1dbeec15f90751e7af364d417d65cb8cd' ; `badssl-selfsigned.crt` thumbprint - !define /ifndef X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT 18 !insertmacro CERT_TEST '${LINK}' '${FILE}' '' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' '' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} - !insertmacro CERT_TEST '${LINK}' '${FILE}' '$PLUGINSDIR\badssl-selfsigned.crt' '' '' http 200 + + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '${BADSSL_SELFSIGNED_CRT}' http 200 !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'true' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} !insertmacro CERT_TEST '${LINK}' '${FILE}' 'builtin' 'false' '' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} @@ -562,8 +588,8 @@ Section "Self-signed certificate" !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' '1111111111111111111111111111111111111111' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' '1111111111111111111111111111111111111111' x509 ${X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT} - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' ${SELFSIGNED_CERT} http 200 - !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' ${SELFSIGNED_CERT} http 200 + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'true' ${BADSSL_SELFSIGNED_THUMBPRINT} http 200 + !insertmacro CERT_TEST '${LINK}' '${FILE}' 'none' 'false' ${BADSSL_SELFSIGNED_THUMBPRINT} http 200 NScurl::cancel /TAG "test" /REMOVE SectionEnd diff --git a/tests/badssl-selfsigned.crt b/tests/badssl-selfsigned.crt deleted file mode 100644 index 45099f6..0000000 --- a/tests/badssl-selfsigned.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDeTCCAmGgAwIBAgIJANuSS2L+9oTlMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV -BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp -c2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0y -NDA1MTcxNzU5MzNaFw0yNjA1MTcxNzU5MzNaMGIxCzAJBgNVBAYTAlVTMRMwEQYD -VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK -DAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2 -PmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW -hyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A -xPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve -ww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY -QCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T -BAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI -hvcNAQELBQADggEBAH1tiJTqI9nW4Vr3q6joNV7+hNKS2OtgqBxQhMVWWWr4mRDf -ayfr4eAJkiHv8/Fvb6WqbGmzClCVNVOrfTzHeLsfROLLmlkYqXSST76XryQR6hyt -4qWqGd4M+MUNf7ty3zcVF0Yt2vqHzp4y8m+mE5nSqRarAGvDNJv+I6e4Edw19u1j -ddjiqyutdMsJkgvfNvSLQA8u7SAVjnhnoC6n2jm2wdFbrB+9rnrGje+Q8r1ERFyj -SG26SdQCiaG5QBCuDhrtLSR1N90URYCY0H6Z57sWcTKEusb95Pz6cBTLGuiNDKJq -juBzebaanR+LTh++Bleb9I0HxFFCTwlQhxo/bfY= ------END CERTIFICATE----- From 93e512f94e4635a2c41aee386fd7ba2ddc99ed7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Negru=C8=9Biu?= Date: Sun, 4 Aug 2024 22:37:08 +0300 Subject: [PATCH 8/8] Minor test improvement --- tests/NScurl-Debug.nsi | 7 +++++-- tests/NScurl-Test.nsi | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/NScurl-Debug.nsi b/tests/NScurl-Debug.nsi index 2c358bf..1090950 100644 --- a/tests/NScurl-Debug.nsi +++ b/tests/NScurl-Debug.nsi @@ -25,6 +25,7 @@ Var /global DLL !include "Sections.nsh" !include "FileFunc.nsh" +!insertmacro GetFileName !insertmacro GetOptions !insertmacro GetParameters @@ -714,7 +715,9 @@ juBzebaanR+LTh++Bleb9I0HxFFCTwlQhxo/bfY=$\n\ StrCpy $R0 '$R0_$0' ${EndIf} - DetailPrint 'NScurl::http "${url}" "$R0.html"' + ${GetFileName} $R0 $0 + DetailPrint 'NScurl::http "${url}" "$0"' + !insertmacro STACK_VERIFY_START Push "/END" ${If} `${cacert}` != "" @@ -740,7 +743,7 @@ juBzebaanR+LTh++Bleb9I0HxFFCTwlQhxo/bfY=$\n\ Push /TAG Push "@ID@" Push /RETURN - Push "$R0.html" + Push memory Push "${url}" Push "GET" CallInstDLL $DLL http diff --git a/tests/NScurl-Test.nsi b/tests/NScurl-Test.nsi index eac57f5..bd769ff 100644 --- a/tests/NScurl-Test.nsi +++ b/tests/NScurl-Test.nsi @@ -20,6 +20,9 @@ !include "LogicLib.nsh" !include "Sections.nsh" +!include "FileFunc.nsh" +!insertmacro GetFileName + !define /ifndef NULL 0 !define TEST_FILE "$SYSDIR\lz32.dll" ; ...random file that exists in every Windows build @@ -459,7 +462,8 @@ juBzebaanR+LTh++Bleb9I0HxFFCTwlQhxo/bfY=$\n\ StrCpy $R0 '$R0_$0' ${EndIf} - DetailPrint 'NScurl::http "${url}" "$R0.html"' + ${GetFileName} $R0 $0 + DetailPrint 'NScurl::http "${url}" "$0"' Push "/END" ${If} `${cacert}` != "" @@ -485,7 +489,7 @@ juBzebaanR+LTh++Bleb9I0HxFFCTwlQhxo/bfY=$\n\ Push /TAG Push "@ID@" Push /RETURN - Push "$R0.html" + Push memory Push "${url}" Push "GET" CallInstDLL "$PLUGINSDIR\NScurl.dll" http