Skip to content

Commit 1de6d6d

Browse files
Get-DbaNetworkEncryption - Fix TLS handshake by wrapping in TDS packets
SQL Server uses STARTTLS-style TLS negotiation where TLS ClientHello/ServerHello messages are wrapped inside TDS packets (type 0x12) during the handshake phase. Previously we were sending a raw TLS ClientHello directly to the NetworkStream, causing SQL Server to wait for a valid TDS packet and the AuthenticateAsClient call to time out. Fix: Add DbaTools.TdsWrappingStream (a custom Stream subclass compiled via Add-Type) that transparently adds TDS packet framing on writes and strips it on reads. The SslStream now wraps this TdsWrappingStream instead of the raw NetworkStream. Also added: parsing of the ENCRYPTION field in the server's pre-login response to emit a clear error when the server has TLS disabled (ENCRYPT_NOT_SUP = 0x02). (do Get-DbaNetworkEncryption) Co-authored-by: Andreas Jordan <andreasjordan@users.noreply.github.com>
1 parent d7c6952 commit 1de6d6d

1 file changed

Lines changed: 137 additions & 4 deletions

File tree

public/Get-DbaNetworkEncryption.ps1

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,110 @@ function Get-DbaNetworkEncryption {
8686
[switch]$EnableException
8787
)
8888
begin {
89+
# SQL Server wraps TLS handshake messages inside TDS packets (type 0x12) during negotiation.
90+
# This helper stream transparently adds/strips TDS framing so that SslStream can perform
91+
# the TLS handshake correctly over the SQL Server pre-login channel.
92+
if (-not ('DbaTools.TdsWrappingStream' -as [type])) {
93+
Add-Type -TypeDefinition @"
94+
using System;
95+
using System.IO;
96+
97+
namespace DbaTools {
98+
public class TdsWrappingStream : Stream {
99+
private Stream _inner;
100+
private byte _packetType;
101+
private byte _packetId;
102+
private byte[] _readBuffer;
103+
private int _readPos;
104+
private int _readCount;
105+
106+
public TdsWrappingStream(Stream inner, byte packetType) {
107+
_inner = inner;
108+
_packetType = packetType;
109+
_packetId = 1;
110+
_readBuffer = null;
111+
_readPos = 0;
112+
_readCount = 0;
113+
}
114+
115+
public override bool CanRead { get { return true; } }
116+
public override bool CanWrite { get { return true; } }
117+
public override bool CanSeek { get { return false; } }
118+
public override long Length { get { throw new NotSupportedException(); } }
119+
public override long Position {
120+
get { throw new NotSupportedException(); }
121+
set { throw new NotSupportedException(); }
122+
}
123+
124+
public override void Flush() { _inner.Flush(); }
125+
126+
// Wrap outgoing data in TDS packet(s) before sending to SQL Server.
127+
public override void Write(byte[] buffer, int offset, int count) {
128+
int maxPayload = 32760;
129+
int remaining = count;
130+
int srcOffset = offset;
131+
while (remaining > 0) {
132+
int chunkSize = remaining < maxPayload ? remaining : maxPayload;
133+
bool isLast = (remaining - chunkSize) == 0;
134+
int packetLen = chunkSize + 8;
135+
byte[] header = new byte[] {
136+
_packetType,
137+
isLast ? (byte)0x01 : (byte)0x00,
138+
(byte)(packetLen >> 8),
139+
(byte)(packetLen & 0xFF),
140+
0x00, 0x00,
141+
_packetId++,
142+
0x00
143+
};
144+
_inner.Write(header, 0, 8);
145+
_inner.Write(buffer, srcOffset, chunkSize);
146+
srcOffset += chunkSize;
147+
remaining -= chunkSize;
148+
}
149+
}
150+
151+
// Strip TDS packet framing from incoming data before delivering to SslStream.
152+
public override int Read(byte[] buffer, int offset, int count) {
153+
// Return buffered payload from the previous TDS packet first.
154+
if (_readBuffer != null && _readPos < _readCount) {
155+
int available = _readCount - _readPos;
156+
int toCopy = available < count ? available : count;
157+
Array.Copy(_readBuffer, _readPos, buffer, offset, toCopy);
158+
_readPos += toCopy;
159+
return toCopy;
160+
}
161+
// Read the 8-byte TDS header of the next packet.
162+
byte[] header = new byte[8];
163+
int headerRead = 0;
164+
while (headerRead < 8) {
165+
int n = _inner.Read(header, headerRead, 8 - headerRead);
166+
if (n == 0) return 0;
167+
headerRead += n;
168+
}
169+
int payloadLen = ((header[2] << 8) | header[3]) - 8;
170+
if (payloadLen <= 0) return 0;
171+
// Read the full payload.
172+
_readBuffer = new byte[payloadLen];
173+
_readCount = 0;
174+
while (_readCount < payloadLen) {
175+
int n = _inner.Read(_readBuffer, _readCount, payloadLen - _readCount);
176+
if (n == 0) break;
177+
_readCount += n;
178+
}
179+
_readPos = 0;
180+
int toCopyNow = _readCount < count ? _readCount : count;
181+
Array.Copy(_readBuffer, 0, buffer, offset, toCopyNow);
182+
_readPos = toCopyNow;
183+
return toCopyNow;
184+
}
185+
186+
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
187+
public override void SetLength(long value) { throw new NotSupportedException(); }
188+
}
189+
}
190+
"@
191+
}
192+
89193
function Get-SqlBrowserPort {
90194
param (
91195
[string]$ComputerName,
@@ -164,8 +268,8 @@ function Get-DbaNetworkEncryption {
164268

165269
$networkStream = $tcpClient.GetStream()
166270

167-
# We need to send a SQL Server pre-login packet before doing TLS,
168-
# because SQL Server uses STARTTLS-style negotiation.
271+
# Send a SQL Server pre-login packet requesting ENCRYPT_ON (0x01).
272+
# SQL Server uses STARTTLS-style negotiation: TLS only starts after this exchange.
169273
# Pre-login packet layout (26 bytes total):
170274
# TDS header (8 bytes): type=0x12 (PRE_LOGIN), status=0x01 (EOM), length=0x001A (26)
171275
# Payload option headers (11 bytes):
@@ -195,7 +299,36 @@ function Get-DbaNetworkEncryption {
195299
throw "Invalid pre-login response from ${ComputerName}:${Port}"
196300
}
197301

198-
# Now wrap in SSL stream and do TLS handshake
302+
# Parse the ENCRYPTION option from the server's pre-login response.
303+
# Option offsets in the payload are relative to the start of the payload (byte 8).
304+
$payloadStart = 8
305+
$optionOffset = $payloadStart
306+
$serverEncryption = $null
307+
while ($optionOffset -lt ($bytesRead - 4)) {
308+
$optionType = $responseBuffer[$optionOffset]
309+
if ($optionType -eq 0xFF) { break } # TERMINATOR
310+
$dataOffset = ($responseBuffer[$optionOffset + 1] -shl 8) -bor $responseBuffer[$optionOffset + 2]
311+
$dataLength = ($responseBuffer[$optionOffset + 3] -shl 8) -bor $responseBuffer[$optionOffset + 4]
312+
if ($optionType -eq 0x01) {
313+
# ENCRYPTION option
314+
$absoluteDataOffset = $payloadStart + $dataOffset
315+
if ($absoluteDataOffset -lt $bytesRead) {
316+
$serverEncryption = $responseBuffer[$absoluteDataOffset]
317+
}
318+
break
319+
}
320+
$optionOffset += 5
321+
}
322+
323+
if ($serverEncryption -eq 0x02) {
324+
# ENCRYPT_NOT_SUP - server does not support TLS at all
325+
throw "Server does not support TLS encryption - no certificate is presented"
326+
}
327+
328+
# SQL Server wraps TLS handshake messages in TDS packets (type 0x12).
329+
# TdsWrappingStream adds/strips that framing so SslStream negotiates correctly.
330+
$tdsStream = New-Object DbaTools.TdsWrappingStream($networkStream, [byte]0x12)
331+
199332
# The server certificate is captured via the validation callback
200333
$script:capturedCertificate = $null
201334

@@ -206,7 +339,7 @@ function Get-DbaNetworkEncryption {
206339
}
207340

208341
$sslStream = New-Object System.Net.Security.SslStream(
209-
$networkStream,
342+
$tdsStream,
210343
$false,
211344
$certValidationCallback
212345
)

0 commit comments

Comments
 (0)