Skip to content

Commit 565697e

Browse files
committed
fix(core): validate ctaphid keepalive framing
1 parent cc37571 commit 565697e

2 files changed

Lines changed: 63 additions & 21 deletions

File tree

src/Core/src/Protocols/Fido/Hid/FidoHidProtocol.cs

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ private async Task<ReadOnlyMemory<byte>> ReceiveResponse(
249249
var initPacket = await _connection.ReceiveAsync(cancellationToken).ConfigureAwait(false);
250250
while (IsKeepAlivePacket(initPacket.Span))
251251
{
252+
ValidateInitPacket(initPacket.Span, channelId);
252253
_logger.LogTrace("Received keep-alive, waiting for response");
253254
initPacket = await _connection.ReceiveAsync(cancellationToken).ConfigureAwait(false);
254255
}
@@ -263,16 +264,11 @@ private async Task<ReadOnlyMemory<byte>> ReceiveResponse(
263264
var responseData = new byte[responseLength];
264265
var initDataLength = Math.Min(responseLength, CtapConstants.InitDataSize);
265266

266-
// Ensure we don't try to read more data than the packet contains
267-
var availableDataInPacket = Math.Min(initDataLength, initPacket.Length - CtapConstants.InitHeaderSize);
268-
if (availableDataInPacket < 0)
269-
availableDataInPacket = 0;
270-
271-
initPacket.Span.Slice(CtapConstants.InitHeaderSize, availableDataInPacket)
267+
initPacket.Span.Slice(CtapConstants.InitHeaderSize, initDataLength)
272268
.CopyTo(responseData);
273269

274270
// Receive continuation packets if needed
275-
var bytesReceived = availableDataInPacket;
271+
var bytesReceived = initDataLength;
276272
byte expectedSequence = 0;
277273
while (bytesReceived < responseLength)
278274
{
@@ -282,15 +278,10 @@ private async Task<ReadOnlyMemory<byte>> ReceiveResponse(
282278
responseLength - bytesReceived,
283279
CtapConstants.ContinuationDataSize);
284280

285-
// Ensure we don't try to read more data than the packet contains
286-
var availableContData = Math.Min(contDataLength, contPacket.Length - CtapConstants.ContinuationHeaderSize);
287-
if (availableContData < 0)
288-
availableContData = 0;
289-
290-
contPacket.Span.Slice(CtapConstants.ContinuationHeaderSize, availableContData)
281+
contPacket.Span.Slice(CtapConstants.ContinuationHeaderSize, contDataLength)
291282
.CopyTo(responseData.AsSpan(bytesReceived));
292283

293-
bytesReceived += availableContData;
284+
bytesReceived += contDataLength;
294285
expectedSequence++;
295286
}
296287

@@ -355,8 +346,8 @@ private static bool IsKeepAlivePacket(ReadOnlySpan<byte> packet) =>
355346

356347
private static void ValidateInitPacket(ReadOnlySpan<byte> packet, uint channelId)
357348
{
358-
if (packet.Length < CtapConstants.InitHeaderSize)
359-
throw new InvalidOperationException("CTAP HID init packet is too short");
349+
if (packet.Length != CtapConstants.PacketSize)
350+
throw new InvalidOperationException("CTAP HID init packet must be exactly 64 bytes");
360351

361352
var packetChannelId = BinaryPrimitives.ReadUInt32BigEndian(packet);
362353
if (packetChannelId != channelId)
@@ -368,8 +359,8 @@ private static void ValidateInitPacket(ReadOnlySpan<byte> packet, uint channelId
368359

369360
private static void ValidateContinuationPacket(ReadOnlySpan<byte> packet, uint channelId, byte expectedSequence)
370361
{
371-
if (packet.Length < CtapConstants.ContinuationHeaderSize)
372-
throw new InvalidOperationException("CTAP HID continuation packet is too short");
362+
if (packet.Length != CtapConstants.PacketSize)
363+
throw new InvalidOperationException("CTAP HID continuation packet must be exactly 64 bytes");
373364

374365
var packetChannelId = BinaryPrimitives.ReadUInt32BigEndian(packet);
375366
if (packetChannelId != channelId)
@@ -440,4 +431,4 @@ public void Dispose()
440431
_connection.Dispose();
441432
_disposed = true;
442433
}
443-
}
434+
}

src/Core/tests/Yubico.YubiKit.Core.UnitTests/Protocols/Fido/Hid/FidoHidProtocolTests.cs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,58 @@ public async Task SendVendorCommandAsync_ResponseWithShortInitPacket_ThrowsInval
9898
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
9999
protocol.SendVendorCommandAsync(CtapConstants.CtapVendorFirst, ReadOnlyMemory<byte>.Empty, TestContext.Current.CancellationToken));
100100

101-
Assert.Contains("too short", ex.Message, StringComparison.OrdinalIgnoreCase);
101+
Assert.Contains("exactly", ex.Message, StringComparison.OrdinalIgnoreCase);
102+
}
103+
104+
[Fact]
105+
public async Task SendVendorCommandAsync_ResponseWithPartialInitPacket_ThrowsInvalidOperationException()
106+
{
107+
var connection = new FakeFidoHidConnection();
108+
var protocol = new FidoHidProtocol(connection);
109+
110+
var packet = CreateInitPacket(0x01020304, CtapConstants.CtapVendorFirst, [0xAA]);
111+
connection.QueueResponsePackets(packet[..CtapConstants.InitHeaderSize]);
112+
113+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
114+
protocol.SendVendorCommandAsync(CtapConstants.CtapVendorFirst, ReadOnlyMemory<byte>.Empty, TestContext.Current.CancellationToken));
115+
116+
Assert.Contains("exactly", ex.Message, StringComparison.OrdinalIgnoreCase);
117+
}
118+
119+
[Fact]
120+
public async Task SendVendorCommandAsync_ResponseWithPartialContinuationPacket_ThrowsInvalidOperationException()
121+
{
122+
var connection = new FakeFidoHidConnection();
123+
var protocol = new FidoHidProtocol(connection);
124+
125+
var responsePayload = Enumerable.Range(0, CtapConstants.InitDataSize + 1)
126+
.Select(i => (byte)i)
127+
.ToArray();
128+
var continuation = CreateContinuationPacket(0x01020304, sequence: 0, responsePayload.AsSpan(CtapConstants.InitDataSize));
129+
connection.QueueResponsePackets(
130+
CreateInitPacket(0x01020304, CtapConstants.CtapVendorFirst, responsePayload),
131+
continuation[..CtapConstants.ContinuationHeaderSize]);
132+
133+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
134+
protocol.SendVendorCommandAsync(CtapConstants.CtapVendorFirst, ReadOnlyMemory<byte>.Empty, TestContext.Current.CancellationToken));
135+
136+
Assert.Contains("exactly", ex.Message, StringComparison.OrdinalIgnoreCase);
137+
}
138+
139+
[Fact]
140+
public async Task SendVendorCommandAsync_ResponseWithWrongChannelKeepAlive_ThrowsInvalidOperationException()
141+
{
142+
var connection = new FakeFidoHidConnection();
143+
var protocol = new FidoHidProtocol(connection);
144+
145+
connection.QueueResponsePackets(
146+
CreateInitPacket(0x05060708, CtapConstants.CtapHidKeepAlive, [0x01]),
147+
CreateInitPacket(0x01020304, CtapConstants.CtapVendorFirst, [0xAA]));
148+
149+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
150+
protocol.SendVendorCommandAsync(CtapConstants.CtapVendorFirst, ReadOnlyMemory<byte>.Empty, TestContext.Current.CancellationToken));
151+
152+
Assert.Contains("channel", ex.Message, StringComparison.OrdinalIgnoreCase);
102153
}
103154

104155
[Fact]
@@ -244,4 +295,4 @@ private byte[] CreateInitResponse()
244295
return CreateInitPacket(CtapConstants.BroadcastChannelId, CtapConstants.CtapHidInit, payload);
245296
}
246297
}
247-
}
298+
}

0 commit comments

Comments
 (0)