diff --git a/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs b/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs
index 69a46559b..d48a16173 100644
--- a/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs
+++ b/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs
@@ -2443,5 +2443,140 @@ internal static string YubiKeyOperationFailed {
return ResourceManager.GetString("YubiKeyOperationFailed", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Error generating key pair: {0}.
+ ///
+ internal static string GenerateKeyPairFailed {
+ get {
+ return ResourceManager.GetString("GenerateKeyPairFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Certificate data too short to determine compression format..
+ ///
+ internal static string CertificateDataTooShortToDetectFormat {
+ get {
+ return ResourceManager.GetString("CertificateDataTooShortToDetectFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Could not detect compression format..
+ ///
+ internal static string CouldNotDetectCompressionFormat {
+ get {
+ return ResourceManager.GetString("CouldNotDetectCompressionFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Decompressed data length {0} does not match expected length {1} from GIDS header..
+ ///
+ internal static string DecompressedLengthMismatch {
+ get {
+ return ResourceManager.GetString("DecompressedLengthMismatch", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The stream does not support writing..
+ ///
+ internal static string StreamDoesNotSupportWriting {
+ get {
+ return ResourceManager.GetString("StreamDoesNotSupportWriting", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The stream does not support reading..
+ ///
+ internal static string StreamDoesNotSupportReading {
+ get {
+ return ResourceManager.GetString("StreamDoesNotSupportReading", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Invalid CompressionMode value..
+ ///
+ internal static string InvalidCompressionModeValue {
+ get {
+ return ResourceManager.GetString("InvalidCompressionModeValue", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Reading is not supported on compression streams..
+ ///
+ internal static string ReadingNotSupportedOnCompressionStreams {
+ get {
+ return ResourceManager.GetString("ReadingNotSupportedOnCompressionStreams", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Writing is not supported on decompression streams..
+ ///
+ internal static string WritingNotSupportedOnDecompressionStreams {
+ get {
+ return ResourceManager.GetString("WritingNotSupportedOnDecompressionStreams", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to CopyTo is not supported on compression streams..
+ ///
+ internal static string CopyToNotSupportedOnCompressionStreams {
+ get {
+ return ResourceManager.GetString("CopyToNotSupportedOnCompressionStreams", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to CopyToAsync is not supported on compression streams..
+ ///
+ internal static string CopyToAsyncNotSupportedOnCompressionStreams {
+ get {
+ return ResourceManager.GetString("CopyToAsyncNotSupportedOnCompressionStreams", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unexpected end of stream while reading zlib header..
+ ///
+ internal static string UnexpectedEndOfZlibHeader {
+ get {
+ return ResourceManager.GetString("UnexpectedEndOfZlibHeader", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Invalid zlib header checksum..
+ ///
+ internal static string InvalidZlibHeaderChecksum {
+ get {
+ return ResourceManager.GetString("InvalidZlibHeaderChecksum", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unsupported zlib compression method: {0}. Only deflate (8) is supported..
+ ///
+ internal static string UnsupportedZlibCompressionMethod {
+ get {
+ return ResourceManager.GetString("UnsupportedZlibCompressionMethod", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Zlib streams with a preset dictionary are not supported..
+ ///
+ internal static string ZlibPresetDictionaryNotSupported {
+ get {
+ return ResourceManager.GetString("ZlibPresetDictionaryNotSupported", resourceCulture);
+ }
+ }
}
}
diff --git a/Yubico.YubiKey/src/Resources/ExceptionMessages.resx b/Yubico.YubiKey/src/Resources/ExceptionMessages.resx
index fcee6f050..3b6911e76 100644
--- a/Yubico.YubiKey/src/Resources/ExceptionMessages.resx
+++ b/Yubico.YubiKey/src/Resources/ExceptionMessages.resx
@@ -916,4 +916,49 @@
Key agreement receipts do not match
+
+ Error generating key pair: {0}
+
+
+ Certificate data too short to determine compression format.
+
+
+ Could not detect compression format.
+
+
+ Decompressed data length {0} does not match expected length {1} from GIDS header.
+
+
+ The stream does not support writing.
+
+
+ The stream does not support reading.
+
+
+ Invalid CompressionMode value.
+
+
+ Reading is not supported on compression streams.
+
+
+ Writing is not supported on decompression streams.
+
+
+ CopyTo is not supported on compression streams.
+
+
+ CopyToAsync is not supported on compression streams.
+
+
+ Unexpected end of stream while reading zlib header.
+
+
+ Invalid zlib header checksum.
+
+
+ Unsupported zlib compression method: {0}. Only deflate (8) is supported.
+
+
+ Zlib streams with a preset dictionary are not supported.
+
\ No newline at end of file
diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ZLibStream.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ZLibStream.cs
new file mode 100644
index 000000000..bda4904be
--- /dev/null
+++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ZLibStream.cs
@@ -0,0 +1,598 @@
+// Copyright 2025 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This implementation is based on RFC 1950 (ZLIB Compressed Data Format Specification).
+// It handles the zlib framing (header + Adler-32 trailer) around raw deflate data,
+// delegating the actual inflate/deflate work to the standard System.IO.Compression.DeflateStream.
+
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Yubico.YubiKey.Cryptography
+{
+ ///
+ /// Provides methods and properties used to compress and decompress streams by
+ /// using the zlib data format specification (RFC 1950).
+ ///
+ ///
+ ///
+ /// The zlib format wraps raw DEFLATE compressed data with a 2-byte header
+ /// (CMF and FLG) and a 4-byte Adler-32 checksum trailer. This class handles
+ /// the framing and delegates the actual compression/decompression to
+ /// .
+ ///
+ ///
+ /// During compression, an Adler-32 checksum of all written bytes is
+ /// computed and appended as a 4-byte big-endian trailer when the stream is
+ /// disposed, producing a fully RFC 1950-compliant zlib stream.
+ ///
+ ///
+ /// During decompression, the 2-byte zlib header is validated (checksum,
+ /// compression method, and FDICT flag), but the 4-byte Adler-32 trailer is
+ /// not verified. Corruption that is not caught by the underlying DEFLATE
+ /// decoder will go undetected.
+ ///
+ ///
+ /// This implementation targets .NET Standard 2.0 / 2.1 / .NET Framework 4.7.2
+ /// where System.IO.Compression.ZLibStream is not available.
+ ///
+ ///
+ internal sealed class ZLibStream : Stream
+ {
+ ///
+ /// The default zlib CMF byte: deflate method (CM=8), window size 2^15 (CINFO=7).
+ ///
+ private const byte DefaultCmf = 0x78;
+
+ ///
+ /// The FLG byte for default compression level.
+ /// Chosen so that (DefaultCmf * 256 + DefaultFlg) % 31 == 0.
+ ///
+ private const byte DefaultFlg = 0x9C;
+
+ private readonly CompressionMode _mode;
+ private readonly bool _leaveOpen;
+ private DeflateStream? _deflateStream;
+ private bool _headerProcessed;
+ private bool _disposed;
+
+ // For compression: tracks written data for Adler-32 computation
+ private uint _adlerA = 1;
+ private uint _adlerB;
+
+ ///
+ /// Initializes a new instance of the class by using the
+ /// specified stream and compression mode.
+ ///
+ /// The stream to which compressed data is written or from
+ /// which data to decompress is read.
+ /// One of the enumeration values that indicates whether to
+ /// compress data to the stream or decompress data from the stream.
+ public ZLibStream(Stream stream, CompressionMode mode)
+ : this(stream, mode, leaveOpen: false)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class by using the
+ /// specified stream, compression mode, and whether to leave the stream open.
+ ///
+ /// The stream to which compressed data is written or from
+ /// which data to decompress is read.
+ /// One of the enumeration values that indicates whether to
+ /// compress data to the stream or decompress data from the stream.
+ /// to leave the stream object open
+ /// after disposing the object; otherwise,
+ /// .
+ /// is
+ /// .
+ /// is
+ /// and the stream does not support
+ /// reading, or is
+ /// and the stream does not support writing.
+ public ZLibStream(Stream stream, CompressionMode mode, bool leaveOpen)
+ {
+ BaseStream = stream ?? throw new ArgumentNullException(nameof(stream));
+ _mode = mode;
+ _leaveOpen = leaveOpen;
+
+ if (mode == CompressionMode.Compress)
+ {
+ if (!stream.CanWrite)
+ {
+ throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportWriting, nameof(stream));
+ }
+ }
+ else if (mode == CompressionMode.Decompress)
+ {
+ if (!stream.CanRead)
+ {
+ throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportReading, nameof(stream));
+ }
+ }
+ else
+ {
+ throw new ArgumentException(ExceptionMessages.InvalidCompressionModeValue, nameof(mode));
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class by using the
+ /// specified stream and compression level.
+ ///
+ /// The stream to which compressed data is written.
+ /// One of the enumeration values that indicates
+ /// whether to emphasize speed or compression efficiency when compressing data.
+ public ZLibStream(Stream stream, CompressionLevel compressionLevel)
+ : this(stream, compressionLevel, leaveOpen: false)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class by using the
+ /// specified stream, compression level, and whether to leave the stream open.
+ ///
+ /// The stream to which compressed data is written.
+ /// One of the enumeration values that indicates
+ /// whether to emphasize speed or compression efficiency when compressing data.
+ /// to leave the stream object open
+ /// after disposing the object; otherwise,
+ /// .
+ public ZLibStream(Stream stream, CompressionLevel compressionLevel, bool leaveOpen)
+ {
+ BaseStream = stream ?? throw new ArgumentNullException(nameof(stream));
+
+ if (!stream.CanWrite)
+ {
+ throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportWriting, nameof(stream));
+ }
+
+ _mode = CompressionMode.Compress;
+ _leaveOpen = leaveOpen;
+
+ // Write the zlib header immediately
+ WriteZLibHeader(compressionLevel);
+
+ _deflateStream = new DeflateStream(stream, compressionLevel, leaveOpen: true);
+ _headerProcessed = true;
+ }
+
+ ///
+ public override bool CanRead => !_disposed && _mode == CompressionMode.Decompress;
+
+ ///
+ public override bool CanWrite => !_disposed && _mode == CompressionMode.Compress;
+
+ ///
+ public override bool CanSeek => false;
+
+ ///
+ public override long Length => throw new NotSupportedException();
+
+ ///
+ public override long Position
+ {
+ get => throw new NotSupportedException();
+ set => throw new NotSupportedException();
+ }
+
+ ///
+ /// Gets a reference to the underlying stream.
+ ///
+ public Stream BaseStream { get; }
+
+ ///
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ ThrowIfDisposed();
+
+ if (_mode != CompressionMode.Decompress)
+ {
+ throw new InvalidOperationException(ExceptionMessages.ReadingNotSupportedOnCompressionStreams);
+ }
+
+ EnsureDecompressionInitialized();
+
+ return _deflateStream!.Read(buffer, offset, count);
+ }
+
+ ///
+ public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ ThrowIfDisposed();
+
+ if (_mode != CompressionMode.Decompress)
+ {
+ throw new InvalidOperationException(ExceptionMessages.ReadingNotSupportedOnCompressionStreams);
+ }
+
+ EnsureDecompressionInitialized();
+
+ return _deflateStream!.ReadAsync(buffer, offset, count, cancellationToken);
+ }
+
+ ///
+ public override int ReadByte()
+ {
+ ThrowIfDisposed();
+
+ if (_mode != CompressionMode.Decompress)
+ {
+ throw new InvalidOperationException(ExceptionMessages.ReadingNotSupportedOnCompressionStreams);
+ }
+
+ EnsureDecompressionInitialized();
+
+ return _deflateStream!.ReadByte();
+ }
+
+ ///
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ ThrowIfDisposed();
+
+ if (_mode != CompressionMode.Compress)
+ {
+ throw new InvalidOperationException(ExceptionMessages.WritingNotSupportedOnDecompressionStreams);
+ }
+
+ EnsureCompressionInitialized();
+
+ // Track uncompressed data for Adler-32
+ UpdateAdler32(buffer, offset, count);
+
+ _deflateStream!.Write(buffer, offset, count);
+ }
+
+ ///
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ ThrowIfDisposed();
+
+ if (_mode != CompressionMode.Compress)
+ {
+ throw new InvalidOperationException(ExceptionMessages.WritingNotSupportedOnDecompressionStreams);
+ }
+
+ EnsureCompressionInitialized();
+
+ // Track uncompressed data for Adler-32
+ UpdateAdler32(buffer, offset, count);
+
+ return _deflateStream!.WriteAsync(buffer, offset, count, cancellationToken);
+ }
+
+#if NETSTANDARD2_1_OR_GREATER
+ ///
+ public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default)
+ {
+ ThrowIfDisposed();
+
+ if (_mode != CompressionMode.Decompress)
+ {
+ throw new InvalidOperationException(ExceptionMessages.ReadingNotSupportedOnCompressionStreams);
+ }
+
+ EnsureDecompressionInitialized();
+
+ return _deflateStream!.ReadAsync(buffer, cancellationToken);
+ }
+
+ ///
+ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default)
+ {
+ ThrowIfDisposed();
+
+ if (_mode != CompressionMode.Compress)
+ {
+ throw new InvalidOperationException(ExceptionMessages.WritingNotSupportedOnDecompressionStreams);
+ }
+
+ EnsureCompressionInitialized();
+
+ // Track uncompressed data for Adler-32
+ if (!buffer.IsEmpty)
+ {
+ byte[] temp = buffer.ToArray();
+ UpdateAdler32(temp, 0, temp.Length);
+ }
+
+ return _deflateStream!.WriteAsync(buffer, cancellationToken);
+ }
+#endif
+
+ ///
+ public override void Flush()
+ {
+ ThrowIfDisposed();
+ _deflateStream?.Flush();
+ }
+
+ ///
+ public override Task FlushAsync(CancellationToken cancellationToken)
+ {
+ ThrowIfDisposed();
+
+ if (_deflateStream != null)
+ {
+ return _deflateStream.FlushAsync(cancellationToken);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public override long Seek(long offset, SeekOrigin origin) =>
+ throw new NotSupportedException();
+
+ ///
+ public override void SetLength(long value) =>
+ throw new NotSupportedException();
+
+#if NETSTANDARD2_1_OR_GREATER
+ ///
+ public override void CopyTo(Stream destination, int bufferSize)
+ {
+ ThrowIfDisposed();
+
+ if (_mode != CompressionMode.Decompress)
+ {
+ throw new InvalidOperationException(ExceptionMessages.CopyToNotSupportedOnCompressionStreams);
+ }
+
+ EnsureDecompressionInitialized();
+
+ _deflateStream!.CopyTo(destination, bufferSize);
+ }
+#endif
+
+ ///
+ public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
+ {
+ ThrowIfDisposed();
+
+ if (_mode != CompressionMode.Decompress)
+ {
+ throw new InvalidOperationException(ExceptionMessages.CopyToAsyncNotSupportedOnCompressionStreams);
+ }
+
+ EnsureDecompressionInitialized();
+
+ return _deflateStream!.CopyToAsync(destination, bufferSize, cancellationToken);
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ if (_mode == CompressionMode.Compress && _deflateStream != null)
+ {
+ // Flush and close the deflate stream to finalize compressed data
+ _deflateStream.Dispose();
+ _deflateStream = null;
+
+ // Write the Adler-32 checksum trailer (big-endian)
+ WriteAdler32Trailer();
+ }
+ else
+ {
+ _deflateStream?.Dispose();
+ _deflateStream = null;
+ }
+
+ if (!_leaveOpen)
+ {
+ BaseStream.Dispose();
+ }
+ }
+
+ _disposed = true;
+ }
+
+ base.Dispose(disposing);
+ }
+
+ ///
+ /// Reads and validates the 2-byte zlib header (RFC 1950 section 2.2).
+ /// After validation, creates the internal
+ /// positioned at the start of the raw deflate data.
+ ///
+ /// The zlib header is invalid.
+ private void ReadAndValidateZLibHeader()
+ {
+ int cmf = BaseStream.ReadByte();
+ int flg = BaseStream.ReadByte();
+
+ if (cmf == -1 || flg == -1)
+ {
+ throw new InvalidDataException(ExceptionMessages.UnexpectedEndOfZlibHeader);
+ }
+
+ // Validate the header checksum: (CMF * 256 + FLG) must be divisible by 31
+ if (((cmf * 256) + flg) % 31 != 0)
+ {
+ throw new InvalidDataException(ExceptionMessages.InvalidZlibHeaderChecksum);
+ }
+
+ // Extract compression method (lower 4 bits of CMF)
+ int compressionMethod = cmf & 0x0F;
+ if (compressionMethod != 8)
+ {
+ throw new InvalidDataException(
+ string.Format(
+ System.Globalization.CultureInfo.CurrentCulture,
+ ExceptionMessages.UnsupportedZlibCompressionMethod,
+ compressionMethod));
+ }
+
+ // Check FDICT flag (bit 5 of FLG) - preset dictionary not supported
+ bool hasPresetDictionary = (flg & 0x20) != 0;
+ if (hasPresetDictionary)
+ {
+ throw new InvalidDataException(ExceptionMessages.ZlibPresetDictionaryNotSupported);
+ }
+ }
+
+ ///
+ /// Writes the 2-byte zlib header to the base stream.
+ ///
+ private void WriteZLibHeader(CompressionLevel compressionLevel)
+ {
+ byte cmf = DefaultCmf;
+ byte flg;
+
+ // Choose FLEVEL based on compression level and ensure header checksum is valid
+ switch (compressionLevel)
+ {
+ case CompressionLevel.NoCompression:
+ // FLEVEL = 0 (compressor used fastest algorithm)
+ flg = ComputeFlg(cmf, 0);
+ break;
+ case CompressionLevel.Fastest:
+ // FLEVEL = 1 (compressor used fast algorithm)
+ flg = ComputeFlg(cmf, 1);
+ break;
+ default:
+ // FLEVEL = 2 (default) - covers Optimal and SmallestSize
+ flg = DefaultFlg;
+ break;
+ }
+
+ BaseStream.WriteByte(cmf);
+ BaseStream.WriteByte(flg);
+ }
+
+ ///
+ /// Computes the FLG byte given a CMF byte and desired FLEVEL (0-3).
+ /// Ensures that (CMF * 256 + FLG) % 31 == 0 per RFC 1950.
+ ///
+ private static byte ComputeFlg(byte cmf, int flevel)
+ {
+ // FLG layout: FLEVEL (2 bits) | FDICT (1 bit, 0) | FCHECK (5 bits)
+ int flgBase = (flevel & 0x03) << 6;
+ int remainder = ((cmf * 256) + flgBase) % 31;
+ int fcheck = (31 - remainder) % 31;
+
+ return (byte)(flgBase | fcheck);
+ }
+
+ ///
+ /// Writes the 4-byte Adler-32 checksum trailer in big-endian byte order.
+ ///
+ private void WriteAdler32Trailer()
+ {
+ uint checksum = (_adlerB << 16) | _adlerA;
+
+ BaseStream.WriteByte((byte)(checksum >> 24));
+ BaseStream.WriteByte((byte)(checksum >> 16));
+ BaseStream.WriteByte((byte)(checksum >> 8));
+ BaseStream.WriteByte((byte)checksum);
+ }
+
+ ///
+ /// Updates the running Adler-32 checksum with the given data.
+ ///
+ ///
+ /// Adler-32 is defined in RFC 1950 section 9. It consists of two 16-bit
+ /// checksums A and B: A = 1 + sum of all bytes, B = sum of all A values,
+ /// both modulo 65521.
+ ///
+ private void UpdateAdler32(byte[] buffer, int offset, int count)
+ {
+ const uint modAdler = 65521;
+
+ for (int i = offset; i < offset + count; i++)
+ {
+ _adlerA = (_adlerA + buffer[i]) % modAdler;
+ _adlerB = (_adlerB + _adlerA) % modAdler;
+ }
+ }
+
+ ///
+ /// Ensures the zlib header has been read and the internal DeflateStream
+ /// is initialized for decompression.
+ ///
+ private void EnsureDecompressionInitialized()
+ {
+ if (!_headerProcessed)
+ {
+ ReadAndValidateZLibHeader();
+ _deflateStream = new DeflateStream(BaseStream, CompressionMode.Decompress, leaveOpen: true);
+ _headerProcessed = true;
+ }
+ }
+
+ ///
+ /// Ensures the zlib header has been written and the internal DeflateStream
+ /// is initialized for compression.
+ ///
+ private void EnsureCompressionInitialized()
+ {
+ if (!_headerProcessed)
+ {
+ // Default compression level header
+ BaseStream.WriteByte(DefaultCmf);
+ BaseStream.WriteByte(DefaultFlg);
+ _deflateStream = new DeflateStream(BaseStream, CompressionLevel.Optimal, leaveOpen: true);
+ _headerProcessed = true;
+ }
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(GetType().FullName);
+ }
+ }
+
+ ///
+ /// Computes the Adler-32 checksum over an entire byte array.
+ ///
+ /// The data to compute the checksum for.
+ /// The 32-bit Adler-32 checksum value.
+ internal static uint ComputeAdler32(byte[] data)
+ {
+ return ComputeAdler32(data, 0, data.Length);
+ }
+
+ ///
+ /// Computes the Adler-32 checksum over a segment of a byte array.
+ ///
+ /// The data to compute the checksum for.
+ /// The offset into the data to start from.
+ /// The number of bytes to include.
+ /// The 32-bit Adler-32 checksum value.
+ internal static uint ComputeAdler32(byte[] data, int offset, int count)
+ {
+ const uint modAdler = 65521;
+ uint a = 1;
+ uint b = 0;
+
+ for (int i = offset; i < offset + count; i++)
+ {
+ a = (a + data[i]) % modAdler;
+ b = (b + a) % modAdler;
+ }
+
+ return (b << 16) | a;
+ }
+ }
+}
diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.KeyPairs.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.KeyPairs.cs
index 1a7decc40..b06cddcda 100644
--- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.KeyPairs.cs
+++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.KeyPairs.cs
@@ -176,7 +176,11 @@ public IPublicKey GenerateKeyPair(
if (response.Status != ResponseStatus.Success)
{
- throw new InvalidOperationException("Error generating key pair: " + response);
+ throw new InvalidOperationException(
+ string.Format(
+ CultureInfo.CurrentCulture,
+ ExceptionMessages.GenerateKeyPairFailed,
+ response));
}
return PivKeyDecoder.CreatePublicKey(response.Data, keyType);
@@ -605,14 +609,16 @@ public X509Certificate2 GetCertificate(byte slotNumber)
try
{
- return new X509Certificate2(Decompress(certBytesCopy));
+ byte[] decompressedData = DecompressWithFormatDetection(certBytesCopy);
+ return new X509Certificate2(decompressedData);
}
- catch (Exception)
+ catch (Exception ex)
{
throw new InvalidOperationException(
string.Format(
CultureInfo.CurrentCulture,
- ExceptionMessages.FailedDecompressingCertificate));
+ ExceptionMessages.FailedDecompressingCertificate),
+ ex);
}
}
@@ -661,14 +667,113 @@ static private byte[] Compress(byte[] data)
return compressedStream.ToArray();
}
- static private byte[] Decompress(byte[] data)
+ static private byte[] Decompress(byte[] data, int offset = 0)
{
- using var dataStream = new MemoryStream(data);
- using var decompressor = new GZipStream(dataStream, CompressionMode.Decompress);
- using var decompressedStream = new MemoryStream();
- decompressor.CopyTo(decompressedStream);
-
- return decompressedStream.ToArray();
+ using (var dataStream = new MemoryStream(data, offset, data.Length - offset))
+ {
+ using (var decompressor = new GZipStream(dataStream, CompressionMode.Decompress))
+ {
+ using (var decompressedStream = new MemoryStream())
+ {
+ decompressor.CopyTo(decompressedStream);
+ return decompressedStream.ToArray();
+ }
+ }
+ }
+ }
+
+ ///
+ /// Decompresses a certificate by detecting the compression format.
+ ///
+ ///
+ ///
+ /// Attempts to decompress using the following formats in order of detection:
+ ///
+ ///
+ /// - GZip (magic bytes 0x1F, 0x8B) — as specified by the PIV standard for
+ /// compressed certificates.
+ /// - GIDS (magic bytes 0x01, 0x00 + 2-byte LE uncompressed length)
+ /// followed by zlib (RFC 1950) compressed data, as used by the GIDS smartcard
+ /// standard.
+ ///
+ ///
+ /// If none of the above formats are detected, throws an exception.
+ ///
+ ///
+ static private byte[] DecompressWithFormatDetection(byte[] data)
+ {
+ if (data.Length < 2)
+ {
+ throw new InvalidOperationException(ExceptionMessages.CertificateDataTooShortToDetectFormat);
+ }
+
+ // Check for GZip magic bytes (0x1F, 0x8B)
+ if (data[0] == 0x1F && data[1] == 0x8B)
+ {
+ return Decompress(data);
+ }
+
+ // Check for GIDS header (0x01, 0x00) followed by 2-byte LE length and zlib payload
+ if (data[0] == 0x01 && data[1] == 0x00 && data.Length >= 6)
+ {
+ return DecompressGids(data);
+ }
+
+ throw new InvalidOperationException(ExceptionMessages.CouldNotDetectCompressionFormat);
+ }
+
+ ///
+ /// Decompresses GIDS-formatted data.
+ ///
+ ///
+ ///
+ /// The GIDS format uses a 4-byte header:
+ ///
+ ///
+ /// - Bytes 0–1: Magic prefix (0x01, 0x00).
+ /// - Bytes 2–3: Expected uncompressed data length in little-endian byte order.
+ ///
+ ///
+ /// After the 4-byte header, the payload is zlib (RFC 1950) compressed data.
+ /// The decompressed length is validated against the expected length from the header.
+ ///
+ ///
+ static private byte[] DecompressGids(byte[] data)
+ {
+ const int gidsHeaderLength = 4;
+
+ int expectedLength = data[2] | (data[3] << 8);
+ byte[] decompressed = DecompressZlib(data, offset: gidsHeaderLength);
+
+ if (decompressed.Length != expectedLength)
+ {
+ throw new InvalidOperationException(
+ string.Format(
+ CultureInfo.CurrentCulture,
+ ExceptionMessages.DecompressedLengthMismatch,
+ decompressed.Length,
+ expectedLength));
+ }
+
+ return decompressed;
+ }
+
+ ///
+ /// Decompresses zlib (RFC 1950) data starting at the specified offset.
+ ///
+ static private byte[] DecompressZlib(byte[] data, int offset = 0)
+ {
+ using (var dataStream = new MemoryStream(data, offset, data.Length - offset))
+ {
+ using (var decompressor = new ZLibStream(dataStream, CompressionMode.Decompress))
+ {
+ using (var decompressedStream = new MemoryStream())
+ {
+ decompressor.CopyTo(decompressedStream);
+ return decompressedStream.ToArray();
+ }
+ }
+ }
}
}
}
diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ZLibStreamTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ZLibStreamTests.cs
new file mode 100644
index 000000000..e7e73d01e
--- /dev/null
+++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ZLibStreamTests.cs
@@ -0,0 +1,574 @@
+// Copyright 2025 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Text;
+using Xunit;
+
+namespace Yubico.YubiKey.Cryptography
+{
+ public class ZLibStreamTests
+ {
+ // "Hello, World!" compressed with zlib (RFC 1950).
+ private static readonly byte[] ZLibCompressedHelloWorld =
+ {
+ 0x78, 0x9C, 0xF3, 0x48, 0xCD, 0xC9, 0xC9, 0xD7,
+ 0x51, 0x08, 0xCF, 0x2F, 0xCA, 0x49, 0x51, 0x04,
+ 0x00, 0x20, 0x5E, 0x04, 0x8A
+ };
+ private const string HelloWorldText = "Hello, World!";
+
+ [Fact]
+ public void Decompress_ValidZLibData_ReturnsOriginalData()
+ {
+ // Pure zlib (RFC 1950) data without any prefix.
+ string hex = "789c8b2c4dcaf4ce2c5148cb2f5270cc4b29cacf4c5128492d2e5148492c4904009f2e0aa4";
+
+ byte[] data = Convert.FromHexString(hex);
+
+ using var compressedStream = new MemoryStream(data);
+ using var zlibStream = new ZLibStream(compressedStream, CompressionMode.Decompress);
+ using var resultStream = new MemoryStream();
+
+ zlibStream.CopyTo(resultStream);
+ string result = Encoding.UTF8.GetString(resultStream.ToArray());
+
+ Assert.Equal("YubiKit for Android test data", result);
+ }
+
+ [Fact]
+ public void Decompress_GidsFormat_StripsHeaderAndDecompresses()
+ {
+ // GIDS format: 4-byte header (01 00 = magic, 1D 00 = LE uncompressed length 29)
+ // followed by standard zlib (RFC 1950) data.
+ string hex = "01001d00789c8b2c4dcaf4ce2c5148cb2f5270cc4b29cacf4c5128492d2e5148492c4904009f2e0aa4";
+
+ byte[] data = Convert.FromHexString(hex);
+
+ // Strip 4-byte GIDS header, then decompress the zlib payload
+ const int gidsHeaderLength = 4;
+ using var compressedStream = new MemoryStream(data, gidsHeaderLength, data.Length - gidsHeaderLength);
+ using var zlibStream = new ZLibStream(compressedStream, CompressionMode.Decompress);
+ using var resultStream = new MemoryStream();
+
+ zlibStream.CopyTo(resultStream);
+ string result = Encoding.UTF8.GetString(resultStream.ToArray());
+
+ Assert.Equal("YubiKit for Android test data", result);
+ }
+
+ [Fact]
+ public void Decompress_ReadByteArray_ReturnsOriginalData()
+ {
+ using var compressedStream = new MemoryStream(ZLibCompressedHelloWorld);
+ using var zlibStream = new ZLibStream(compressedStream, CompressionMode.Decompress);
+
+ byte[] buffer = new byte[256];
+ int totalRead = 0;
+ int bytesRead;
+
+ while ((bytesRead = zlibStream.Read(buffer, totalRead, buffer.Length - totalRead)) > 0)
+ {
+ totalRead += bytesRead;
+ }
+
+ string result = Encoding.UTF8.GetString(buffer, 0, totalRead);
+ Assert.Equal(HelloWorldText, result);
+ }
+
+ [Fact]
+ public void Decompress_ReadByte_ReturnsCorrectFirstByte()
+ {
+ using var compressedStream = new MemoryStream(ZLibCompressedHelloWorld);
+ using var zlibStream = new ZLibStream(compressedStream, CompressionMode.Decompress);
+
+ int firstByte = zlibStream.ReadByte();
+
+ Assert.Equal((int)'H', firstByte);
+ }
+
+ [Fact]
+ public void Compress_ThenDecompress_RoundTrips()
+ {
+ byte[] original = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog.");
+
+ // Compress
+ byte[] compressed;
+ using (var compressedStream = new MemoryStream())
+ {
+ using (var zlibStream = new ZLibStream(compressedStream, CompressionLevel.Optimal, leaveOpen: true))
+ {
+ zlibStream.Write(original, 0, original.Length);
+ }
+
+ compressed = compressedStream.ToArray();
+ }
+
+ // Verify zlib header is present
+ Assert.Equal(0x78, compressed[0]);
+
+ // Decompress
+ byte[] decompressed;
+ using (var compressedStream = new MemoryStream(compressed))
+ {
+ using (var zlibStream = new ZLibStream(compressedStream, CompressionMode.Decompress))
+ {
+ using (var resultStream = new MemoryStream())
+ {
+ zlibStream.CopyTo(resultStream);
+ decompressed = resultStream.ToArray();
+ }
+ }
+ }
+
+ Assert.Equal(original, decompressed);
+ }
+
+ [Fact]
+ public void Compress_EmptyData_RoundTrips()
+ {
+ byte[] original = Array.Empty();
+
+ // Compress
+ byte[] compressed;
+ using (var compressedStream = new MemoryStream())
+ {
+ using (var zlibStream = new ZLibStream(compressedStream, CompressionLevel.Optimal, leaveOpen: true))
+ {
+ zlibStream.Write(original, 0, original.Length);
+ }
+
+ compressed = compressedStream.ToArray();
+ }
+
+ // Decompress
+ byte[] decompressed;
+ using (var compressedStream = new MemoryStream(compressed))
+ {
+ using (var zlibStream = new ZLibStream(compressedStream, CompressionMode.Decompress))
+ {
+ using (var resultStream = new MemoryStream())
+ {
+ zlibStream.CopyTo(resultStream);
+ decompressed = resultStream.ToArray();
+ }
+ }
+ }
+
+ Assert.Equal(original, decompressed);
+ }
+
+ [Fact]
+ public void Compress_LargeData_RoundTrips()
+ {
+ // Create a large repetitive payload (~10KB)
+ var sb = new StringBuilder();
+ for (int i = 0; i < 500; i++)
+ {
+ sb.AppendLine($"Line {i}: The quick brown fox jumps over the lazy dog.");
+ }
+
+ byte[] original = Encoding.UTF8.GetBytes(sb.ToString());
+
+ // Compress
+ byte[] compressed;
+ using (var compressedStream = new MemoryStream())
+ {
+ using (var zlibStream = new ZLibStream(compressedStream, CompressionLevel.Optimal, leaveOpen: true))
+ {
+ zlibStream.Write(original, 0, original.Length);
+ }
+
+ compressed = compressedStream.ToArray();
+ }
+
+ // Should actually be smaller due to repetition
+ Assert.True(compressed.Length < original.Length);
+
+ // Decompress
+ byte[] decompressed;
+ using (var compressedStream = new MemoryStream(compressed))
+ {
+ using (var zlibStream = new ZLibStream(compressedStream, CompressionMode.Decompress))
+ {
+ using (var resultStream = new MemoryStream())
+ {
+ zlibStream.CopyTo(resultStream);
+ decompressed = resultStream.ToArray();
+ }
+ }
+ }
+
+ Assert.Equal(original, decompressed);
+ }
+
+ [Fact]
+ public void Decompress_InvalidHeader_ThrowsInvalidDataException()
+ {
+ // Invalid zlib header — checksum fails
+ byte[] invalidData = { 0x78, 0x00, 0x00, 0x00 };
+
+ using var stream = new MemoryStream(invalidData);
+ using var zlibStream = new ZLibStream(stream, CompressionMode.Decompress);
+
+ Assert.Throws(() => zlibStream.ReadByte());
+ }
+
+ [Fact]
+ public void Decompress_NonDeflateCompressionMethod_ThrowsInvalidDataException()
+ {
+ // CMF = 0x09 means compression method 9 (not deflate)
+ // FLG must satisfy (CMF * 256 + FLG) % 31 == 0
+ // 0x09 * 256 = 2304, 2304 % 31 = 10, so FLG = 31 - 10 = 21 = 0x15
+ byte[] invalidData = { 0x09, 0x15, 0x00, 0x00 };
+
+ using var stream = new MemoryStream(invalidData);
+ using var zlibStream = new ZLibStream(stream, CompressionMode.Decompress);
+
+ Assert.Throws(() => zlibStream.ReadByte());
+ }
+
+ [Fact]
+ public void Decompress_TruncatedHeader_ThrowsInvalidDataException()
+ {
+ byte[] truncatedData = { 0x78 };
+
+ using var stream = new MemoryStream(truncatedData);
+ using var zlibStream = new ZLibStream(stream, CompressionMode.Decompress);
+
+ Assert.Throws(() => zlibStream.ReadByte());
+ }
+
+ [Fact]
+ public void Constructor_NullStream_ThrowsArgumentNullException()
+ {
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ Assert.Throws(() => new ZLibStream(null, CompressionMode.Decompress));
+#pragma warning restore CS8625
+ }
+
+ [Fact]
+ public void CanRead_DecompressMode_ReturnsTrue()
+ {
+ using var stream = new MemoryStream(ZLibCompressedHelloWorld);
+ using var zlibStream = new ZLibStream(stream, CompressionMode.Decompress);
+
+ Assert.True(zlibStream.CanRead);
+ Assert.False(zlibStream.CanWrite);
+ Assert.False(zlibStream.CanSeek);
+ }
+
+ [Fact]
+ public void CanWrite_CompressMode_ReturnsTrue()
+ {
+ using var stream = new MemoryStream();
+ using var zlibStream = new ZLibStream(stream, CompressionMode.Compress);
+
+ Assert.True(zlibStream.CanWrite);
+ Assert.False(zlibStream.CanRead);
+ Assert.False(zlibStream.CanSeek);
+ }
+
+ [Fact]
+ public void Write_InDecompressMode_ThrowsInvalidOperationException()
+ {
+ using var stream = new MemoryStream(ZLibCompressedHelloWorld);
+ using var zlibStream = new ZLibStream(stream, CompressionMode.Decompress);
+
+ Assert.Throws(() => zlibStream.Write(new byte[] { 1 }, 0, 1));
+ }
+
+ [Fact]
+ public void Read_InCompressMode_ThrowsInvalidOperationException()
+ {
+ using var stream = new MemoryStream();
+ using var zlibStream = new ZLibStream(stream, CompressionMode.Compress);
+
+ Assert.Throws(() => zlibStream.Read(new byte[1], 0, 1));
+ }
+
+ [Fact]
+ public void Seek_ThrowsNotSupportedException()
+ {
+ using var stream = new MemoryStream(ZLibCompressedHelloWorld);
+ using var zlibStream = new ZLibStream(stream, CompressionMode.Decompress);
+
+ Assert.Throws(() => zlibStream.Seek(0, SeekOrigin.Begin));
+ }
+
+ [Fact]
+ public void Length_ThrowsNotSupportedException()
+ {
+ using var stream = new MemoryStream(ZLibCompressedHelloWorld);
+ using var zlibStream = new ZLibStream(stream, CompressionMode.Decompress);
+
+ Assert.Throws(() => _ = zlibStream.Length);
+ }
+
+ [Fact]
+ public void Dispose_ThenRead_ThrowsObjectDisposedException()
+ {
+ var stream = new MemoryStream(ZLibCompressedHelloWorld);
+ var zlibStream = new ZLibStream(stream, CompressionMode.Decompress);
+
+ zlibStream.Dispose();
+
+ Assert.Throws(() => zlibStream.Read(new byte[1], 0, 1));
+ }
+
+ [Fact]
+ public void LeaveOpen_True_DoesNotDisposeBaseStream()
+ {
+ var stream = new MemoryStream(ZLibCompressedHelloWorld);
+ var zlibStream = new ZLibStream(stream, CompressionMode.Decompress, leaveOpen: true);
+
+ zlibStream.Dispose();
+
+ // Stream should still be accessible
+ Assert.True(stream.CanRead);
+ }
+
+ [Fact]
+ public void LeaveOpen_False_DisposesBaseStream()
+ {
+ var stream = new MemoryStream(ZLibCompressedHelloWorld);
+ var zlibStream = new ZLibStream(stream, CompressionMode.Decompress, leaveOpen: false);
+
+ zlibStream.Dispose();
+
+ // Stream should be disposed
+ Assert.False(stream.CanRead);
+ }
+
+ [Fact]
+ public void BaseStream_ReturnsUnderlyingStream()
+ {
+ var stream = new MemoryStream(ZLibCompressedHelloWorld);
+ using var zlibStream = new ZLibStream(stream, CompressionMode.Decompress);
+
+ Assert.Same(stream, zlibStream.BaseStream);
+ }
+
+ [Fact]
+ public void CompressedOutput_HasValidZLibHeader()
+ {
+ byte[] compressed;
+ using (var output = new MemoryStream())
+ {
+ using (var zlibStream = new ZLibStream(output, CompressionLevel.Optimal, leaveOpen: true))
+ {
+ byte[] data = Encoding.UTF8.GetBytes("test");
+ zlibStream.Write(data, 0, data.Length);
+ }
+
+ compressed = output.ToArray();
+ }
+
+ // Verify CMF byte: deflate (method 8), window size 15 (CINFO 7)
+ Assert.Equal(0x78, compressed[0]);
+
+ // Verify header checksum: (CMF * 256 + FLG) % 31 == 0
+ int headerCheck = (compressed[0] * 256) + compressed[1];
+ Assert.Equal(0, headerCheck % 31);
+ }
+
+ [Fact]
+ public void ComputeAdler32_EmptyInput_ReturnsOne()
+ {
+ uint result = ZLibStream.ComputeAdler32(Array.Empty());
+
+ // For empty input, A=1, B=0, so Adler32 = (0 << 16) | 1 = 1
+ Assert.Equal(1u, result);
+ }
+
+ [Fact]
+ public void ComputeAdler32_KnownInput_ReturnsExpectedChecksum()
+ {
+ // "Wikipedia" Adler-32 is well-known: 0x11E60398
+ byte[] data = Encoding.ASCII.GetBytes("Wikipedia");
+ uint result = ZLibStream.ComputeAdler32(data);
+
+ Assert.Equal(0x11E60398u, result);
+ }
+
+ [Fact]
+ public void ComputeAdler32_WithOffset_ComputesCorrectly()
+ {
+ byte[] data = Encoding.ASCII.GetBytes("XXWikipediaYY");
+ // Offset 2, count 9 = "Wikipedia"
+ uint result = ZLibStream.ComputeAdler32(data, 2, 9);
+
+ Assert.Equal(0x11E60398u, result);
+ }
+
+ [Fact]
+ public void Compress_Fastest_ProducesValidOutput()
+ {
+ byte[] original = Encoding.UTF8.GetBytes("test data for fastest compression level");
+
+ byte[] compressed;
+ using (var compressedStream = new MemoryStream())
+ {
+ using (var zlibStream = new ZLibStream(compressedStream, CompressionLevel.Fastest, leaveOpen: true))
+ {
+ zlibStream.Write(original, 0, original.Length);
+ }
+
+ compressed = compressedStream.ToArray();
+ }
+
+ // Verify valid header
+ Assert.Equal(0x78, compressed[0]);
+ int headerCheck = (compressed[0] * 256) + compressed[1];
+ Assert.Equal(0, headerCheck % 31);
+
+ // Verify decompression round-trip
+ byte[] decompressed;
+ using (var compressedStream = new MemoryStream(compressed))
+ {
+ using (var zlibStream = new ZLibStream(compressedStream, CompressionMode.Decompress))
+ {
+ using (var resultStream = new MemoryStream())
+ {
+ zlibStream.CopyTo(resultStream);
+ decompressed = resultStream.ToArray();
+ }
+ }
+ }
+
+ Assert.Equal(original, decompressed);
+ }
+
+ [Fact]
+ public void Compress_NoCompression_ProducesValidOutput()
+ {
+ byte[] original = Encoding.UTF8.GetBytes("test data for no compression level");
+
+ byte[] compressed;
+ using (var compressedStream = new MemoryStream())
+ {
+ using (var zlibStream = new ZLibStream(compressedStream, CompressionLevel.NoCompression, leaveOpen: true))
+ {
+ zlibStream.Write(original, 0, original.Length);
+ }
+
+ compressed = compressedStream.ToArray();
+ }
+
+ // Verify valid header
+ Assert.Equal(0x78, compressed[0]);
+ int headerCheck = (compressed[0] * 256) + compressed[1];
+ Assert.Equal(0, headerCheck % 31);
+
+ // Verify decompression round-trip
+ byte[] decompressed;
+ using (var compressedStream = new MemoryStream(compressed))
+ {
+ using (var zlibStream = new ZLibStream(compressedStream, CompressionMode.Decompress))
+ {
+ using (var resultStream = new MemoryStream())
+ {
+ zlibStream.CopyTo(resultStream);
+ decompressed = resultStream.ToArray();
+ }
+ }
+ }
+
+ Assert.Equal(original, decompressed);
+ }
+
+ [Fact]
+ public void Compress_WritesCorrectAdler32Trailer()
+ {
+ // "Wikipedia" has the well-known Adler-32 value 0x11E60398.
+ // Verify that the last 4 bytes of the compressed output match the
+ // Adler-32 of the original data in big-endian order.
+ byte[] original = Encoding.ASCII.GetBytes("Wikipedia");
+
+ byte[] compressed;
+ using (var output = new MemoryStream())
+ {
+ using (var zlibStream = new ZLibStream(output, CompressionLevel.Optimal, leaveOpen: true))
+ {
+ zlibStream.Write(original, 0, original.Length);
+ }
+
+ compressed = output.ToArray();
+ }
+
+ Assert.True(compressed.Length >= 6, "Compressed output too short to contain header + trailer.");
+
+ // Parse the 4-byte big-endian Adler-32 trailer
+ uint trailer = ((uint)compressed[^4] << 24)
+ | ((uint)compressed[^3] << 16)
+ | ((uint)compressed[^2] << 8)
+ | compressed[^1];
+
+ uint expected = ZLibStream.ComputeAdler32(original);
+ Assert.Equal(0x11E60398u, expected); // sanity-check known test vector
+ Assert.Equal(expected, trailer);
+ }
+
+ ///
+ /// Simulates the GIDS format: 4-byte GIDS header (0x01, 0x00 magic +
+ /// 2-byte LE uncompressed length) followed by standard zlib-compressed data.
+ /// Verifies the decompression approach used in PivSession.KeyPairs.DecompressGids.
+ ///
+ [Fact]
+ public void Decompress_GidsFormat_WithHeaderStripping_Works()
+ {
+ byte[] original = Encoding.UTF8.GetBytes("Certificate data for GIDS test");
+
+ // Compress with zlib
+ byte[] zlibCompressed;
+ using (var compressedStream = new MemoryStream())
+ {
+ using (var zlibStream = new ZLibStream(compressedStream, CompressionLevel.Optimal, leaveOpen: true))
+ {
+ zlibStream.Write(original, 0, original.Length);
+ }
+
+ zlibCompressed = compressedStream.ToArray();
+ }
+
+ // Prepend the 4-byte GIDS header: magic (0x01, 0x00) + LE uncompressed length
+ int uncompressedLength = original.Length;
+ byte[] gidsData = new byte[4 + zlibCompressed.Length];
+ gidsData[0] = 0x01;
+ gidsData[1] = 0x00;
+ gidsData[2] = (byte)(uncompressedLength & 0xFF);
+ gidsData[3] = (byte)((uncompressedLength >> 8) & 0xFF);
+ Buffer.BlockCopy(zlibCompressed, 0, gidsData, 4, zlibCompressed.Length);
+
+ // Decompress like PivSession.KeyPairs.DecompressGids does:
+ // strip 4-byte header, then pass to ZLibStream
+ const int gidsHeaderLength = 4;
+ using (var dataStream = new MemoryStream(gidsData, gidsHeaderLength, gidsData.Length - gidsHeaderLength))
+ {
+ using (var decompressor = new ZLibStream(dataStream, CompressionMode.Decompress))
+ {
+ using (var resultStream = new MemoryStream())
+ {
+ decompressor.CopyTo(resultStream);
+ byte[] decompressed = resultStream.ToArray();
+
+ Assert.Equal(original, decompressed);
+ }
+ }
+ }
+ }
+ }
+}