Skip to content

Commit 487494f

Browse files
committed
fix: no longer sending chained data to key after receiving sw 0x67
1 parent a5fbe56 commit 487494f

File tree

5 files changed

+209
-68
lines changed

5 files changed

+209
-68
lines changed

Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Globalization;
17+
using Microsoft.Extensions.Logging;
1618
using Yubico.Core.Iso7816;
19+
using Yubico.Core.Logging;
1720

1821
namespace Yubico.YubiKey.Pipelines
1922
{
@@ -23,7 +26,9 @@ namespace Yubico.YubiKey.Pipelines
2326
/// </summary>
2427
internal class CommandChainingTransform : IApduTransform
2528
{
26-
public int MaxSize { get; internal set; } = 255;
29+
private readonly ILogger _log = Log.GetLogger<CommandChainingTransform>();
30+
31+
public int MaxChunkSize { get; internal set; } = 255;
2732

2833
readonly IApduTransform _pipeline;
2934

@@ -41,33 +46,51 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT
4146
throw new ArgumentNullException(nameof(command));
4247
}
4348

44-
if (command.Data.IsEmpty || command.Data.Length <= MaxSize)
49+
// Send regular short APDU
50+
int commandDataSize = command.Data.Length;
51+
if (commandDataSize <= MaxChunkSize)
4552
{
53+
_log.LogDebug("Sending short APDU");
4654
return _pipeline.Invoke(command, commandType, responseType);
4755
}
4856

57+
// Send chained short APDU
4958
var sourceData = command.Data;
5059
ResponseApdu? responseApdu = null;
51-
60+
_log.LogDebug("APDU size exceeds size of short APDU, proceeding to send data in chunks instead");
5261
while (!sourceData.IsEmpty)
5362
{
54-
int length = Math.Min(MaxSize, sourceData.Length);
55-
var data = sourceData.Slice(0, length);
56-
sourceData = sourceData.Slice(length);
63+
int chunkLength = Math.Min(MaxChunkSize, sourceData.Length);
64+
var dataChunk = sourceData[..chunkLength];
65+
sourceData = sourceData[chunkLength..];
5766

5867
var partialApdu = new CommandApdu
5968
{
60-
Cla = (byte)(command.Cla | (sourceData.IsEmpty ? 0 : 0x10)),
69+
Cla = (byte)(command.Cla | (sourceData.IsEmpty
70+
? 0
71+
: 0x10)),
6172
Ins = command.Ins,
6273
P1 = command.P1,
6374
P2 = command.P2,
64-
Data = data
75+
Data = dataChunk
6576
};
6677

6778
responseApdu = _pipeline.Invoke(partialApdu, commandType, responseType);
79+
80+
// Stop sending data when the YubiKey response is 0x67 (when the chained data
81+
// sent exceeds the max allowed length) and let caller handle the response
82+
if (responseApdu.SW != SWConstants.WrongLength)
83+
{
84+
continue;
85+
}
86+
87+
_log.LogWarning("Sent data exceeds max allowed length by YubiKey. (SW: 0x{StatusWord})",
88+
responseApdu.SW.ToString("X4", CultureInfo.InvariantCulture));
89+
90+
return responseApdu;
6891
}
6992

70-
return responseApdu!; // Covered by Debug.Assert above.
93+
return responseApdu!;
7194
}
7295

7396
public void Setup() => _pipeline.Setup();

Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs

Lines changed: 51 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ internal class SmartCardConnection : IYubiKeyConnection
3434
private readonly byte[]? _applicationId;
3535
private readonly ISmartCardConnection _smartCardConnection;
3636
private IApduTransform _apduPipeline;
37-
private bool _disposedValue;
3837

38+
private bool _disposedValue;
39+
private bool IsOath => GetIsAuth();
3940
public ISelectApplicationData? SelectApplicationData { get; set; }
4041

4142
/// <summary>
@@ -47,7 +48,7 @@ internal class SmartCardConnection : IYubiKeyConnection
4748
public SmartCardConnection(
4849
ISmartCardDevice smartCardDevice,
4950
YubiKeyApplication yubiKeyApplication)
50-
: this(smartCardDevice, yubiKeyApplication, null)
51+
: this(smartCardDevice, yubiKeyApplication, (byte[]?)null)
5152
{
5253
if (yubiKeyApplication == YubiKeyApplication.Fido2)
5354
{
@@ -69,10 +70,7 @@ public SmartCardConnection(
6970
public SmartCardConnection(
7071
ISmartCardDevice smartCardDevice,
7172
byte[] applicationId)
72-
: this(
73-
smartCardDevice,
74-
YubiKeyApplication.Unknown,
75-
applicationId)
73+
: this(smartCardDevice, YubiKeyApplication.Unknown, applicationId)
7674
{
7775
if (applicationId.SequenceEqual(YubiKeyApplication.Fido2.GetIso7816ApplicationId()))
7876
{
@@ -111,11 +109,36 @@ protected SmartCardConnection(
111109

112110
_smartCardConnection = smartCardDevice.Connect();
113111

114-
_apduPipeline = new SmartCardTransform(_smartCardConnection);
112+
// Set up the pipeline
113+
_apduPipeline =new SmartCardTransform(_smartCardConnection);
115114
_apduPipeline = AddResponseChainingTransform(_apduPipeline);
116115
_apduPipeline = new CommandChainingTransform(_apduPipeline);
117116
}
118117

118+
public virtual TResponse SendCommand<TResponse>(IYubiKeyCommand<TResponse> yubiKeyCommand)
119+
where TResponse : IYubiKeyResponse
120+
{
121+
using var _ = _smartCardConnection.BeginTransaction(out bool cardWasReset);
122+
if (cardWasReset)
123+
{
124+
SelectApplication();
125+
}
126+
127+
var responseApdu = _apduPipeline.Invoke(
128+
yubiKeyCommand.CreateCommandApdu(),
129+
yubiKeyCommand.GetType(),
130+
typeof(TResponse));
131+
132+
return yubiKeyCommand.CreateResponseForApdu(responseApdu);
133+
}
134+
135+
public void Dispose()
136+
{
137+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
138+
Dispose(disposing: true);
139+
GC.SuppressFinalize(this);
140+
}
141+
119142
// Allow subclasses to build a different pipeline, which means they need
120143
// to get the current one.
121144
protected IApduTransform GetPipeline() => _apduPipeline;
@@ -133,12 +156,19 @@ protected void SetPipeline(IApduTransform apduPipeline)
133156
SelectApplication();
134157
}
135158

136-
// The application is set to Oath by enum or by application id
137-
private bool IsOath =>
138-
_yubiKeyApplication == YubiKeyApplication.Oath ||
139-
(_applicationId != null &&
140-
_applicationId.SequenceEqual(
141-
YubiKeyApplication.Oath.GetIso7816ApplicationId()));
159+
protected virtual void Dispose(bool disposing)
160+
{
161+
if (!_disposedValue)
162+
{
163+
if (disposing)
164+
{
165+
_apduPipeline.Cleanup();
166+
_smartCardConnection.Dispose();
167+
}
168+
169+
_disposedValue = true;
170+
}
171+
}
142172

143173
private IApduTransform AddResponseChainingTransform(IApduTransform pipeline) =>
144174
IsOath
@@ -170,55 +200,20 @@ private void SelectApplication()
170200
CultureInfo.CurrentCulture,
171201
ExceptionMessages.SmartCardPipelineSetupFailed,
172202
responseApdu.SW))
173-
{
174-
SW = responseApdu.SW
175-
};
203+
{
204+
SW = responseApdu.SW
205+
};
176206
}
177207

178208
// Set the instance property SelectApplicationData
179209
var response = selectApplicationCommand.CreateResponseForApdu(responseApdu);
180210
SelectApplicationData = response.GetData();
181211
}
182212

183-
public virtual TResponse SendCommand<TResponse>(IYubiKeyCommand<TResponse> yubiKeyCommand)
184-
where TResponse : IYubiKeyResponse
185-
{
186-
using (var _ = _smartCardConnection.BeginTransaction(out bool cardWasReset))
187-
{
188-
if (cardWasReset)
189-
{
190-
SelectApplication();
191-
}
192-
193-
var responseApdu = _apduPipeline.Invoke(
194-
yubiKeyCommand.CreateCommandApdu(),
195-
yubiKeyCommand.GetType(),
196-
typeof(TResponse)
197-
);
198-
199-
return yubiKeyCommand.CreateResponseForApdu(responseApdu);
200-
}
201-
}
202-
203-
protected virtual void Dispose(bool disposing)
204-
{
205-
if (!_disposedValue)
206-
{
207-
if (disposing)
208-
{
209-
_apduPipeline.Cleanup();
210-
_smartCardConnection.Dispose();
211-
}
212-
213-
_disposedValue = true;
214-
}
215-
}
216-
217-
public void Dispose()
218-
{
219-
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
220-
Dispose(disposing: true);
221-
GC.SuppressFinalize(this);
222-
}
213+
private bool GetIsAuth() =>
214+
_yubiKeyApplication == YubiKeyApplication.Oath ||
215+
(_applicationId != null &&
216+
_applicationId.SequenceEqual(
217+
YubiKeyApplication.Oath.GetIso7816ApplicationId()));
223218
}
224219
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2024 Yubico AB
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License").
4+
// You may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace Yubico.YubiKey;
16+
17+
#pragma warning disable CA1707 // Allow underscore in constant
18+
/// <summary>
19+
/// This contains the maximum size (in bytes) of APDU commands for the various YubiKey models.
20+
/// </summary>
21+
public static class SmartCardMaxApduSizes
22+
{
23+
/// <summary>
24+
/// The max APDU command size for the YubiKey NEO
25+
/// </summary>
26+
public const int NEO = 1390;
27+
28+
/// <summary>
29+
/// The max APDU command size for the YubiKey 4 and greater
30+
/// </summary>
31+
public const int YK4 = 2038;
32+
33+
/// <summary>
34+
/// The max APDU command size for the YubiKey 4.3 and greater
35+
/// </summary>
36+
public const int YK4_3 = 3062;
37+
}
38+
#pragma warning restore CA1707
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2024 Yubico AB
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License").
4+
// You may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Xunit;
16+
using Yubico.Core.Iso7816;
17+
using Yubico.Core.Tlv;
18+
using Yubico.YubiKey.TestUtilities;
19+
20+
namespace Yubico.YubiKey.Piv.Commands;
21+
22+
public class PutDataCommandTests
23+
{
24+
[Theory]
25+
[InlineData(StandardTestDevice.Fw5)]
26+
public void SendCommand_With_TooLargeApdu_ReturnsResultFailed(
27+
StandardTestDevice testDeviceType)
28+
{
29+
using var pivSession = GetSession(testDeviceType);
30+
31+
var tooLargeTlv = new TlvObject(0x53, new byte[10000]);
32+
var tlvBytes = tooLargeTlv.GetBytes();
33+
var command = new PutDataCommand(0x5F0000, tlvBytes);
34+
35+
var response = pivSession.Connection.SendCommand(command);
36+
37+
Assert.Equal(ResponseStatus.Failed, response.Status);
38+
Assert.Equal(SWConstants.WrongLength, response.StatusWord);
39+
40+
// Cleanup
41+
pivSession.ResetApplication();
42+
}
43+
44+
[Theory]
45+
[InlineData(StandardTestDevice.Fw5)]
46+
public void SendCommand_with_ValidSizeApdu_ReturnsResultSuccess(
47+
StandardTestDevice testDeviceType)
48+
{
49+
// Arrange
50+
using var pivSession = GetSession(testDeviceType);
51+
52+
var validSizeTlv = new TlvObject(0x53, new byte[SmartCardMaxApduSizes.YK4_3]);
53+
var tlvBytes = validSizeTlv.GetBytes();
54+
var command = new PutDataCommand(0x5F0000, tlvBytes);
55+
56+
// Act
57+
var response = pivSession.Connection.SendCommand(command);
58+
var actualSize = command.CreateCommandApdu().AsByteArray().Length;
59+
Assert.Equal(3078, actualSize); // This is the current max APDU size of the YubiKey 5 series.
60+
Assert.Equal(ResponseStatus.Success, response.Status);
61+
Assert.Equal(SWConstants.Success, response.StatusWord);
62+
63+
// Cleanup
64+
pivSession.ResetApplication();
65+
}
66+
67+
private static PivSession GetSession(StandardTestDevice testDeviceType)
68+
{
69+
PivSession? pivSession = null;
70+
try
71+
{
72+
var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType);
73+
pivSession = new PivSession(testDevice);
74+
var collectorObj = new Simple39KeyCollector();
75+
pivSession.KeyCollector = collectorObj.Simple39KeyCollectorDelegate;
76+
pivSession.AuthenticateManagementKey();
77+
return pivSession;
78+
}
79+
catch
80+
{
81+
pivSession?.Dispose();
82+
throw;
83+
}
84+
}
85+
}

Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/CommandChainingTransformTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public void Invoke_CommandApduWithLargeDataBuffer_OrsHex10ToClaOnAllExceptLast()
148148

149149
// Arrange
150150
var mockTransform = new Mock<IApduTransform>();
151-
var transform = new CommandChainingTransform(mockTransform.Object) { MaxSize = 4 };
151+
var transform = new CommandChainingTransform(mockTransform.Object) { MaxChunkSize = 4 };
152152
var commandApdu = new CommandApdu { Data = Enumerable.Repeat<byte>(0xFF, 16).ToArray() };
153153

154154
_ = mockTransform
@@ -169,7 +169,7 @@ public void Invoke_CommandApduWithLargeDataBuffer_AllOtherApduPropertiesRemainUn
169169

170170
// Arrange
171171
var mockTransform = new Mock<IApduTransform>();
172-
var transform = new CommandChainingTransform(mockTransform.Object) { MaxSize = 4 };
172+
var transform = new CommandChainingTransform(mockTransform.Object) { MaxChunkSize = 4 };
173173
var commandApdu = new CommandApdu
174174
{
175175
Ins = 1,
@@ -201,7 +201,7 @@ public void Invoke_CommandApduWithLargeDataBuffer_SplitsDataAcrossInvokeCalls()
201201

202202
// Arrange
203203
var mockTransform = new Mock<IApduTransform>();
204-
var transform = new CommandChainingTransform(mockTransform.Object) { MaxSize = 4 };
204+
var transform = new CommandChainingTransform(mockTransform.Object) { MaxChunkSize = 4 };
205205
var commandApdu = new CommandApdu
206206
{
207207
Data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }

0 commit comments

Comments
 (0)