Skip to content

Commit 0708bd8

Browse files
authored
Merge pull request #208 from Yubico/feature/toolargeapduexception
fix: Stop send short APDUs when exceeded max APDU size
2 parents 42ab092 + eed6c90 commit 0708bd8

File tree

5 files changed

+256
-77
lines changed

5 files changed

+256
-77
lines changed
Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2021 Yubico AB
1+
// Copyright 2021 Yubico AB
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License").
44
// You may not use this file except in compliance with the License.
@@ -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

@@ -33,6 +38,7 @@ public CommandChainingTransform(IApduTransform pipeline)
3338
}
3439

3540
public void Cleanup() => _pipeline.Cleanup();
41+
public void Setup() => _pipeline.Setup();
3642

3743
public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType)
3844
{
@@ -41,35 +47,59 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT
4147
throw new ArgumentNullException(nameof(command));
4248
}
4349

44-
if (command.Data.IsEmpty || command.Data.Length <= MaxSize)
50+
// Send single short APDU
51+
int commandDataSize = command.Data.Length;
52+
if (commandDataSize <= MaxChunkSize)
4553
{
54+
_log.LogDebug("Sending short APDU");
4655
return _pipeline.Invoke(command, commandType, responseType);
4756
}
4857

49-
var sourceData = command.Data;
50-
ResponseApdu? responseApdu = null;
58+
// Send a series of short APDU's
59+
_log.LogDebug("APDU size exceeds size of short APDU, proceeding to send data in chunks instead");
60+
return SendChainedApdu(command, commandType, responseType);
61+
}
5162

63+
private ResponseApdu SendChainedApdu(CommandApdu command, Type commandType, Type responseType)
64+
{
65+
ResponseApdu? responseApdu = null;
66+
var sourceData = command.Data;
5267
while (!sourceData.IsEmpty)
5368
{
54-
int length = Math.Min(MaxSize, sourceData.Length);
55-
var data = sourceData.Slice(0, length);
56-
sourceData = sourceData.Slice(length);
57-
58-
var partialApdu = new CommandApdu
69+
responseApdu = SendPartial(command, commandType, responseType, ref sourceData);
70+
if (responseApdu.SW != SWConstants.Success)
5971
{
60-
Cla = (byte)(command.Cla | (sourceData.IsEmpty ? 0 : 0x10)),
61-
Ins = command.Ins,
62-
P1 = command.P1,
63-
P2 = command.P2,
64-
Data = data
65-
};
66-
67-
responseApdu = _pipeline.Invoke(partialApdu, commandType, responseType);
72+
_log.LogWarning("Received error response from YubiKey. (SW: 0x{StatusWord})", responseApdu.SW.ToString("X4", CultureInfo.CurrentCulture));
73+
return responseApdu;
74+
}
6875
}
6976

70-
return responseApdu!; // Covered by Debug.Assert above.
77+
return responseApdu!;
7178
}
7279

73-
public void Setup() => _pipeline.Setup();
80+
private ResponseApdu SendPartial(
81+
CommandApdu command,
82+
Type commandType,
83+
Type responseType,
84+
ref ReadOnlyMemory<byte> sourceData)
85+
{
86+
int chunkLength = Math.Min(MaxChunkSize, sourceData.Length);
87+
var dataChunk = sourceData[..chunkLength];
88+
sourceData = sourceData[chunkLength..];
89+
90+
var partialApdu = new CommandApdu
91+
{
92+
Cla = (byte)(command.Cla | (sourceData.IsEmpty
93+
? 0
94+
: 0x10)),
95+
Ins = command.Ins,
96+
P1 = command.P1,
97+
P2 = command.P2,
98+
Data = dataChunk
99+
};
100+
101+
var responseApdu = _pipeline.Invoke(partialApdu, commandType, responseType);
102+
return responseApdu;
103+
}
74104
}
75105
}

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

Lines changed: 48 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ internal class SmartCardConnection : IYubiKeyConnection
3535
private readonly ISmartCardConnection _smartCardConnection;
3636
private IApduTransform _apduPipeline;
3737
private bool _disposedValue;
38-
38+
private bool IsOath => GetIsOauth();
3939
public ISelectApplicationData? SelectApplicationData { get; set; }
4040

4141
/// <summary>
@@ -69,10 +69,7 @@ public SmartCardConnection(
6969
public SmartCardConnection(
7070
ISmartCardDevice smartCardDevice,
7171
byte[] applicationId)
72-
: this(
73-
smartCardDevice,
74-
YubiKeyApplication.Unknown,
75-
applicationId)
72+
: this(smartCardDevice, YubiKeyApplication.Unknown, applicationId)
7673
{
7774
if (applicationId.SequenceEqual(YubiKeyApplication.Fido2.GetIso7816ApplicationId()))
7875
{
@@ -111,11 +108,36 @@ protected SmartCardConnection(
111108

112109
_smartCardConnection = smartCardDevice.Connect();
113110

111+
// Set up the pipeline
114112
_apduPipeline = new SmartCardTransform(_smartCardConnection);
115113
_apduPipeline = AddResponseChainingTransform(_apduPipeline);
116114
_apduPipeline = new CommandChainingTransform(_apduPipeline);
117115
}
118116

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

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()));
158+
protected virtual void Dispose(bool disposing)
159+
{
160+
if (!_disposedValue)
161+
{
162+
if (disposing)
163+
{
164+
_apduPipeline.Cleanup();
165+
_smartCardConnection.Dispose();
166+
}
167+
168+
_disposedValue = true;
169+
}
170+
}
142171

143172
private IApduTransform AddResponseChainingTransform(IApduTransform pipeline) =>
144173
IsOath
@@ -170,55 +199,20 @@ private void SelectApplication()
170199
CultureInfo.CurrentCulture,
171200
ExceptionMessages.SmartCardPipelineSetupFailed,
172201
responseApdu.SW))
173-
{
174-
SW = responseApdu.SW
175-
};
202+
{
203+
SW = responseApdu.SW
204+
};
176205
}
177206

178207
// Set the instance property SelectApplicationData
179208
var response = selectApplicationCommand.CreateResponseForApdu(responseApdu);
180209
SelectApplicationData = response.GetData();
181210
}
182211

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-
}
212+
private bool GetIsOauth() =>
213+
_yubiKeyApplication == YubiKeyApplication.Oath ||
214+
(_applicationId != null &&
215+
_applicationId.SequenceEqual(
216+
YubiKeyApplication.Oath.GetIso7816ApplicationId()));
223217
}
224218
}
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+
}

0 commit comments

Comments
 (0)