diff --git a/.github/workflows/Notifications.yml b/.github/workflows/Notifications.yml new file mode 100644 index 00000000..bd385304 --- /dev/null +++ b/.github/workflows/Notifications.yml @@ -0,0 +1,70 @@ +name: TaurusTLS GitHub Notifications + +on: + pull_request: + types: [opened, reopened, closed, review_requested] + branches: + - main + issues: + types: [opened, reopened, closed] + issue_comment: + types: [created] + +jobs: + notify-telegram: + runs-on: ubuntu-latest + # Skip job if it's a comment on a PR (GitHub treats PRs as issues for comments) + if: ${{ github.event_name != 'issue_comment' || !github.event.issue.pull_request }} + + steps: + - name: Set Event Details + id: event-info + run: | + if [ "${{ github.event_name }}" == "pull_request" ]; then + echo "TITLE=Pull Request ${{ github.event.action }}" >> $GITHUB_OUTPUT + echo "SUBJECT=${{ github.event.pull_request.title }}" >> $GITHUB_OUTPUT + echo "URL=${{ github.event.pull_request.html_url }}" >> $GITHUB_OUTPUT + echo "AUTHOR=${{ github.event.pull_request.user.login }}" >> $GITHUB_OUTPUT + + elif [ "${{ github.event_name }}" == "issues" ]; then + echo "TITLE=Issue ${{ github.event.action }}" >> $GITHUB_OUTPUT + echo "SUBJECT=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT + echo "URL=${{ github.event.issue.html_url }}" >> $GITHUB_OUTPUT + echo "AUTHOR=${{ github.event.issue.user.login }}" >> $GITHUB_OUTPUT + + elif [ "${{ github.event_name }}" == "issue_comment" ]; then + echo "TITLE=New Comment on Issue #${{ github.event.issue.number }}" >> $GITHUB_OUTPUT + echo "SUBJECT=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT + echo "URL=${{ github.event.comment.html_url }}" >> $GITHUB_OUTPUT + echo "AUTHOR=${{ github.event.comment.user.login }}" >> $GITHUB_OUTPUT + + elif [ "${{ github.event_name }}" == "workflow_run" ]; then + echo "TITLE=CI Build Failed" >> $GITHUB_OUTPUT + echo "SUBJECT=${{ github.repository }}" >> $GITHUB_OUTPUT + echo "URL=${{ github.event.workflow_run.html_url }}" >> $GITHUB_OUTPUT + echo "AUTHOR=GitHub Actions" >> $GITHUB_OUTPUT + + else + echo "TITLE=Event Triggered: ${{ github.event_name }}" >> $GITHUB_OUTPUT + echo "SUBJECT=${{ github.repository }}" >> $GITHUB_OUTPUT + echo "URL=https://github.com/${{ github.repository }}" >> $GITHUB_OUTPUT + echo "AUTHOR=${{ github.actor }}" >> $GITHUB_OUTPUT + fi + + - name: Send Telegram Notification + uses: appleboy/telegram-action@master + with: + to: ${{ secrets.TLG_CHAT_ID_TAURUSTLS_ALERTS }} + token: ${{ secrets.TLG_BOT_TKN_TAURUSTLSDEV_BOT }} + message_thread_id: ${{ vars.TLG_THRD_ID_TAURUSTLS_ALERTS }} + disable_web_page_preview: true + format: markdown + message: | + 🛠️ *TaurusTLS GitHub Notification* + + *Event:* ${{ steps.event-info.outputs.TITLE }} + *Subject:* ${{ steps.event-info.outputs.SUBJECT }} + *Repository:* `TaurusTLS/${{ github.event.repository.name }}` + *Triggered by:* @${{ steps.event-info.outputs.AUTHOR }} + + [🔗 View on GitHub](${{ steps.event-info.outputs.URL }}) diff --git a/Source/TaurusTLS_Utils.pas b/Source/TaurusTLS_Utils.pas index df42c1b2..503f7279 100644 --- a/Source/TaurusTLS_Utils.pas +++ b/Source/TaurusTLS_Utils.pas @@ -145,12 +145,6 @@ function MDAsString(const AMD: TTaurusTLSLEVP_MD): String; {$IFDEF USE_INLINE} /// /// Returns the second of the minute. /// -/// -/// Returns the Time Zone offset hour -/// -/// -/// Returns the Time Zone offset minute. -/// /// /// True if parsing was successful or False if failed. /// @@ -260,6 +254,7 @@ implementation {$IFDEF WINDOWS} IdIDN, {$ENDIF} + DateUtils, TaurusTLS_ResourceStrings, TaurusTLSHeaders_bio, TaurusTLSHeaders_objects, TaurusTLSHeaders_x509, TaurusTLSHeaders_x509_vfy, SysUtils; @@ -555,106 +550,190 @@ function ASN1_OBJECT_ToStr(a: PASN1_OBJECT): String; end; end; +// Inline helper function for `ASN1_Time_Decode` +function CharArrayToWord(AChars: PIdAnsiChar; AStart, AEnd: TIdC_INT; + out AResult: Word): Boolean; {$IFDEF USE_INLINE}inline; {$ENDIF} +var + i: TIdC_INT; + lChar: AnsiChar; + lResult: Word; +begin + Result := Assigned(AChars) and (AStart >= 0) and (AEnd >= AStart); + if Result then + begin + lResult := 0; + for i := AStart to AEnd do + begin + lChar := AChars[i]; + if lChar in ['0'..'9'] then + lResult := (lResult * 10) + (Byte(lChar) - Byte('0')) + else + Exit(False); + end; + AResult := lResult; + end; +end; + +// Note: ASN1TimeToDateTime was previously the sole consumer of this function. +// While ASN1TimeToDateTime has been refactored to use native OpenSSL routines +// and ASN1_Time_Decode is no longer used internally within TaurusTLS, this function +// remains part of the public API contract and must be preserved for compatibility. +// +// The legacy implementation relied on multiple temporary string allocations and +// lacked adequate input validation for malformed time strings, making it inefficient +// and prone to unexpected failures. +// +// The refactored version below provides a highly optimized, low-footprint parser +// that extracts raw ASN.1 time components into local time values safely. function ASN1_Time_Decode(const a: PASN1_TIME; out year, month, day, hour, min, sec: Word; out tz_hour, tz_min: Integer): Boolean; var - i, tz_dir: Integer; -{$IFDEF FPC} - time_str: AnsiString; -{$ELSE} - time_str: string; -{$ENDIF} -{$IFNDEF USE_MARSHALLED_PTRS} -{$IFNDEF STRING_IS_ANSI} - LTemp: AnsiString; -{$ENDIF} -{$ENDIF} + lStr: PASN1_STRING; + lTime: PIdAnsiChar; + lLen: TIdC_INT; + lIdx: TIdC_INT; + lIsGeneralized: Boolean; + lRawYear: Word; + lSignIdx: TIdC_INT; + lPositiveTZ: Boolean; + lTZH, lTZM: Word; + begin - year := 0; - month := 1; - day := 1; - hour := 0; - min := 0; - sec := 0; - tz_hour := 0; - tz_min := 0; - Result := False; { default is to return with an error indication } - if ASN1_STRING_length(PASN1_STRING(a)) < 12 then //PALOFF - Possible bad typecast + year := 0; month := 0; day := 0; + hour := 0; min := 0; sec := 0; + tz_hour := 0; tz_min := 0; + + lStr := PASN1_STRING(a); + if not Assigned(lStr) then + Exit(False); + + lLen := ASN1_STRING_length(lStr); + lTime := PIdAnsiChar(ASN1_STRING_get0_data(lStr)); + + if not Assigned(lTime) or (lLen < 11) then + Exit(False); + + // Determine if UTCTime (2-digit year) or GeneralizedTime (4-digit year) + // ASN1_STRING_type(lStr) can be checked, or infer from string length/format. + lIsGeneralized := (ASN1_STRING_type(lStr) = V_ASN1_GENERALIZEDTIME) or + ((lLen >= 15) and (lTime[14] in ['Z', 'z', '+', '-', '.', ','])); + + lIdx := 0; + + // 1. Parse Year + if lIsGeneralized then begin - Exit; + if not CharArrayToWord(lTime, lIdx, lIdx + 3, year) then Exit(False); + Inc(lIdx, 4); + end + else + begin + if not CharArrayToWord(lTime, lIdx, lIdx + 1, lRawYear) then Exit(False); + // UTCTime sliding window rule (RFC 5280): YY >= 50 -> 19YY, YY < 50 -> 20YY + if lRawYear >= 50 then + year := 1900 + lRawYear + else + year := 2000 + lRawYear; + Inc(lIdx, 2); end; -{$IFDEF USE_MARSHALLED_PTRS} - time_str := TMarshal.ReadStringAsAnsi(TPtrWrapper.Create(ASN1_STRING_get0_data(PASN1_STRING(a))), - ASN1_STRING_length(PASN1_STRING(a))); -{$ELSE} -{$IFDEF STRING_IS_ANSI} - SetString(time_str, PIdAnsiChar(ASN1_STRING_get0_data(PASN1_STRING(a))), ASN1_STRING_length(PASN1_STRING(a))); -{$ELSE} - SetString(LTemp, PIdAnsiChar(ASN1_STRING_get0_data(PASN1_STRING(a))), ASN1_STRING_length(PASN1_STRING(a))); //PALOFF - Possible bad pointer usage [data : PByte cast to PAnsiChar] - { Note: UTCtime is a type defined by OpenSSL and hence is ansistring and not UCS-2 } - // TODO: do we need to use SetCodePage() here? - time_str := String(LTemp); // explicit convert to Unicode -{$ENDIF} -{$ENDIF} - // Check if first 12 chars are numbers - if not IsNumeric(time_str, 12) then + + // 2. Parse Month, Day, Hour, Minute + if not CharArrayToWord(lTime, lIdx, lIdx + 1, month) then Exit(False); + if not CharArrayToWord(lTime, lIdx + 2, lIdx + 3, day) then Exit(False); + if not CharArrayToWord(lTime, lIdx + 4, lIdx + 5, hour) then Exit(False); + if not CharArrayToWord(lTime, lIdx + 6, lIdx + 7, min) then Exit(False); + Inc(lIdx, 8); + + // 3. Parse Seconds (optional in some BER structures, standard in DER) + if (lIdx + 1 < lLen) and (lTime[lIdx] in ['0'..'9']) then begin - Exit; + if not CharArrayToWord(lTime, lIdx, lIdx + 1, sec) then Exit(False); + Inc(lIdx, 2); + end + else + sec := 0; + + // 4. Skip Fractional Seconds (.sss or ,sss if present in GeneralizedTime) + if (lIdx < lLen) and (lTime[lIdx] in ['.', ',']) then + begin + Inc(lIdx); + while (lIdx < lLen) and (lTime[lIdx] in ['0'..'9']) do + Inc(lIdx); end; - // Convert time from string to number - year := IndyStrToInt(Copy(time_str, 1, 2)) + 1900; - month := IndyStrToInt(Copy(time_str, 3, 2)); - day := IndyStrToInt(Copy(time_str, 5, 2)); - hour := IndyStrToInt(Copy(time_str, 7, 2)); - min := IndyStrToInt(Copy(time_str, 9, 2)); - sec := IndyStrToInt(Copy(time_str, 11, 2)); - // Fix year. This function is Y2k but isn't compatible with Y2k5 :-( {Do not Localize} - if year < 1950 then + + // 5. Parse Timezone Portion + if (lIdx >= lLen) or (lTime[lIdx] in ['Z', 'z']) then begin - Inc(year, 100); + // UTC / Zulu time + tz_hour := 0; + tz_min := 0; + Exit(True); end; - // Check TZ - tz_hour := 0; - tz_min := 0; - if CharIsInSet(time_str, 13, '-+') then - begin { Do not Localize } - tz_dir := iif(CharEquals(time_str, 13, '-'), -1, 1); { Do not Localize } - for i := 14 to 18 do - begin // Check if numbers are numbers - if i = 16 then - begin - Continue; - end; - if not IsNumeric(time_str[i]) then - begin - Exit; - end; - end; - tz_hour := IndyStrToInt(Copy(time_str, 14, 15)) * tz_dir; - tz_min := IndyStrToInt(Copy(time_str, 17, 18)) * tz_dir; + + // Scan for offset sign '+' or '-' + lSignIdx := lIdx; + while (lSignIdx < lLen) and not (lTime[lSignIdx] in ['+', '-']) do + Inc(lSignIdx); + + if lSignIdx >= lLen then + Exit(True); // Implicit local time or no explicit offset provided + + lPositiveTZ := (lTime[lSignIdx] = '+'); + + // Parse TZ Hours (2 digits) + if not CharArrayToWord(lTime, lSignIdx + 1, lSignIdx + 2, lTZH) then + Exit(False); + + // Parse TZ Minutes (2 digits, if available) + if (lSignIdx + 4 < lLen) and (lTime[lSignIdx + 3] in ['0'..'9']) then + begin + if not CharArrayToWord(lTime, lSignIdx + 3, lSignIdx + 4, lTZM) then + Exit(False); + end + else + lTZM := 0; + + if lPositiveTZ then + begin + tz_hour := lTZH; + tz_min := lTZM; + end + else + begin + tz_hour := -Integer(lTZH); + tz_min := -Integer(lTZM); end; - Result := True; { everthing OK } + + Result := True; end; function ASN1TimeToDateTime(a: PASN1_TIME): TDateTime; var - year: Word; - month: Word; - day: Word; - hour: Word; - min: Word; - sec: Word; - tz_h: Integer; - tz_m: Integer; + lTm: TIdC_TM; + lUtcDT: TDateTime; + lSec: Word; + begin Result := 0; - if ASN1_Time_Decode(a, year, month, day, hour, min, sec, tz_h, tz_m) then + if not Assigned(a) then + Exit; + + {$IFDEF DCC}{$WARN UNSAFE_CODE OFF}{$ENDIF} + if ASN1_TIME_to_tm(a, @lTm) = 1 then begin - Result := EncodeDate(year, month, day) + EncodeTime(hour, min, sec, 0); - Result := Result + (tz_m / (60 * 24)); - Result := Result + (tz_h / 24.0); - Result := UTCTimeToLocalTime(Result); + // Handle leap seconds safely (clamp sec = 60 to 59) + lSec := lTm.tm_sec; + if lSec > 59 then + lSec := 59; + + // Safely encode UTC DateTime without throwing exceptions on edge cases + if TryEncodeDateTime(lTm.tm_year + 1900, lTm.tm_mon + 1, lTm.tm_mday, + lTm.tm_hour, lTm.tm_min, lSec, 0, lUtcDT) then + begin + Result := UTCTimeToLocalTime(lUtcDT); + end; end; + {$IFDEF DCC}{$WARN UNSAFE_CODE DEFAULT}{$ENDIF} end; function DirName(const ADirName: PX509_NAME): String; diff --git a/Tests/TaurusTLS.UT.Headers.Bio.pas b/Tests/TaurusTLS.UT.Headers.Bio.pas index 26262547..df7cf0a2 100644 --- a/Tests/TaurusTLS.UT.Headers.Bio.pas +++ b/Tests/TaurusTLS.UT.Headers.Bio.pas @@ -178,21 +178,11 @@ procedure TBioReadWriteFixture.Test_BIO_new_mem_buf(const AValue: RawByteString) end; end; -type - // https://github.com/openssl/openssl/blob/bde55d421b1f49e31248c240efe50ff1f0d24141/include/openssl/buffer.h#L42 - PBUF_MEMM = ^TBUF_MEMM; - TBUF_MEMM = record - length: TIdC_SIZET; - data: PIdAnsiChar; - max: TIdC_SIZET; - flags: TIdC_UINT; - end; - procedure TBioReadWriteFixture.Test_BIO_new_mem_ref(const AValue: RawByteString); var lBio: PBIO; - lLen: TIdC_INT; - lRef: PBUF_MEM; + lLen, lBioLen: TIdC_INT; + lBioData: pointer; begin lBio:=nil; @@ -201,11 +191,11 @@ procedure TBioReadWriteFixture.Test_BIO_new_mem_ref(const AValue: RawByteString) try lBio:=BIO_new_mem_buf(AValue[1], lLen); Assert.IsNotNull(lBio, 'lBio is ''nil'''); - Assert.AreEqual(1, BIO_get_mem_ptr(lBio, lRef), 'BIO_get_mem_ptr'); - Assert.AreEqual(lLen, PBUF_MEMM(lRef)^.length, - '@AValue[1] <> PBUF_MEMM(lRef)^.length'); - Assert.AreEqual(@AValue[1], PBUF_MEMM(lRef)^.data, - '@AValue[1] <> PBUF_MEMM(lRef)^.data'); + lBioLen:=BIO_get_mem_data(lBio, lBioData); + Assert.AreEqual(lLen, lBioLen, + 'BIO Lengths and Original Data Lengths are not equal.'); + Assert.AreEqual(@AValue[1], lBioData, + 'BIO does not refer Origonal data'); finally BIO_free(lBio); end; diff --git a/Tests/TaurusTLS.UT.Utils.pas b/Tests/TaurusTLS.UT.Utils.pas index 8e0556a5..e288d06e 100644 --- a/Tests/TaurusTLS.UT.Utils.pas +++ b/Tests/TaurusTLS.UT.Utils.pas @@ -27,22 +27,35 @@ interface /// TestFixtures derived from TOsslLoader = class + {$IFNDEF OPENSSL_STATIC_LINK_MODEL} public const - cEnvVarName = 'OPENSSL_PATH'; - cShortOptName = 'osp'; - cLongOptName = 'opensslpath'; + cEnvVarPathName = 'OPENSSL_PATH'; + cShortOptPathName = 'osp'; + cLongOptPathName = 'opensslpath'; + + cEnvVarModulesPathName = 'OPENSSL_MODULES'; + cShortOptModulesPathName = 'osmp'; + cLongOptModulesPathName = 'opensslmodulespath'; private class var FLoader: IOpenSSLLoader; FLoadCount: FixedUInt; + FLock: TObject; private + class procedure Lock; {$IFDEF USE_INLINE}inline; {$ENDIF} + class procedure Unlock; {$IFDEF USE_INLINE}inline; {$ENDIF} class procedure RegisterOptions; static; class function GetFromEnvVar: string; static; class procedure SetPath(const Value: string); static; class function GetPath: string; static; + class function GetProvidersPath: string; static; + class procedure SetProvidersPath(const Value: string); static; + {$ENDIF} class function GetLoaded: boolean; static; public + {$IFNDEF OPENSSL_STATIC_LINK_MODEL} class constructor Create; class destructor Destroy; + {$ENDIF} /// /// Tries to load Openssl library. Can be called multiple times. /// @@ -68,6 +81,7 @@ TOsslLoader = class /// class property Loaded: boolean read GetLoaded; + {$IFNDEF OPENSSL_STATIC_LINK_MODEL} /// /// Allows to change path to the Openssl library folder /// @@ -75,6 +89,15 @@ TOsslLoader = class /// /// class property Path: string read GetPath write SetPath; + /// + /// Allows to change path to the Openssl module provider libraries folder + /// + /// Path can be changed if Openssl library unloaded, otherwise exception is rised. + /// + /// + class property ProvidersPath: string read GetProvidersPath + write SetProvidersPath; + {$ENDIF} end; {$IFDEF USE_FASTMM5} @@ -110,17 +133,18 @@ TFastMMDebugLog = class {$ENDIF} resourcestring - rcOssLoaderHelp = 'Specify path to OpenSSL library folder'; + rcOssLoaderPathHelp = 'Specify path to OpenSSL library folder'; + rcOssLoaderModulesPathHelp = 'Specify path to OpenSSL module provider libraries'' folder'; rcFastMMDebugEnableHelp = 'Enable or disable detailed memory leak'; rcFastMMLogHelp = 'Memory leak report file name.'; -{$IFDEF USE_FASTMM5} - {$IFDEF WIN32} + {$IFDEF USE_FASTMM5} + {$IFDEF WIN32} rcFastMMDllNamelHelp = 'Path to FastMM_FullDebugMode.dll'; - {$ENDIF} - {$IFDEF WIN64} + {$ENDIF} + {$IFDEF WIN64} rcFastMMDllNamelHelp = 'Path to FastMM_FullDebugMode64.dll'; + {$ENDIF} {$ENDIF} -{$ENDIF} implementation @@ -132,8 +156,10 @@ implementation { TOsslLoader } +{$IFNDEF OPENSSL_STATIC_LINK_MODEL} class constructor TOsslLoader.Create; begin + FLock:=TObject.Create; FLoader:=GetOpenSSLLoader; Path:=GetFromEnvVar; RegisterOptions; @@ -142,60 +168,41 @@ implementation class destructor TOsslLoader.Destroy; begin Unload; + FreeAndNil(FLock); end; -class function TOsslLoader.GetFromEnvVar: string; -begin - Result:=GetEnvironmentVariable(cEnvVarName); -end; - -class function TOsslLoader.GetLoaded: boolean; +class procedure TOsslLoader.Lock; begin - Result:=FLoadCount > 0; + TMonitor.Enter(FLock); end; -class function TOsslLoader.Load: boolean; +class procedure TOsslLoader.Unlock; begin - Result:=Loaded; - if Result then - begin - TInterlocked.Increment(FLoadCount); - Exit(True); - end; - FLoader:=GetOpenSSLLoader; - Result:=FLoader.Load; - if Result then - TInterlocked.Increment(FLoadCount); + TMonitor.Exit(FLock); end; -class procedure TOsslLoader.Unload; -var - lLoadCount: cardinal; - lSpinW: TSpinWait; - +class function TOsslLoader.GetFromEnvVar: string; begin - lSpinW.Reset; - repeat - lLoadCount:=FLoadCount; - if lLoadCount = 0 then - break; - if TInterlocked.CompareExchange(FLoadCount, lLoadCount-1, lLoadCount) = lLoadCount then - break; - lSpinW.SpinCycle; - until False; - if lLoadCount > 0 then - FLoader.Unload; + Result:=GetEnvironmentVariable(cEnvVarPathName); end; class procedure TOsslLoader.RegisterOptions; begin - TOptionsRegistry.RegisterOption(cLongOptName, cShortOptName, - rcOssLoaderHelp, + TOptionsRegistry.RegisterOption(cLongOptPathName, cShortOptPathName, + rcOssLoaderPathHelp, procedure(Value: string) begin Path:=Value; end ); + + TOptionsRegistry.RegisterOption(cLongOptModulesPathName, + cShortOptModulesPathName, rcOssLoaderModulesPathHelp, + procedure(Value: string) + begin + ProvidersPath:=Value; + end + ); end; class function TOsslLoader.GetPath: string; @@ -203,11 +210,79 @@ class function TOsslLoader.GetPath: string; Result:=FLoader.OpenSSLPath; end; +class function TOsslLoader.GetProvidersPath: string; +begin + Result:=FLoader.ProvidersPath; +end; + class procedure TOsslLoader.SetPath(const Value: string); begin FLoader.OpenSSLPath:=Value; end; +class procedure TOsslLoader.SetProvidersPath(const Value: string); +begin + FLoader.ProvidersPath:=Value; +end; +{$ENDIF} + +class function TOsslLoader.GetLoaded: boolean; +begin + {$IFDEF OPENSSL_STATIC_LINK_MODEL} + Result:=True; + {$ELSE} + Result:=FLoadCount > 0; + {$ENDIF} +end; + +class function TOsslLoader.Load: boolean; +begin + {$IFDEF OPENSSL_STATIC_LINK_MODEL} + Result:=True; + {$ELSE} + Lock; + try + if FLoadCount = 0 then + begin + if FLoader = nil then + FLoader:=GetOpenSSLLoader; + + Result := FLoader.Load; + if not Result then + Exit(False); + end + else + Result := True; + + Inc(FLoadCount); + finally + Unlock; + end; + {$ENDIF} +end; + +class procedure TOsslLoader.Unload; +begin + {$IFNDEF OPENSSL_STATIC_LINK_MODEL} + Lock; + try + if FLoadCount > 0 then + begin + Dec(FLoadCount); + if FLoadCount = 0 then + begin + if Assigned(FLoader) then + begin + FLoader.Unload; + end; + end; + end; + finally + Unlock; + end; + {$ENDIF} +end; + {$IFDEF USE_FASTMM5} { TFastMMDebugLoader }