Skip to content

Commit a91fe73

Browse files
authored
Merge pull request #504 from Yubico/yubikit-ctaphid-sequence-wrap
feat: Validate CTAPHID response framing
2 parents 15a6445 + 565697e commit a91fe73

2 files changed

Lines changed: 347 additions & 15 deletions

File tree

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

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,15 @@ private async Task<ReadOnlyMemory<byte>> ReceiveResponse(
247247

248248
// Get initialization packet, handling keep-alive
249249
var initPacket = await _connection.ReceiveAsync(cancellationToken).ConfigureAwait(false);
250-
while (GetPacketCommand(initPacket.Span) == CtapConstants.CtapHidKeepAlive)
250+
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
}
255256

257+
ValidateInitPacket(initPacket.Span, channelId);
258+
256259
var responseLength = GetPacketLength(initPacket.Span);
257260
if (responseLength > CtapConstants.MaxPayloadSize)
258261
throw new InvalidOperationException($"Response length {responseLength} exceeds max payload size");
@@ -261,32 +264,25 @@ private async Task<ReadOnlyMemory<byte>> ReceiveResponse(
261264
var responseData = new byte[responseLength];
262265
var initDataLength = Math.Min(responseLength, CtapConstants.InitDataSize);
263266

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

272270
// Receive continuation packets if needed
273-
var bytesReceived = availableDataInPacket;
271+
var bytesReceived = initDataLength;
272+
byte expectedSequence = 0;
274273
while (bytesReceived < responseLength)
275274
{
276275
var contPacket = await _connection.ReceiveAsync(cancellationToken).ConfigureAwait(false);
276+
ValidateContinuationPacket(contPacket.Span, channelId, expectedSequence);
277277
var contDataLength = Math.Min(
278278
responseLength - bytesReceived,
279279
CtapConstants.ContinuationDataSize);
280280

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

289-
bytesReceived += availableContData;
284+
bytesReceived += contDataLength;
285+
expectedSequence++;
290286
}
291287

292288
_logger.LogTrace("Received {Length} bytes in response", responseLength);
@@ -343,6 +339,44 @@ private static byte[] ConstructContinuationPacket(uint channelId, byte sequence,
343339
private static byte GetPacketCommand(ReadOnlySpan<byte> packet) =>
344340
(byte)(packet[4] & ~CtapConstants.InitPacketMask);
345341

342+
private static bool IsKeepAlivePacket(ReadOnlySpan<byte> packet) =>
343+
packet.Length >= CtapConstants.InitHeaderSize &&
344+
(packet[4] & CtapConstants.InitPacketMask) != 0 &&
345+
GetPacketCommand(packet) == CtapConstants.CtapHidKeepAlive;
346+
347+
private static void ValidateInitPacket(ReadOnlySpan<byte> packet, uint channelId)
348+
{
349+
if (packet.Length != CtapConstants.PacketSize)
350+
throw new InvalidOperationException("CTAP HID init packet must be exactly 64 bytes");
351+
352+
var packetChannelId = BinaryPrimitives.ReadUInt32BigEndian(packet);
353+
if (packetChannelId != channelId)
354+
throw new InvalidOperationException("CTAP HID init packet channel mismatch");
355+
356+
if ((packet[4] & CtapConstants.InitPacketMask) == 0)
357+
throw new InvalidOperationException("CTAP HID response packet is not an init packet");
358+
}
359+
360+
private static void ValidateContinuationPacket(ReadOnlySpan<byte> packet, uint channelId, byte expectedSequence)
361+
{
362+
if (packet.Length != CtapConstants.PacketSize)
363+
throw new InvalidOperationException("CTAP HID continuation packet must be exactly 64 bytes");
364+
365+
var packetChannelId = BinaryPrimitives.ReadUInt32BigEndian(packet);
366+
if (packetChannelId != channelId)
367+
throw new InvalidOperationException("CTAP HID continuation packet channel mismatch");
368+
369+
if ((packet[4] & CtapConstants.InitPacketMask) != 0)
370+
throw new InvalidOperationException("CTAP HID continuation packet has init bit set");
371+
372+
var sequence = packet[4];
373+
if (!IsExpectedContinuationSequence(sequence, expectedSequence))
374+
throw new InvalidOperationException("CTAP HID continuation packet sequence mismatch");
375+
}
376+
377+
internal static bool IsExpectedContinuationSequence(byte sequence, byte expectedSequence) =>
378+
(sequence & ~CtapConstants.InitPacketMask) == (expectedSequence & ~CtapConstants.InitPacketMask);
379+
346380
/// <summary>
347381
/// Extracts the payload length from an init packet.
348382
/// </summary>

0 commit comments

Comments
 (0)