From e5fec96a3e5b8efb2c761adc4383161f2eb0f04f Mon Sep 17 00:00:00 2001 From: Ugochukwu Mmaduekwe Date: Tue, 7 Jul 2026 23:07:52 +0100 Subject: [PATCH 1/4] Add id-kp-imUri and harden ASN.1 stream materialization Add TKeyPurposeId.IdKpImUri (1.3.6.1.5.5.7.3.40), TLimitedInputStream leave-open support with PipeAllLimited updated accordingly, and incremental DefiniteLength ToArray via TLimitedCapacityMemoryStream. On FPC, CopyTo/ReadAll/ReadFully dispatch through Read(var), so move bulk read logic onto Read(var) for DefiniteLength, IndefiniteLength, ConstructedOctet, and ConstructedBit streams (TArray delegates down). This ensures large BER/DER payloads materialize through efficient bulk reads instead of byte-at-a-time fallback paths. --- .../Delphi.Tests/CryptoLib.Tests.Mobile.dpr | 3 + .../Delphi.Tests/CryptoLib.Tests.Mobile.dproj | 3 + .../Delphi.Tests/CryptoLib.Tests.dpr | 3 + .../FreePascal.Tests/CryptoLib.Tests.lpi | 14 +- .../FreePascal.Tests/CryptoLib.lpr | 23 +- .../FreePascal.Tests/CryptoLibConsole.lpi | 14 +- .../FreePascal.Tests/CryptoLibConsole.lpr | 31 +- .../src/Asn1/Asn1StreamReadTests.pas | 483 ++++++++++++++++++ .../Asn1/DefiniteLengthAllocationTests.pas | 210 ++++++++ .../src/Asn1/X509/KeyPurposeIDTests.pas | 86 ++++ CryptoLib/src/Asn1/ClpAsn1Streams.pas | 120 ++++- .../src/Asn1/X509/ClpX509Asn1Objects.pas | 10 + CryptoLib/src/IO/ClpStreamUtilities.pas | 2 +- CryptoLib/src/IO/ClpStreams.pas | 31 +- 14 files changed, 976 insertions(+), 57 deletions(-) create mode 100644 CryptoLib.Tests/src/Asn1/Asn1StreamReadTests.pas create mode 100644 CryptoLib.Tests/src/Asn1/DefiniteLengthAllocationTests.pas create mode 100644 CryptoLib.Tests/src/Asn1/X509/KeyPurposeIDTests.pas diff --git a/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.Mobile.dpr b/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.Mobile.dpr index 2e7edcaf..dc9d03c7 100644 --- a/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.Mobile.dpr +++ b/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.Mobile.dpr @@ -827,6 +827,8 @@ uses ExternalTests in '..\src\Asn1\ExternalTests.pas', GeneralizedTimeTests in '..\src\Asn1\GeneralizedTimeTests.pas', InputStreamTests in '..\src\Asn1\InputStreamTests.pas', + DefiniteLengthAllocationTests in '..\src\Asn1\DefiniteLengthAllocationTests.pas', + Asn1StreamReadTests in '..\src\Asn1\Asn1StreamReadTests.pas', OctetStringTests in '..\src\Asn1\OctetStringTests.pas', OIDTests in '..\src\Asn1\OIDTests.pas', ParseTests in '..\src\Asn1\ParseTests.pas', @@ -920,6 +922,7 @@ uses IetfUtilitiesTests in '..\src\Asn1\X500\IetfUtilitiesTests.pas', GeneralNameTests in '..\src\Asn1\X509\GeneralNameTests.pas', KeyUsageTests in '..\src\Asn1\X509\KeyUsageTests.pas', + KeyPurposeIDTests in '..\src\Asn1\X509\KeyPurposeIDTests.pas', SubjectKeyIdentifierTests in '..\src\Asn1\X509\SubjectKeyIdentifierTests.pas', X509AltTests in '..\src\Asn1\X509\X509AltTests.pas', X509ExtensionsTests in '..\src\Asn1\X509\X509ExtensionsTests.pas', diff --git a/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.Mobile.dproj b/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.Mobile.dproj index 91ad8636..0993ce34 100644 --- a/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.Mobile.dproj +++ b/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.Mobile.dproj @@ -1170,6 +1170,8 @@ + + @@ -1258,6 +1260,7 @@ + diff --git a/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.dpr b/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.dpr index 9bb65b5f..9080b797 100644 --- a/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.dpr +++ b/CryptoLib.Tests/Delphi.Tests/CryptoLib.Tests.dpr @@ -846,6 +846,8 @@ uses ExternalTests in '..\src\Asn1\ExternalTests.pas', GeneralizedTimeTests in '..\src\Asn1\GeneralizedTimeTests.pas', InputStreamTests in '..\src\Asn1\InputStreamTests.pas', + DefiniteLengthAllocationTests in '..\src\Asn1\DefiniteLengthAllocationTests.pas', + Asn1StreamReadTests in '..\src\Asn1\Asn1StreamReadTests.pas', OctetStringTests in '..\src\Asn1\OctetStringTests.pas', OIDTests in '..\src\Asn1\OIDTests.pas', ParseTests in '..\src\Asn1\ParseTests.pas', @@ -939,6 +941,7 @@ uses IetfUtilitiesTests in '..\src\Asn1\X500\IetfUtilitiesTests.pas', GeneralNameTests in '..\src\Asn1\X509\GeneralNameTests.pas', KeyUsageTests in '..\src\Asn1\X509\KeyUsageTests.pas', + KeyPurposeIDTests in '..\src\Asn1\X509\KeyPurposeIDTests.pas', SubjectKeyIdentifierTests in '..\src\Asn1\X509\SubjectKeyIdentifierTests.pas', X509AltTests in '..\src\Asn1\X509\X509AltTests.pas', X509ExtensionsTests in '..\src\Asn1\X509\X509ExtensionsTests.pas', diff --git a/CryptoLib.Tests/FreePascal.Tests/CryptoLib.Tests.lpi b/CryptoLib.Tests/FreePascal.Tests/CryptoLib.Tests.lpi index 9c15e648..aebb8170 100644 --- a/CryptoLib.Tests/FreePascal.Tests/CryptoLib.Tests.lpi +++ b/CryptoLib.Tests/FreePascal.Tests/CryptoLib.Tests.lpi @@ -79,7 +79,7 @@ - + @@ -825,6 +825,18 @@ + + + + + + + + + + + + diff --git a/CryptoLib.Tests/FreePascal.Tests/CryptoLib.lpr b/CryptoLib.Tests/FreePascal.Tests/CryptoLib.lpr index 2cb3a95b..19438c72 100644 --- a/CryptoLib.Tests/FreePascal.Tests/CryptoLib.lpr +++ b/CryptoLib.Tests/FreePascal.Tests/CryptoLib.lpr @@ -11,12 +11,12 @@ NamedCurveTests, SignerUtilitiesTests, SecureRandomTests, DigestRandomNumberTests, FixedPointTests, AESTests, AesLightTests, FusedExternalRegistrationTests, AesHardwareEngineTests, AESSICTests, - SicBulkParityTests, EcbBulkParityTests, CbcBulkParityTests, GcmSivBulkParityTests, - SpeckBlockCipherTestBase, SpeckLegacyTests, SpeckTests, IESCipherTests, - HMacTests, Pkcs5Tests, HkdfGeneratorTests, ECIESTests, PascalCoinECIESTests, - ECNRTests, PrimesTests, ECEncodingTests, GF256AesTests, PaddingTests, - DSATests, DeterministicDsaTests, Salsa20Tests, XSalsa20Tests, ChaChaTests, - ChaCha7539ProcessBlocks2Tests, HChaCha20Tests, XChaCha20Tests, + SicBulkParityTests, EcbBulkParityTests, CbcBulkParityTests, + GcmSivBulkParityTests, SpeckBlockCipherTestBase, SpeckLegacyTests, SpeckTests, + IESCipherTests, HMacTests, Pkcs5Tests, HkdfGeneratorTests, ECIESTests, + PascalCoinECIESTests, ECNRTests, PrimesTests, ECEncodingTests, GF256AesTests, + PaddingTests, DSATests, DeterministicDsaTests, Salsa20Tests, XSalsa20Tests, + ChaChaTests, ChaCha7539ProcessBlocks2Tests, HChaCha20Tests, XChaCha20Tests, XChaCha20Poly1305Tests, StreamCipherResetTests, CTSTests, X25519Tests, X448Tests, Ed25519Tests, Ed448Tests, X25519HigherLevelTests, Ed25519HigherLevelTests, ShortenedDigestTests, Kdf1GeneratorTests, @@ -27,15 +27,16 @@ DHTests, Asn1IntegerTests, BitStringTests, GeneralizedTimeTests, OctetStringTests, RelativeOidTests, UtcTimeTests, InputStreamTests, SetTests, X9Tests, PrivateKeyInfoTests, DerUtf8StringTests, - EncryptedPrivateKeyInfoTests, Pkcs10CertRequestTests, DeltaCertificateTests, + EncryptedPrivateKeyInfoTests, DefiniteLengthAllocationTests, + Asn1StreamReadTests, Pkcs10CertRequestTests, DeltaCertificateTests, RelatedCertificateTests, CcmParametersTests, GcmParametersTests, CertificateTests, X509AltTests, X509ExtensionsTests, AuthorityKeyIdentifierTests, IdpRelativeNameTests, IetfUtilitiesTests, X509NameTests, SubjectKeyIdentifierTests, KeyUsageTests, GeneralNameTests, - KMacTests, RSATests, PssTests, ISO9796Tests, RSABlindedTests, - RSADigestSignerTests, X931SignerTests, CryptoIOStreamTests, CryptoIOSinkTests, - OaepTests, RijndaelTests, BlowfishTests, Poly1305Tests, MacTests, - ChaCha20Poly1305Tests, OcbTests, CcmTests, EaxTests, CMacTests, + KeyPurposeIDTests, KMacTests, RSATests, PssTests, ISO9796Tests, + RSABlindedTests, RSADigestSignerTests, X931SignerTests, CryptoIOStreamTests, + CryptoIOSinkTests, OaepTests, RijndaelTests, BlowfishTests, Poly1305Tests, + MacTests, ChaCha20Poly1305Tests, OcbTests, CcmTests, EaxTests, CMacTests, AeadTestUtilities, GcmReorderTests, GCMTests, GcmSivTests, GMacTests, Pkcs12Tests, Bip327MuSig2Tests, Bip340SchnorrTests, AlgorithmFinderTests, MlKemTests, MlDsaTests, PqcPkcsTests, SlhDsaTests, Lib25519Tests, diff --git a/CryptoLib.Tests/FreePascal.Tests/CryptoLibConsole.lpi b/CryptoLib.Tests/FreePascal.Tests/CryptoLibConsole.lpi index 213a0319..d2d1e7eb 100644 --- a/CryptoLib.Tests/FreePascal.Tests/CryptoLibConsole.lpi +++ b/CryptoLib.Tests/FreePascal.Tests/CryptoLibConsole.lpi @@ -39,7 +39,7 @@ - + @@ -784,6 +784,18 @@ + + + + + + + + + + + + diff --git a/CryptoLib.Tests/FreePascal.Tests/CryptoLibConsole.lpr b/CryptoLib.Tests/FreePascal.Tests/CryptoLibConsole.lpr index 7dcf180a..f4dc5434 100644 --- a/CryptoLib.Tests/FreePascal.Tests/CryptoLibConsole.lpr +++ b/CryptoLib.Tests/FreePascal.Tests/CryptoLibConsole.lpr @@ -12,12 +12,12 @@ NamedCurveTests, SignerUtilitiesTests, SecureRandomTests, DigestRandomNumberTests, FixedPointTests, AESTests, AesLightTests, FusedExternalRegistrationTests, AesHardwareEngineTests, AESSICTests, - SicBulkParityTests, EcbBulkParityTests, CbcBulkParityTests, GcmSivBulkParityTests, - SpeckBlockCipherTestBase, SpeckLegacyTests, SpeckTests, IESCipherTests, - HMacTests, Pkcs5Tests, HkdfGeneratorTests, ECIESTests, PascalCoinECIESTests, - ECNRTests, PrimesTests, ECEncodingTests, GF256AesTests, PaddingTests, - DSATests, DeterministicDsaTests, Salsa20Tests, XSalsa20Tests, ChaChaTests, - ChaCha7539ProcessBlocks2Tests, HChaCha20Tests, XChaCha20Tests, + SicBulkParityTests, EcbBulkParityTests, CbcBulkParityTests, + GcmSivBulkParityTests, SpeckBlockCipherTestBase, SpeckLegacyTests, SpeckTests, + IESCipherTests, HMacTests, Pkcs5Tests, HkdfGeneratorTests, ECIESTests, + PascalCoinECIESTests, ECNRTests, PrimesTests, ECEncodingTests, GF256AesTests, + PaddingTests, DSATests, DeterministicDsaTests, Salsa20Tests, XSalsa20Tests, + ChaChaTests, ChaCha7539ProcessBlocks2Tests, HChaCha20Tests, XChaCha20Tests, XChaCha20Poly1305Tests, StreamCipherResetTests, CTSTests, X25519Tests, X448Tests, Ed25519Tests, Ed448Tests, X25519HigherLevelTests, Ed25519HigherLevelTests, Curve25519KeyUtilitiesTests, ShortenedDigestTests, @@ -28,19 +28,20 @@ DHTests, Asn1IntegerTests, GeneralizedTimeTests, BitStringTests, InputStreamTests, UtcTimeTests, RelativeOidTests, OctetStringTests, SetTests, X9Tests, PrivateKeyInfoTests, DerUtf8StringTests, - EncryptedPrivateKeyInfoTests, Pkcs10CertRequestTests, DeltaCertificateTests, + EncryptedPrivateKeyInfoTests, DefiniteLengthAllocationTests, + Asn1StreamReadTests, Pkcs10CertRequestTests, DeltaCertificateTests, RelatedCertificateTests, CcmParametersTests, GcmParametersTests, CertificateTests, X509AltTests, X509ExtensionsTests, AuthorityKeyIdentifierTests, IdpRelativeNameTests, IetfUtilitiesTests, X509NameTests, SubjectKeyIdentifierTests, KeyUsageTests, GeneralNameTests, - KMacTests, PssTests, ISO9796Tests, RSABlindedTests, RSADigestSignerTests, - RSATests, X931SignerTests, CryptoIOStreamTests, CryptoIOSinkTests, OaepTests, - RijndaelTests, BlowfishTests, CcmTests, ChaCha20Poly1305Tests, CMacTests, - EaxTests, OcbTests, MacTests, Poly1305Tests, AeadTestUtilities, - GcmReorderTests, GCMTests, GcmSivTests, GMacTests, Pkcs12Tests, - Bip327MuSig2Tests, Bip340SchnorrTests, AlgorithmFinderTests, MlKemTests, - MlDsaTests, SlhDsaTests, PqcTestSampler, PqcPkcsTests, Lib25519Tests, - Asn1CipherBuilderWithKeyTests, CryptoLibTestBase, CtrDrbgTests, + KeyPurposeIDTests, KMacTests, PssTests, ISO9796Tests, RSABlindedTests, + RSADigestSignerTests, RSATests, X931SignerTests, CryptoIOStreamTests, + CryptoIOSinkTests, OaepTests, RijndaelTests, BlowfishTests, CcmTests, + ChaCha20Poly1305Tests, CMacTests, EaxTests, OcbTests, MacTests, Poly1305Tests, + AeadTestUtilities, GcmReorderTests, GCMTests, GcmSivTests, GMacTests, + Pkcs12Tests, Bip327MuSig2Tests, Bip340SchnorrTests, AlgorithmFinderTests, + MlKemTests, MlDsaTests, SlhDsaTests, PqcTestSampler, PqcPkcsTests, + Lib25519Tests, Asn1CipherBuilderWithKeyTests, CryptoLibTestBase, CtrDrbgTests, DrbgTestSupport, HashDrbgTests, HMacDrbgTests, SimdSelectSlotTests, BinaryPrimitivesTests, PkcsEncryptedPrivateKeyInfoTests, Pkcs12StoreTests, OpenSslReaderTests, OpenSslWriterTests, X509CertGenTests, diff --git a/CryptoLib.Tests/src/Asn1/Asn1StreamReadTests.pas b/CryptoLib.Tests/src/Asn1/Asn1StreamReadTests.pas new file mode 100644 index 00000000..b1a961b9 --- /dev/null +++ b/CryptoLib.Tests/src/Asn1/Asn1StreamReadTests.pas @@ -0,0 +1,483 @@ +{ *********************************************************************************** } +{ * CryptoLib Library * } +{ * Author - Ugochukwu Mmaduekwe * } +{ * Github Repository * } +{ * * } +{ * Distributed under the MIT software license, see the accompanying file LICENSE * } +{ * or visit http://www.opensource.org/licenses/mit-license.php. * } +{ * * } +{ * Acknowledgements: * } +{ * * } +{ * Thanks to Sphere 10 Software (http://www.sphere10.com/) for sponsoring * } +{ * the development of this library * } +{ * ******************************************************************************* * } + +(* &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& *) + +unit Asn1StreamReadTests; + +interface + +{$IFDEF FPC} +{$MODE DELPHI} +{$ENDIF FPC} + +uses + SysUtils, + Classes, + Math, +{$IFDEF FPC} + fpcunit, + testregistry, +{$ELSE} + TestFramework, +{$ENDIF FPC} + ClpAsn1Objects, + ClpIAsn1Objects, + ClpAsn1Streams, + ClpAsn1Parsers, + ClpIAsn1Parsers, + ClpIAsn1Generators, + ClpAsn1Generators, + ClpCryptoLibTypes, + ClpArrayUtilities, + ClpStreams, + ClpStreamUtilities, + CryptoLibTestBase; + +type + + TRecordingStream = class(TBaseInputStream) + strict private + FData: TCryptoLibByteArray; + FPos: Int32; + FFirstBulkReadLength: Int32; + + procedure RecordFirstBulkReadLength(ALength: Int32); + public + constructor Create(const AData: TCryptoLibByteArray); + + function Read(var ABuffer; ACount: LongInt): LongInt; override; + function Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt): LongInt; override; + function ReadByte: Int32; override; + + property FirstBulkReadLength: Int32 read FFirstBulkReadLength; + end; + + TProbeConstructedOctetStream = class(TAsn1ConstructedOctetStream) + strict private + FReadByteCallCount: Int32; + public + constructor Create(const AParser: IAsn1StreamParser); + + function Read(var ABuffer; ACount: LongInt): LongInt; override; + function Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt): LongInt; override; + function ReadByte: Int32; override; + + property ReadByteCallCount: Int32 read FReadByteCallCount; + end; + + TProbeConstructedBitStream = class(TAsn1ConstructedBitStream) + strict private + FReadByteCallCount: Int32; + public + constructor Create(const AParser: IAsn1StreamParser; AOctetAligned: Boolean); + + function Read(var ABuffer; ACount: LongInt): LongInt; override; + function Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt): LongInt; override; + function ReadByte: Int32; override; + + property ReadByteCallCount: Int32 read FReadByteCallCount; + end; + + TAsn1StreamReadTest = class(TCryptoLibAlgorithmTestCase) + strict private + const + LargePayloadSize = 5000; + MaxReadByteCallsForBulkPath = 16; + + function BuildBerIndefiniteOctetEncoding(const APayload: TCryptoLibByteArray): TCryptoLibByteArray; + function BuildBerIndefiniteConstructedBitString(const APayload: TCryptoLibByteArray): TCryptoLibByteArray; + + published + procedure TestConstructedOctetReadAllRoundTrip; + procedure TestConstructedOctetReadAllUsesBulkVarRead; + procedure TestConstructedBitReadAllRoundTrip; + procedure TestConstructedBitReadAllUsesBulkVarRead; + procedure TestIndefiniteLengthBulkReadDelegatesToWrappedStream; + procedure TestIndefiniteLengthRespectsEndOfContentsMarker; + procedure TestIndefiniteLengthMalformedEndOfContentsRaises; + end; + +implementation + +{ TRecordingStream } + +constructor TRecordingStream.Create(const AData: TCryptoLibByteArray); +begin + inherited Create; + FData := AData; + FPos := 0; + FFirstBulkReadLength := -1; +end; + +procedure TRecordingStream.RecordFirstBulkReadLength(ALength: Int32); +begin + if FFirstBulkReadLength < 0 then + FFirstBulkReadLength := ALength; +end; + +function TRecordingStream.Read(var ABuffer; ACount: LongInt): LongInt; +var + LToRead: Int32; +begin + RecordFirstBulkReadLength(ACount); + + if FPos >= System.Length(FData) then + begin + Result := 0; + Exit; + end; + + LToRead := Min(ACount, System.Length(FData) - FPos); + Move(FData[FPos], ABuffer, LToRead); + Inc(FPos, LToRead); + Result := LToRead; +end; + +function TRecordingStream.Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt): LongInt; +begin + TStreamUtilities.ValidateBufferArguments(ABuffer, AOffset, ACount); + Result := Read(ABuffer[AOffset], ACount); +end; + +function TRecordingStream.ReadByte: Int32; +begin + if FPos >= System.Length(FData) then + Result := -1 + else + begin + Result := FData[FPos]; + Inc(FPos); + end; +end; + +{ TProbeConstructedOctetStream } + +constructor TProbeConstructedOctetStream.Create(const AParser: IAsn1StreamParser); +begin + inherited Create(AParser); + FReadByteCallCount := 0; +end; + +function TProbeConstructedOctetStream.Read(var ABuffer; ACount: LongInt): LongInt; +begin + Result := inherited Read(ABuffer, ACount); +end; + +function TProbeConstructedOctetStream.Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt): LongInt; +begin + TStreamUtilities.ValidateBufferArguments(ABuffer, AOffset, ACount); + Result := Read(ABuffer[AOffset], ACount); +end; + +function TProbeConstructedOctetStream.ReadByte: Int32; +begin + Inc(FReadByteCallCount); + Result := inherited ReadByte; +end; + +{ TProbeConstructedBitStream } + +constructor TProbeConstructedBitStream.Create(const AParser: IAsn1StreamParser; + AOctetAligned: Boolean); +begin + inherited Create(AParser, AOctetAligned); + FReadByteCallCount := 0; +end; + +function TProbeConstructedBitStream.Read(var ABuffer; ACount: LongInt): LongInt; +begin + Result := inherited Read(ABuffer, ACount); +end; + +function TProbeConstructedBitStream.Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt): LongInt; +begin + TStreamUtilities.ValidateBufferArguments(ABuffer, AOffset, ACount); + Result := Read(ABuffer[AOffset], ACount); +end; + +function TProbeConstructedBitStream.ReadByte: Int32; +begin + Inc(FReadByteCallCount); + Result := inherited ReadByte; +end; + +{ TAsn1StreamReadTest } + +function TAsn1StreamReadTest.BuildBerIndefiniteOctetEncoding + (const APayload: TCryptoLibByteArray): TCryptoLibByteArray; +var + LBOut: TMemoryStream; + LOctGen: IBerOctetStringGenerator; + LOutStream: TStream; +begin + LBOut := TMemoryStream.Create(); + try + LOctGen := TBerOctetStringGenerator.Create(LBOut); + LOutStream := LOctGen.GetOctetOutputStream(); + try + if System.Length(APayload) > 0 then + LOutStream.Write(APayload[0], System.Length(APayload)); + finally + LOutStream.Free; + end; + LOctGen := nil; + + LBOut.Position := 0; + System.SetLength(Result, LBOut.Size); + LBOut.Read(Result[0], System.Length(Result)); + finally + LBOut.Free; + end; +end; + +function TAsn1StreamReadTest.BuildBerIndefiniteConstructedBitString + (const APayload: TCryptoLibByteArray): TCryptoLibByteArray; +var + LFragmentBodyLen, LFragmentLen, LTotalLen, LI: Int32; +begin + LFragmentBodyLen := 1 + System.Length(APayload); + LFragmentLen := 1 + 3 + LFragmentBodyLen; + LTotalLen := 2 + LFragmentLen + 2; + + System.SetLength(Result, LTotalLen); + LI := 0; + Result[LI] := $23; + Inc(LI); + Result[LI] := $80; + Inc(LI); + + Result[LI] := $03; + Inc(LI); + Result[LI] := $82; + Inc(LI); + Result[LI] := Byte((LFragmentBodyLen shr 8) and $FF); + Inc(LI); + Result[LI] := Byte(LFragmentBodyLen and $FF); + Inc(LI); + Result[LI] := 0; + Inc(LI); + + if System.Length(APayload) > 0 then + begin + Move(APayload[0], Result[LI], System.Length(APayload)); + Inc(LI, System.Length(APayload)); + end; + + Result[LI] := 0; + Inc(LI); + Result[LI] := 0; +end; + +procedure TAsn1StreamReadTest.TestConstructedOctetReadAllRoundTrip; +var + LPayload, LEncoding, LActual: TCryptoLibByteArray; + LParser: IAsn1StreamParser; + LOctets: IBerOctetString; + LI: Int32; +begin + System.SetLength(LPayload, LargePayloadSize); + for LI := 0 to LargePayloadSize - 1 do + LPayload[LI] := Byte(LI); + + LEncoding := BuildBerIndefiniteOctetEncoding(LPayload); + LParser := TAsn1StreamParser.Create(LEncoding); + LOctets := TBerOctetStringParser.Parse(LParser); + + CheckTrue(TArrayUtilities.AreEqual(LPayload, LOctets.GetOctets()), + 'constructed BER octet string did not round-trip through ReadAll'); +end; + +procedure TAsn1StreamReadTest.TestConstructedOctetReadAllUsesBulkVarRead; +var + LPayload, LEncoding, LActual: TCryptoLibByteArray; + LParser: IAsn1StreamParser; + LProbe: TProbeConstructedOctetStream; + LI: Int32; +begin + System.SetLength(LPayload, LargePayloadSize); + for LI := 0 to LargePayloadSize - 1 do + LPayload[LI] := Byte(LI); + + LEncoding := BuildBerIndefiniteOctetEncoding(LPayload); + LParser := TAsn1StreamParser.Create(LEncoding); + LProbe := TProbeConstructedOctetStream.Create(LParser); + try + LActual := TStreamUtilities.ReadAll(LProbe); + CheckTrue(TArrayUtilities.AreEqual(LPayload, LActual), + 'constructed BER octet stream did not materialize payload'); + + if LProbe.ReadByteCallCount > MaxReadByteCallsForBulkPath then + Fail(Format('constructed octet stream used byte-at-a-time reads (%d ReadByte calls)', + [LProbe.ReadByteCallCount])); + finally + LProbe.Free; + end; +end; + +procedure TAsn1StreamReadTest.TestConstructedBitReadAllRoundTrip; +var + LPayload, LEncoding, LActual: TCryptoLibByteArray; + LParser: IAsn1StreamParser; + LBits: IBerBitString; + LI: Int32; +begin + System.SetLength(LPayload, LargePayloadSize); + for LI := 0 to LargePayloadSize - 1 do + LPayload[LI] := Byte(LI xor $5A); + + LEncoding := BuildBerIndefiniteConstructedBitString(LPayload); + LParser := TAsn1StreamParser.Create(LEncoding); + LBits := TBerBitStringParser.Parse(LParser); + + LActual := LBits.GetBytes(); + CheckTrue(TArrayUtilities.AreEqual(LPayload, LActual), + 'constructed BER bit string did not round-trip through ReadAll'); +end; + +procedure TAsn1StreamReadTest.TestConstructedBitReadAllUsesBulkVarRead; +var + LPayload, LEncoding, LActual: TCryptoLibByteArray; + LParser: IAsn1StreamParser; + LProbe: TProbeConstructedBitStream; + LI: Int32; +begin + System.SetLength(LPayload, LargePayloadSize); + for LI := 0 to LargePayloadSize - 1 do + LPayload[LI] := Byte(LI xor $5A); + + LEncoding := BuildBerIndefiniteConstructedBitString(LPayload); + LParser := TAsn1StreamParser.Create(LEncoding); + LProbe := TProbeConstructedBitStream.Create(LParser, False); + try + LActual := TStreamUtilities.ReadAll(LProbe); + CheckTrue(TArrayUtilities.AreEqual(LPayload, LActual), + 'constructed BER bit stream did not materialize payload'); + + if LProbe.ReadByteCallCount > MaxReadByteCallsForBulkPath then + Fail(Format('constructed bit stream used byte-at-a-time reads (%d ReadByte calls)', + [LProbe.ReadByteCallCount])); + finally + LProbe.Free; + end; +end; + +procedure TAsn1StreamReadTest.TestIndefiniteLengthBulkReadDelegatesToWrappedStream; +var + LPayload: TCryptoLibByteArray; + LRecording: TRecordingStream; + LIndefinite: TAsn1IndefiniteLengthInputStream; + LBuffer: TCryptoLibByteArray; + LRead, LI: Int32; +begin + System.SetLength(LPayload, LargePayloadSize); + for LI := 0 to LargePayloadSize - 1 do + LPayload[LI] := Byte(LI + 1); + + LRecording := TRecordingStream.Create(LPayload); + try + LIndefinite := TAsn1IndefiniteLengthInputStream.Create(LRecording, LargePayloadSize); + try + LIndefinite.SetEofOn00(False); + + System.SetLength(LBuffer, TStreamUtilities.DefaultBufferSize); + LRead := LIndefinite.Read(LBuffer[0], System.Length(LBuffer)); + + if LRead <= 1 then + Fail(Format('indefinite stream bulk read returned too few bytes (%d)', [LRead])); + + if (LRecording.FirstBulkReadLength < 0) or (LRecording.FirstBulkReadLength <= 1) then + Fail(Format('indefinite stream did not delegate bulk read to wrapped stream (%d)', + [LRecording.FirstBulkReadLength])); + finally + LIndefinite.Free; + end; + finally + LRecording.Free; + end; +end; + +procedure TAsn1StreamReadTest.TestIndefiniteLengthRespectsEndOfContentsMarker; +var + LPayload: TCryptoLibByteArray; + LWrapped: TMemoryStream; + LIndefinite: TAsn1IndefiniteLengthInputStream; + LBuffer: TCryptoLibByteArray; + LRead, LI: Int32; +begin + System.SetLength(LPayload, 4); + for LI := 0 to 3 do + LPayload[LI] := Byte(LI + 10); + + LWrapped := TMemoryStream.Create(); + try + LWrapped.WriteBuffer(LPayload[0], System.Length(LPayload)); + LWrapped.WriteBuffer(TCryptoLibByteArray.Create(0, 0)[0], 2); + LWrapped.Position := 0; + + LIndefinite := TAsn1IndefiniteLengthInputStream.Create(LWrapped, System.Length(LPayload) + 2); + try + System.SetLength(LBuffer, System.Length(LPayload)); + LRead := TStreamUtilities.ReadFully(LIndefinite, LBuffer, 0, System.Length(LBuffer)); + CheckEquals(System.Length(LPayload), LRead, + 'indefinite stream did not return payload before EOC'); + + CheckTrue(TArrayUtilities.AreEqual(LPayload, LBuffer), + 'indefinite stream payload mismatch before EOC'); + + LRead := LIndefinite.Read(LBuffer[0], System.Length(LBuffer)); + CheckEquals(0, LRead, 'indefinite stream should be exhausted after EOC'); + finally + LIndefinite.Free; + end; + finally + LWrapped.Free; + end; +end; + +procedure TAsn1StreamReadTest.TestIndefiniteLengthMalformedEndOfContentsRaises; +var + LWrapped: TMemoryStream; + LIndefinite: TAsn1IndefiniteLengthInputStream; +begin + LWrapped := TMemoryStream.Create(); + try + LWrapped.WriteBuffer(TCryptoLibByteArray.Create(0, 1)[0], 2); + LWrapped.Position := 0; + + try + LIndefinite := TAsn1IndefiniteLengthInputStream.Create(LWrapped, 2); + try + Fail('expected malformed end-of-contents exception'); + finally + LIndefinite.Free; + end; + except + on E: EIOCryptoLibException do + CheckEquals('malformed end-of-contents marker', E.Message); + end; + finally + LWrapped.Free; + end; +end; + +initialization + +{$IFDEF FPC} +RegisterTest(TAsn1StreamReadTest); +{$ELSE} +RegisterTest(TAsn1StreamReadTest.Suite); +{$ENDIF FPC} + +end. diff --git a/CryptoLib.Tests/src/Asn1/DefiniteLengthAllocationTests.pas b/CryptoLib.Tests/src/Asn1/DefiniteLengthAllocationTests.pas new file mode 100644 index 00000000..24a6b94f --- /dev/null +++ b/CryptoLib.Tests/src/Asn1/DefiniteLengthAllocationTests.pas @@ -0,0 +1,210 @@ +{ *********************************************************************************** } +{ * CryptoLib Library * } +{ * Author - Ugochukwu Mmaduekwe * } +{ * Github Repository * } +{ * * } +{ * Distributed under the MIT software license, see the accompanying file LICENSE * } +{ * or visit http://www.opensource.org/licenses/mit-license.php. * } +{ * * } +{ * Acknowledgements: * } +{ * * } +{ * Thanks to Sphere 10 Software (http://www.sphere10.com/) for sponsoring * } +{ * the development of this library * } +{ * ******************************************************************************* * } + +(* &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& *) + +unit DefiniteLengthAllocationTests; + +interface + +{$IFDEF FPC} +{$MODE DELPHI} +{$ENDIF FPC} + +uses + SysUtils, + Classes, + Math, +{$IFDEF FPC} + fpcunit, + testregistry, +{$ELSE} + TestFramework, +{$ENDIF FPC} + ClpAsn1Objects, + ClpIAsn1Objects, + ClpAsn1Streams, + ClpCryptoLibTypes, + ClpStreams, + ClpStreamUtilities, + CryptoLibTestBase; + +type + + TRecordingStream = class(TBaseInputStream) + strict private + FData: TCryptoLibByteArray; + FPos: Int32; + FFirstBulkReadLength: Int32; + + procedure RecordFirstBulkReadLength(ALength: Int32); + public + constructor Create(const AData: TCryptoLibByteArray); + + function Read(var ABuffer; ACount: LongInt): LongInt; override; + function Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt): LongInt; override; + function ReadByte: Int32; override; + + property FirstBulkReadLength: Int32 read FFirstBulkReadLength; + end; + + TDefiniteLengthAllocationTest = class(TCryptoLibAlgorithmTestCase) + published + procedure TestDeclaredLengthNotAllocatedUpFront; + procedure TestTruncatedObjectReportsExpectedMessage; + end; + +implementation + +{ TRecordingStream } + +constructor TRecordingStream.Create(const AData: TCryptoLibByteArray); +begin + inherited Create; + FData := AData; + FPos := 0; + FFirstBulkReadLength := -1; +end; + +procedure TRecordingStream.RecordFirstBulkReadLength(ALength: Int32); +begin + if FFirstBulkReadLength < 0 then + FFirstBulkReadLength := ALength; +end; + +function TRecordingStream.Read(var ABuffer; ACount: LongInt): LongInt; +var + LToRead: Int32; +begin + RecordFirstBulkReadLength(ACount); + + if FPos >= System.Length(FData) then + begin + Result := 0; + Exit; + end; + + LToRead := Min(ACount, System.Length(FData) - FPos); + Move(FData[FPos], ABuffer, LToRead); + Inc(FPos, LToRead); + Result := LToRead; +end; + +function TRecordingStream.Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt): LongInt; +begin + TStreamUtilities.ValidateBufferArguments(ABuffer, AOffset, ACount); + Result := Read(ABuffer[AOffset], ACount); +end; + +function TRecordingStream.ReadByte: Int32; +begin + if FPos >= System.Length(FData) then + Result := -1 + else + begin + Result := FData[FPos]; + Inc(FPos); + end; +end; + +{ TDefiniteLengthAllocationTest } + +procedure TDefiniteLengthAllocationTest.TestDeclaredLengthNotAllocatedUpFront; +const + DeclaredLength = 1 shl 20; +var + LInput: TCryptoLibByteArray; + LInStr: TRecordingStream; + LInputStream: TAsn1InputStream; + LOctets: IAsn1OctetString; + LFirstBulkReadLength: Int32; +begin + System.SetLength(LInput, 5 + DeclaredLength); + LInput[0] := $04; + LInput[1] := $83; + LInput[2] := $10; + LInput[3] := $00; + LInput[4] := $00; + + LInStr := TRecordingStream.Create(LInput); + try + LInputStream := TAsn1InputStream.Create(LInStr, True); + try + LOctets := TAsn1OctetString.GetInstance(LInputStream.ReadObject()); + CheckEquals(DeclaredLength, LOctets.GetOctetsLength(), + 'octet string did not materialize to the declared length'); + + LFirstBulkReadLength := LInStr.FirstBulkReadLength; + if (LFirstBulkReadLength < 0) or (LFirstBulkReadLength >= DeclaredLength) then + Fail(Format('first bulk read requested the full declared length (%d)', + [LFirstBulkReadLength])); + finally + LInputStream.Free; + end; + finally + LInStr.Free; + end; +end; + +procedure TDefiniteLengthAllocationTest.TestTruncatedObjectReportsExpectedMessage; +const + DeclaredLength = 1 shl 20; + BodySupplied = 10; +var + LInput: TCryptoLibByteArray; + LInStr: TRecordingStream; + LInputStream: TAsn1InputStream; + LExpected: String; +begin + System.SetLength(LInput, 5 + BodySupplied); + LInput[0] := $04; + LInput[1] := $83; + LInput[2] := $10; + LInput[3] := $00; + LInput[4] := $00; + + LExpected := Format('DEF length %d object truncated by %d', + [DeclaredLength, DeclaredLength - BodySupplied]); + + LInStr := TRecordingStream.Create(LInput); + try + LInputStream := TAsn1InputStream.Create(LInStr, True); + try + try + LInputStream.ReadObject(); + Fail('no exception on truncated definite-length object'); + except + on E: EEndOfStreamCryptoLibException do + CheckEquals(LExpected, E.Message, + Format('unexpected truncation message: %s', [E.Message])); + on E: Exception do + Fail(Format('unexpected exception: %s', [E.Message])); + end; + finally + LInputStream.Free; + end; + finally + LInStr.Free; + end; +end; + +initialization + +{$IFDEF FPC} +RegisterTest(TDefiniteLengthAllocationTest); +{$ELSE} +RegisterTest(TDefiniteLengthAllocationTest.Suite); +{$ENDIF FPC} + +end. diff --git a/CryptoLib.Tests/src/Asn1/X509/KeyPurposeIDTests.pas b/CryptoLib.Tests/src/Asn1/X509/KeyPurposeIDTests.pas new file mode 100644 index 00000000..8e12bb56 --- /dev/null +++ b/CryptoLib.Tests/src/Asn1/X509/KeyPurposeIDTests.pas @@ -0,0 +1,86 @@ +{ *********************************************************************************** } +{ * CryptoLib Library * } +{ * Author - Ugochukwu Mmaduekwe * } +{ * Github Repository * } +{ * * } +{ * Distributed under the MIT software license, see the accompanying file LICENSE * } +{ * or visit http://www.opensource.org/licenses/mit-license.php. * } +{ * * } +{ * Acknowledgements: * } +{ * * } +{ * Thanks to Sphere 10 Software (http://www.sphere10.com/) for sponsoring * } +{ * the development of this library * } +{ * ******************************************************************************* * } + +(* &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& *) + +unit KeyPurposeIDTests; + +interface + +{$IFDEF FPC} +{$MODE DELPHI} +{$ENDIF FPC} + +uses + SysUtils, +{$IFDEF FPC} + fpcunit, + testregistry, +{$ELSE} + TestFramework, +{$ENDIF FPC} + ClpX509Asn1Objects, + ClpAsn1Objects, + ClpIAsn1Objects, + CryptoLibTestBase; + +type + + TKeyPurposeIDTest = class(TCryptoLibAlgorithmTestCase) + strict private + procedure CheckKeyPurposeID(const AKeyPurposeID: IDerObjectIdentifier; + const AExpectedID: String); + + published + procedure TestKeyPurposeIDs; + end; + +implementation + +{ TKeyPurposeIDTest } + +procedure TKeyPurposeIDTest.CheckKeyPurposeID(const AKeyPurposeID: IDerObjectIdentifier; + const AExpectedID: String); +var + LRecoveredOid: IDerObjectIdentifier; +begin + if not SameText(AExpectedID, AKeyPurposeID.ID) then + Fail(Format('wrong ID for KeyPurposeID: expected %s but got %s', + [AExpectedID, AKeyPurposeID.ID])); + + LRecoveredOid := TDerObjectIdentifier.GetInstance(AKeyPurposeID.GetEncoded()); + if not SameText(AExpectedID, LRecoveredOid.ID) then + Fail(Format('KeyPurposeID did not round-trip: expected %s but got %s', + [AExpectedID, LRecoveredOid.ID])); +end; + +procedure TKeyPurposeIDTest.TestKeyPurposeIDs; +begin + CheckKeyPurposeID(TKeyPurposeId.IdKpDocumentSigning, '1.3.6.1.5.5.7.3.36'); + CheckKeyPurposeID(TKeyPurposeId.IdKpImUri, '1.3.6.1.5.5.7.3.40'); + CheckKeyPurposeID(TKeyPurposeId.IdKpConfigSigning, '1.3.6.1.5.5.7.3.41'); + CheckKeyPurposeID(TKeyPurposeId.IdKpTrustAnchorConfigSigning, '1.3.6.1.5.5.7.3.42'); + CheckKeyPurposeID(TKeyPurposeId.IdKpUpdatePackageSigning, '1.3.6.1.5.5.7.3.43'); + CheckKeyPurposeID(TKeyPurposeId.IdKpSafetyCommunication, '1.3.6.1.5.5.7.3.44'); +end; + +initialization + +{$IFDEF FPC} +RegisterTest(TKeyPurposeIDTest); +{$ELSE} +RegisterTest(TKeyPurposeIDTest.Suite); +{$ENDIF FPC} + +end. diff --git a/CryptoLib/src/Asn1/ClpAsn1Streams.pas b/CryptoLib/src/Asn1/ClpAsn1Streams.pas index b36204b8..976af1f4 100644 --- a/CryptoLib/src/Asn1/ClpAsn1Streams.pas +++ b/CryptoLib/src/Asn1/ClpAsn1Streams.pas @@ -123,6 +123,7 @@ TAsn1DefiniteLengthInputStream = class sealed(TAsn1LimitedInputStream) /// /// Reads bytes from the stream into a buffer. /// + function Read(var ABuffer; ACount: LongInt): LongInt; override; function Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt) : LongInt; override; @@ -165,6 +166,7 @@ TAsn1IndefiniteLengthInputStream = class sealed(TAsn1LimitedInputStream) /// /// Reads bytes from the stream into a buffer. /// + function Read(var ABuffer; ACount: LongInt): LongInt; override; function Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt) : LongInt; override; @@ -386,6 +388,7 @@ TAsn1ConstructedBitStream = class(TBaseInputStream) /// /// Read bytes from the stream. /// + function Read(var ABuffer; ACount: LongInt): LongInt; override; function Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt) : LongInt; override; @@ -425,6 +428,7 @@ TAsn1ConstructedOctetStream = class(TBaseInputStream) /// /// Read bytes from the stream. /// + function Read(var ABuffer; ACount: LongInt): LongInt; override; function Read(ABuffer: TCryptoLibByteArray; AOffset, ACount: LongInt) : LongInt; override; @@ -630,6 +634,49 @@ procedure TAsn1LimitedInputStream.SetParentEofDetect; end; end; +type + TLimitedCapacityMemoryStream = class(TMemoryStream) + strict private + FLimitedCapacity: Int32; + strict protected +{$IFDEF FPC} + function Realloc(var NewCapacity: PtrInt): Pointer; override; +{$ELSE} + procedure SetCapacity(NewCapacity: NativeInt); override; +{$ENDIF} + public + constructor Create(ALimitedCapacity: Int32); + function GetBuffer: TCryptoLibByteArray; + end; + +{ TLimitedCapacityMemoryStream } + +constructor TLimitedCapacityMemoryStream.Create(ALimitedCapacity: Int32); +begin + inherited Create; + FLimitedCapacity := ALimitedCapacity; +end; + +{$IFDEF FPC} +function TLimitedCapacityMemoryStream.Realloc(var NewCapacity: PtrInt): Pointer; +begin + NewCapacity := Min(NewCapacity * 2, FLimitedCapacity); + Result := inherited Realloc(NewCapacity); +end; +{$ELSE} +procedure TLimitedCapacityMemoryStream.SetCapacity(NewCapacity: NativeInt); +begin + inherited SetCapacity(Min(NewCapacity * 2, FLimitedCapacity)); +end; +{$ENDIF} + +function TLimitedCapacityMemoryStream.GetBuffer: TCryptoLibByteArray; +begin + System.SetLength(Result, Size); + if Size > 0 then + Move(Memory^, Result[0], Size); +end; + { TAsn1DefiniteLengthInputStream } constructor TAsn1DefiniteLengthInputStream.Create(const AInStream: TStream; @@ -678,8 +725,7 @@ function TAsn1DefiniteLengthInputStream.ReadByte: Int32; end; end; -function TAsn1DefiniteLengthInputStream.Read(ABuffer: TCryptoLibByteArray; - AOffset, ACount: LongInt): LongInt; +function TAsn1DefiniteLengthInputStream.Read(var ABuffer; ACount: LongInt): LongInt; var LToRead, LNumRead: Int32; begin @@ -690,7 +736,7 @@ function TAsn1DefiniteLengthInputStream.Read(ABuffer: TCryptoLibByteArray; end; LToRead := Min(ACount, FRemaining); - LNumRead := FIn.Read(ABuffer[AOffset], LToRead); + LNumRead := FIn.Read(ABuffer, LToRead); if LNumRead < 1 then raise EEndOfStreamCryptoLibException.CreateResFmt(@SDefLengthObjectTruncated, [FOriginalLength, FRemaining]); @@ -704,6 +750,13 @@ function TAsn1DefiniteLengthInputStream.Read(ABuffer: TCryptoLibByteArray; Result := LNumRead; end; +function TAsn1DefiniteLengthInputStream.Read(ABuffer: TCryptoLibByteArray; + AOffset, ACount: LongInt): LongInt; +begin + TStreamUtilities.ValidateBufferArguments(ABuffer, AOffset, ACount); + Result := Read(ABuffer[AOffset], ACount); +end; + procedure TAsn1DefiniteLengthInputStream.ReadAllIntoByteArray (const ABuf: TCryptoLibByteArray); var @@ -727,6 +780,9 @@ procedure TAsn1DefiniteLengthInputStream.ReadAllIntoByteArray end; function TAsn1DefiniteLengthInputStream.ToArray: TCryptoLibByteArray; +var + LBuf: TLimitedCapacityMemoryStream; + LThreshold: Int32; begin Result := nil; if FRemaining = 0 then @@ -736,6 +792,19 @@ function TAsn1DefiniteLengthInputStream.ToArray: TCryptoLibByteArray; TAsn1InputStream.CheckLength(FRemaining, Limit); + LThreshold := TStreamUtilities.DefaultBufferSize * 8; + if FRemaining > LThreshold then + begin + LBuf := TLimitedCapacityMemoryStream.Create(FRemaining); + try + TStreamUtilities.CopyTo(Self, LBuf); + Result := LBuf.GetBuffer(); + finally + LBuf.Free; + end; + Exit; + end; + System.SetLength(Result, FRemaining); FRemaining := FRemaining - TStreamUtilities.ReadFully(FIn, Result, 0, System.Length(Result)); @@ -796,15 +865,13 @@ function TAsn1IndefiniteLengthInputStream.ReadByte: Int32; FLookAhead := RequireByte(); end; -function TAsn1IndefiniteLengthInputStream.Read(ABuffer: TCryptoLibByteArray; - AOffset, ACount: LongInt): LongInt; +function TAsn1IndefiniteLengthInputStream.Read(var ABuffer; ACount: LongInt): LongInt; var LNumRead: Int32; begin - // Only use this optimisation if we aren't checking for 00 if FEofOn00 or (ACount <= 1) then begin - Result := inherited Read(ABuffer, AOffset, ACount); + Result := inherited Read(ABuffer, ACount); Exit; end; @@ -814,16 +881,23 @@ function TAsn1IndefiniteLengthInputStream.Read(ABuffer: TCryptoLibByteArray; Exit; end; - LNumRead := FIn.Read(ABuffer[AOffset + 1], ACount - 1); + LNumRead := FIn.Read(PByte(@ABuffer)[1], ACount - 1); if LNumRead <= 0 then raise EEndOfStreamCryptoLibException.CreateRes(@SUnexpectedEndOfStream); - ABuffer[AOffset] := Byte(FLookAhead); + PByte(@ABuffer)[0] := Byte(FLookAhead); FLookAhead := RequireByte(); Result := LNumRead + 1; end; +function TAsn1IndefiniteLengthInputStream.Read(ABuffer: TCryptoLibByteArray; + AOffset, ACount: LongInt): LongInt; +begin + TStreamUtilities.ValidateBufferArguments(ABuffer, AOffset, ACount); + Result := Read(ABuffer[AOffset], ACount); +end; + function TAsn1IndefiniteLengthInputStream.RequireByte: Int32; var LB: Int32; @@ -1275,13 +1349,10 @@ function TAsn1ConstructedBitStream.GetNextParser(): IAsn1BitStringParser; end; end; -function TAsn1ConstructedBitStream.Read(ABuffer: TCryptoLibByteArray; - AOffset, ACount: LongInt): LongInt; +function TAsn1ConstructedBitStream.Read(var ABuffer; ACount: LongInt): LongInt; var LTotalRead, LNumRead: Int32; begin - TStreamUtilities.ValidateBufferArguments(ABuffer, AOffset, ACount); - if ACount < 1 then begin Result := 0; @@ -1311,7 +1382,7 @@ function TAsn1ConstructedBitStream.Read(ABuffer: TCryptoLibByteArray; while True do begin - LNumRead := FCurrentStream.Read(ABuffer[AOffset + LTotalRead], ACount - LTotalRead); + LNumRead := FCurrentStream.Read(PByte(@ABuffer)[LTotalRead], ACount - LTotalRead); if LNumRead > 0 then begin @@ -1340,6 +1411,13 @@ function TAsn1ConstructedBitStream.Read(ABuffer: TCryptoLibByteArray; end; end; +function TAsn1ConstructedBitStream.Read(ABuffer: TCryptoLibByteArray; + AOffset, ACount: LongInt): LongInt; +begin + TStreamUtilities.ValidateBufferArguments(ABuffer, AOffset, ACount); + Result := Read(ABuffer[AOffset], ACount); +end; + function TAsn1ConstructedBitStream.ReadByte: Int32; var LB: Int32; @@ -1427,13 +1505,10 @@ function TAsn1ConstructedOctetStream.GetNextParser(): IAsn1OctetStringParser; end; end; -function TAsn1ConstructedOctetStream.Read(ABuffer: TCryptoLibByteArray; - AOffset, ACount: LongInt): LongInt; +function TAsn1ConstructedOctetStream.Read(var ABuffer; ACount: LongInt): LongInt; var LTotalRead, LNumRead: Int32; begin - TStreamUtilities.ValidateBufferArguments(ABuffer, AOffset, ACount); - if ACount < 1 then begin Result := 0; @@ -1463,7 +1538,7 @@ function TAsn1ConstructedOctetStream.Read(ABuffer: TCryptoLibByteArray; while True do begin - LNumRead := FCurrentStream.Read(ABuffer[AOffset + LTotalRead], ACount - LTotalRead); + LNumRead := FCurrentStream.Read(PByte(@ABuffer)[LTotalRead], ACount - LTotalRead); if LNumRead > 0 then begin @@ -1491,6 +1566,13 @@ function TAsn1ConstructedOctetStream.Read(ABuffer: TCryptoLibByteArray; end; end; +function TAsn1ConstructedOctetStream.Read(ABuffer: TCryptoLibByteArray; + AOffset, ACount: LongInt): LongInt; +begin + TStreamUtilities.ValidateBufferArguments(ABuffer, AOffset, ACount); + Result := Read(ABuffer[AOffset], ACount); +end; + function TAsn1ConstructedOctetStream.ReadByte: Int32; var LB: Int32; diff --git a/CryptoLib/src/Asn1/X509/ClpX509Asn1Objects.pas b/CryptoLib/src/Asn1/X509/ClpX509Asn1Objects.pas index caedd057..468ca9f4 100644 --- a/CryptoLib/src/Asn1/X509/ClpX509Asn1Objects.pas +++ b/CryptoLib/src/Asn1/X509/ClpX509Asn1Objects.pas @@ -801,6 +801,7 @@ TKeyPurposeId = class abstract(TObject) FIdKpCmcRa: IDerObjectIdentifier; FIdKpCmKga: IDerObjectIdentifier; FIdKpDocumentSigning: IDerObjectIdentifier; + FIdKpImUri: IDerObjectIdentifier; FIdKpConfigSigning: IDerObjectIdentifier; FIdKpTrustAnchorConfigSigning: IDerObjectIdentifier; FIdKpUpdatePackageSigning: IDerObjectIdentifier; @@ -840,6 +841,7 @@ TKeyPurposeId = class abstract(TObject) class function GetIdKpCmcRa: IDerObjectIdentifier; static; inline; class function GetIdKpCmKga: IDerObjectIdentifier; static; inline; class function GetIdKpDocumentSigning: IDerObjectIdentifier; static; inline; + class function GetIdKpImUri: IDerObjectIdentifier; static; inline; class function GetIdKpConfigSigning: IDerObjectIdentifier; static; inline; class function GetIdKpTrustAnchorConfigSigning: IDerObjectIdentifier; static; inline; class function GetIdKpUpdatePackageSigning: IDerObjectIdentifier; static; inline; @@ -882,6 +884,8 @@ TKeyPurposeId = class abstract(TObject) class property IdKpCmKga: IDerObjectIdentifier read GetIdKpCmKga; /// RFC 9336 sec. 3.1 ? signing documents (e.g. PDF, XML, JSON) for human consumption (id-kp-documentSigning, { id-kp 36 }). class property IdKpDocumentSigning: IDerObjectIdentifier read GetIdKpDocumentSigning; + /// RFC 9734 sec. 3 ? proving the identity of an IM client whose IM URI (RFC 3860) or XMPP URI (RFC 6121) appears in the subjectAltName (id-kp-imUri, { id-kp 40 }). + class property IdKpImUri: IDerObjectIdentifier read GetIdKpImUri; /// RFC 9809 sec. 3 ? signing general-purpose configuration files (id-kp-configSigning, { id-kp 41 }). class property IdKpConfigSigning: IDerObjectIdentifier read GetIdKpConfigSigning; /// RFC 9809 sec. 3 ? signing trust anchor configuration files (id-kp-trustAnchorConfigSigning, { id-kp 42 }). @@ -6162,6 +6166,7 @@ function TExtensions.ToAsn1Object: IAsn1Object; FIdKpCmcRa := TDerObjectIdentifier.Create(LIdKp.ID + '.28'); FIdKpCmKga := TDerObjectIdentifier.Create(LIdKp.ID + '.32'); FIdKpDocumentSigning := TDerObjectIdentifier.Create(LIdKp.ID + '.36'); + FIdKpImUri := TDerObjectIdentifier.Create(LIdKp.ID + '.40'); FIdKpConfigSigning := TDerObjectIdentifier.Create(LIdKp.ID + '.41'); FIdKpTrustAnchorConfigSigning := TDerObjectIdentifier.Create(LIdKp.ID + '.42'); FIdKpUpdatePackageSigning := TDerObjectIdentifier.Create(LIdKp.ID + '.43'); @@ -6299,6 +6304,11 @@ class function TKeyPurposeId.GetIdKpDocumentSigning: IDerObjectIdentifier; Result := FIdKpDocumentSigning; end; +class function TKeyPurposeId.GetIdKpImUri: IDerObjectIdentifier; +begin + Result := FIdKpImUri; +end; + class function TKeyPurposeId.GetIdKpConfigSigning: IDerObjectIdentifier; begin Result := FIdKpConfigSigning; diff --git a/CryptoLib/src/IO/ClpStreamUtilities.pas b/CryptoLib/src/IO/ClpStreamUtilities.pas index 0d979e57..8f47fce2 100644 --- a/CryptoLib/src/IO/ClpStreamUtilities.pas +++ b/CryptoLib/src/IO/ClpStreamUtilities.pas @@ -160,7 +160,7 @@ class function TStreamUtilities.PipeAllLimited(const AInStr: TStream; var LLimited: TLimitedInputStream; begin - LLimited := TLimitedInputStream.Create(AInStr, ALimit); + LLimited := TLimitedInputStream.Create(ALimit, AInStr, True); try CopyTo(LLimited, AOutStr, ABufferSize); Result := ALimit - LLimited.CurrentLimit; diff --git a/CryptoLib/src/IO/ClpStreams.pas b/CryptoLib/src/IO/ClpStreams.pas index 0f856ae9..e57cade6 100644 --- a/CryptoLib/src/IO/ClpStreams.pas +++ b/CryptoLib/src/IO/ClpStreams.pas @@ -207,17 +207,21 @@ TLimitedInputStream = class sealed(TBaseInputStream) strict private var - FLimit: Int64; + FCurrentLimit: Int64; + FLeaveOpen: Boolean; FStream: TStream; public - constructor Create(const AStream: TStream; ALimit: Int64); + constructor Create(ALimit: Int64; const AStream: TStream; + ALeaveOpen: Boolean = False); + + destructor Destroy; override; function Read(var ABuffer; ACount: LongInt): LongInt; override; function ReadByte(): Int32; override; - property CurrentLimit: Int64 read FLimit; + property CurrentLimit: Int64 read FCurrentLimit; end; @@ -649,11 +653,20 @@ constructor TBufferedFilterStream.Create(const AStream: TStream; { TLimitedInputStream } -constructor TLimitedInputStream.Create(const AStream: TStream; ALimit: Int64); +constructor TLimitedInputStream.Create(ALimit: Int64; const AStream: TStream; + ALeaveOpen: Boolean); begin inherited Create(); FStream := AStream; - FLimit := ALimit; + FCurrentLimit := ALimit; + FLeaveOpen := ALeaveOpen; +end; + +destructor TLimitedInputStream.Destroy; +begin + if not FLeaveOpen then + FStream.Free; + inherited Destroy; end; function TLimitedInputStream.Read(var ABuffer; ACount: LongInt): LongInt; @@ -663,8 +676,8 @@ function TLimitedInputStream.Read(var ABuffer; ACount: LongInt): LongInt; LNumRead := FStream.Read(ABuffer, ACount); if LNumRead > 0 then begin - FLimit := FLimit - LNumRead; - if FLimit < 0 then + FCurrentLimit := FCurrentLimit - LNumRead; + if FCurrentLimit < 0 then raise EStreamOverflowCryptoLibException.CreateRes(@SDataOverflow); end; Result := LNumRead; @@ -677,8 +690,8 @@ function TLimitedInputStream.ReadByte(): Int32; LB := FStream.ReadByte(); if LB >= 0 then begin - System.Dec(FLimit); - if FLimit < 0 then + System.Dec(FCurrentLimit); + if FCurrentLimit < 0 then raise EStreamOverflowCryptoLibException.CreateRes(@SDataOverflow); end; Result := LB; From 9d1e1822c62e4f124b9c2b75f1e76d4eb0de3e35 Mon Sep 17 00:00:00 2001 From: Ugochukwu Mmaduekwe Date: Tue, 7 Jul 2026 23:30:03 +0100 Subject: [PATCH 2/4] Lower default PBE iteration ceiling to 5_000_000 and add regression tests. Lower default PBE iteration ceiling to 5_000_000 and add regression tests. Expose CheckPbeIterationCount so callers can validate PBKDF2 iteration counts before key derivation, and add PKCS#8/PBES2 tests for the new limit. --- .../Pkcs/PkcsEncryptedPrivateKeyInfoTests.pas | 68 +++++++++++++++++++ CryptoLib/src/Crypto/ClpPbeUtilities.pas | 9 ++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/CryptoLib.Tests/src/Pkcs/PkcsEncryptedPrivateKeyInfoTests.pas b/CryptoLib.Tests/src/Pkcs/PkcsEncryptedPrivateKeyInfoTests.pas index 327a6b73..7c135d10 100644 --- a/CryptoLib.Tests/src/Pkcs/PkcsEncryptedPrivateKeyInfoTests.pas +++ b/CryptoLib.Tests/src/Pkcs/PkcsEncryptedPrivateKeyInfoTests.pas @@ -45,6 +45,8 @@ interface ClpPrivateKeyInfoFactory, ClpPrivateKeyFactory, ClpPbeUtilities, + ClpIAsn1Objects, + ClpAsn1Objects, ClpIPkcsAsn1Objects, ClpPkcsAsn1Objects, ClpCryptoLibTypes, @@ -73,6 +75,8 @@ TTestPkcsEncryptedPrivateKeyInfo = class(TCryptoLibAlgorithmTestCase) procedure TestOpensslPbes2AesDefaultKeys; procedure TestPbkdf2IterationCountBound; procedure TestPkcs5V1PbeIterationCountBound; + procedure TestPbeDefaultMaxIterationCount; + procedure TestPbes2DefaultMaxIterationCountBound; end; @@ -278,6 +282,70 @@ procedure TTestPkcsEncryptedPrivateKeyInfo.TestPkcs5V1PbeIterationCountBound; end; end; +procedure TTestPkcsEncryptedPrivateKeyInfo.TestPbeDefaultMaxIterationCount; +var + LOldMax: Int32; + LCount: Int32; +begin + LOldMax := TPbeUtilities.MaxIterationCount; + try + TPbeUtilities.MaxIterationCount := -1; + + LCount := TPbeUtilities.CheckPbeIterationCount(TDerInteger.ValueOf(5000000)); + CheckEquals(5000000, LCount, 'default max iteration count should allow 5_000_000'); + + try + TPbeUtilities.CheckPbeIterationCount(TDerInteger.ValueOf(5000001)); + Fail('iteration count above default max accepted'); + except + on E: EArgumentCryptoLibException do + CheckTrue(Pos('greater than 5000000', E.Message) > 0, + 'unexpected message: ' + E.Message); + end; + finally + TPbeUtilities.MaxIterationCount := LOldMax; + end; +end; + +procedure TTestPkcsEncryptedPrivateKeyInfo.TestPbes2DefaultMaxIterationCountBound; +const + LExcessiveIterationCount = 5000001; +var + LSalt, LIv: TCryptoLibByteArray; + LPassword: TCryptoLibCharArray; + LPbkdf2Params: IPbkdf2Params; + LKeyDerivFunc: IKeyDerivationFunc; + LEncScheme: IEncryptionScheme; + LPbeS2Params: IPbeS2Parameters; + LOldMax: Int32; +begin + LPassword := StringToCharArray('password'); + LSalt := DecodeHex('0102030405060708090A0B0C0D0E0F1011121314'); + LIv := DecodeHex('0102030405060708090A0B0C0D0E0F10'); + + LPbkdf2Params := TPbkdf2Params.Create(LSalt, LExcessiveIterationCount); + LKeyDerivFunc := TKeyDerivationFunc.Create(TPkcsObjectIdentifiers.IdPbkdf2, LPbkdf2Params); + LEncScheme := TEncryptionScheme.Create(TNistObjectIdentifiers.IdAes256Cbc, + TDerOctetString.FromContents(LIv)); + LPbeS2Params := TPbeS2Parameters.Create(LKeyDerivFunc, LEncScheme); + + LOldMax := TPbeUtilities.MaxIterationCount; + try + TPbeUtilities.MaxIterationCount := -1; + try + TPbeUtilities.GenerateCipherParameters(TPkcsObjectIdentifiers.IdPbeS2, + LPassword, LPbeS2Params); + Fail('PBES2 derivation accepted excessive default-max iteration count'); + except + on E: EArgumentCryptoLibException do + CheckTrue(Pos('greater than 5000000', E.Message) > 0, + 'unexpected message: ' + E.Message); + end; + finally + TPbeUtilities.MaxIterationCount := LOldMax; + end; +end; + initialization {$IFDEF FPC} diff --git a/CryptoLib/src/Crypto/ClpPbeUtilities.pas b/CryptoLib/src/Crypto/ClpPbeUtilities.pas index c90a1356..d74e7ecc 100644 --- a/CryptoLib/src/Crypto/ClpPbeUtilities.pas +++ b/CryptoLib/src/Crypto/ClpPbeUtilities.pas @@ -84,7 +84,7 @@ TPbeUtilities = class sealed(TObject) Pkcs5S2 = 'Pkcs5S2'; Pkcs12 = 'Pkcs12'; OpenSsl = 'OpenSsl'; - DefaultMaxIterationCount = 10000000; + DefaultMaxIterationCount = 5000000; class var FAlgorithms: TDictionary; FAlgorithmType: TDictionary; @@ -94,7 +94,6 @@ TPbeUtilities = class sealed(TObject) class destructor Destroy; strict private class function GetEffectiveMaxIterationCount: Int32; static; - class function CheckPbeIterationCount(const AIterationCountObject: IDerInteger): Int32; static; class function MakePbeGenerator(const AType: String; const ADigest: IDigest; const AKey: TCryptoLibByteArray; const ASalt: TCryptoLibByteArray; AIterationCount: Int32): IPbeParametersGenerator; static; @@ -174,6 +173,12 @@ TPbeUtilities = class sealed(TObject) const APassword: TCryptoLibCharArray; AWrongPkcs12Zero: Boolean; const APbeParameters: IAsn1Encodable): ICipherParameters; overload; static; + /// + /// Validates a supplied PBE/PBKDF2 iteration count against + /// (or when unset). + /// + class function CheckPbeIterationCount(const AIterationCountObject: IDerInteger): Int32; static; + /// /// Maximum allowed PBE iteration count when decrypting PKCS#8 / PEM keys. /// Unset (-1) or any negative value selects . From 01ae5f87a20c58c8305ba281f73e3ff03639c7d8 Mon Sep 17 00:00:00 2001 From: Ugochukwu Mmaduekwe Date: Tue, 7 Jul 2026 23:39:07 +0100 Subject: [PATCH 3/4] move private custom stream --- CryptoLib/src/Asn1/ClpAsn1Streams.pas | 86 +++++++++++++-------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/CryptoLib/src/Asn1/ClpAsn1Streams.pas b/CryptoLib/src/Asn1/ClpAsn1Streams.pas index 976af1f4..07c00507 100644 --- a/CryptoLib/src/Asn1/ClpAsn1Streams.pas +++ b/CryptoLib/src/Asn1/ClpAsn1Streams.pas @@ -577,6 +577,49 @@ implementation ClpAsn1Parsers, ClpAsn1Core; +type + TLimitedCapacityMemoryStream = class(TMemoryStream) + strict private + FLimitedCapacity: Int32; + strict protected +{$IFDEF FPC} + function Realloc(var NewCapacity: PtrInt): Pointer; override; +{$ELSE} + procedure SetCapacity(NewCapacity: NativeInt); override; +{$ENDIF} + public + constructor Create(ALimitedCapacity: Int32); + function GetBuffer: TCryptoLibByteArray; + end; + +{ TLimitedCapacityMemoryStream } + +constructor TLimitedCapacityMemoryStream.Create(ALimitedCapacity: Int32); +begin + inherited Create; + FLimitedCapacity := ALimitedCapacity; +end; + +{$IFDEF FPC} +function TLimitedCapacityMemoryStream.Realloc(var NewCapacity: PtrInt): Pointer; +begin + NewCapacity := Min(NewCapacity * 2, FLimitedCapacity); + Result := inherited Realloc(NewCapacity); +end; +{$ELSE} +procedure TLimitedCapacityMemoryStream.SetCapacity(NewCapacity: NativeInt); +begin + inherited SetCapacity(Min(NewCapacity * 2, FLimitedCapacity)); +end; +{$ENDIF} + +function TLimitedCapacityMemoryStream.GetBuffer: TCryptoLibByteArray; +begin + System.SetLength(Result, Size); + if Size > 0 then + Move(Memory^, Result[0], Size); +end; + { TAsn1InputStream } class constructor TAsn1InputStream.Create; @@ -634,49 +677,6 @@ procedure TAsn1LimitedInputStream.SetParentEofDetect; end; end; -type - TLimitedCapacityMemoryStream = class(TMemoryStream) - strict private - FLimitedCapacity: Int32; - strict protected -{$IFDEF FPC} - function Realloc(var NewCapacity: PtrInt): Pointer; override; -{$ELSE} - procedure SetCapacity(NewCapacity: NativeInt); override; -{$ENDIF} - public - constructor Create(ALimitedCapacity: Int32); - function GetBuffer: TCryptoLibByteArray; - end; - -{ TLimitedCapacityMemoryStream } - -constructor TLimitedCapacityMemoryStream.Create(ALimitedCapacity: Int32); -begin - inherited Create; - FLimitedCapacity := ALimitedCapacity; -end; - -{$IFDEF FPC} -function TLimitedCapacityMemoryStream.Realloc(var NewCapacity: PtrInt): Pointer; -begin - NewCapacity := Min(NewCapacity * 2, FLimitedCapacity); - Result := inherited Realloc(NewCapacity); -end; -{$ELSE} -procedure TLimitedCapacityMemoryStream.SetCapacity(NewCapacity: NativeInt); -begin - inherited SetCapacity(Min(NewCapacity * 2, FLimitedCapacity)); -end; -{$ENDIF} - -function TLimitedCapacityMemoryStream.GetBuffer: TCryptoLibByteArray; -begin - System.SetLength(Result, Size); - if Size > 0 then - Move(Memory^, Result[0], Size); -end; - { TAsn1DefiniteLengthInputStream } constructor TAsn1DefiniteLengthInputStream.Create(const AInStream: TStream; From d4c8baa52139f988a843bbe3021ff792573f31b7 Mon Sep 17 00:00:00 2001 From: Ugochukwu Mmaduekwe Date: Tue, 7 Jul 2026 23:44:56 +0100 Subject: [PATCH 4/4] remove unused unit --- CryptoLib.Tests/src/Pkcs/PkcsEncryptedPrivateKeyInfoTests.pas | 1 - 1 file changed, 1 deletion(-) diff --git a/CryptoLib.Tests/src/Pkcs/PkcsEncryptedPrivateKeyInfoTests.pas b/CryptoLib.Tests/src/Pkcs/PkcsEncryptedPrivateKeyInfoTests.pas index 7c135d10..106983fa 100644 --- a/CryptoLib.Tests/src/Pkcs/PkcsEncryptedPrivateKeyInfoTests.pas +++ b/CryptoLib.Tests/src/Pkcs/PkcsEncryptedPrivateKeyInfoTests.pas @@ -45,7 +45,6 @@ interface ClpPrivateKeyInfoFactory, ClpPrivateKeyFactory, ClpPbeUtilities, - ClpIAsn1Objects, ClpAsn1Objects, ClpIPkcsAsn1Objects, ClpPkcsAsn1Objects,