Skip to content

Commit 03f746a

Browse files
committed
Merge branch 'main' into release/2.8.1
2 parents 3792cc0 + fa4756e commit 03f746a

File tree

13 files changed

+457
-40
lines changed

13 files changed

+457
-40
lines changed

core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2024 Yubico.
2+
* Copyright (C) 2024-2025 Yubico.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,12 +23,24 @@
2323
import java.util.Arrays;
2424

2525
public class ScpProcessor extends ChainedResponseProcessor {
26+
27+
private static final int SHORT_APDU_MAX_CHUNK = 255;
28+
2629
private final ScpState state;
30+
private final boolean usingShortApdus;
31+
private final ApduFormatProcessor extendedProcessor;
2732

2833
ScpProcessor(
29-
SmartCardConnection connection, ScpState state, int maxApduSize, byte insSendRemaining) {
30-
super(connection, true, maxApduSize, insSendRemaining);
34+
SmartCardConnection connection,
35+
boolean extendedApdus,
36+
ScpState state,
37+
int maxApduSize,
38+
byte insSendRemaining) {
39+
super(connection, extendedApdus, maxApduSize, insSendRemaining);
3140
this.state = state;
41+
this.usingShortApdus = !extendedApdus;
42+
this.extendedProcessor =
43+
usingShortApdus ? new ExtendedApduProcessor(connection, maxApduSize) : super.processor;
3244
}
3345

3446
@Override
@@ -47,9 +59,7 @@ public ApduResponse sendApdu(Apdu apdu, boolean encrypt)
4759
// Calculate and add MAC to data
4860
byte[] macedData = new byte[data.length + 8];
4961
System.arraycopy(data, 0, macedData, 0, data.length);
50-
byte[] apduData =
51-
processor.formatApdu(
52-
cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, 0, macedData.length, 0);
62+
byte[] apduData = formatApduData(cla, apdu, macedData);
5363
byte[] mac = state.mac(Arrays.copyOf(apduData, apduData.length - 8));
5464
System.arraycopy(mac, 0, macedData, macedData.length - 8, 8);
5565

@@ -69,4 +79,14 @@ public ApduResponse sendApdu(Apdu apdu, boolean encrypt)
6979
return new ApduResponse(
7080
ByteBuffer.allocate(respData.length + 2).put(respData).putShort(resp.getSw()).array());
7181
}
82+
83+
private byte[] formatApduData(byte cla, Apdu apdu, byte[] macedData) {
84+
if (usingShortApdus && macedData.length > SHORT_APDU_MAX_CHUNK) {
85+
return extendedProcessor.formatApdu(
86+
cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, 0, macedData.length, 0);
87+
} else {
88+
return processor.formatApdu(
89+
cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, 0, macedData.length, 0);
90+
}
91+
}
7292
}

core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ public class SmartCardProtocol implements Closeable {
5252

5353
private ApduProcessor processor;
5454

55+
public static class Configuration {
56+
57+
public static final Configuration DEFAULT = new Builder().setForceShortApdus(false).build();
58+
59+
final boolean forceShortApdus;
60+
61+
private Configuration(Builder builder) {
62+
this.forceShortApdus = builder.forceShortApdus;
63+
}
64+
65+
public static class Builder {
66+
private boolean forceShortApdus = false;
67+
68+
public Builder setForceShortApdus(boolean forceShortApdus) {
69+
this.forceShortApdus = forceShortApdus;
70+
return this;
71+
}
72+
73+
public Configuration build() {
74+
return new Configuration(this);
75+
}
76+
}
77+
}
78+
5579
/**
5680
* Create new instance of {@link SmartCardProtocol} and selects the application for use
5781
*
@@ -84,18 +108,28 @@ public void close() throws IOException {
84108
}
85109

86110
/**
87-
* Enable all relevant settings and workarounds given the firmware version of the YubiKey.
111+
* Enable all relevant settings and workarounds given the firmware version of the YubiKey. Uses a
112+
* default configuration.
88113
*
89114
* @param firmwareVersion the firmware version to use to configure relevant settings
90115
*/
91116
public void configure(Version firmwareVersion) throws IOException {
117+
configure(firmwareVersion, Configuration.DEFAULT);
118+
}
119+
120+
/**
121+
* Enable all relevant settings and workarounds given the firmware version of the YubiKey.
122+
*
123+
* @param firmwareVersion the firmware version to use to configure relevant settings
124+
*/
125+
public void configure(Version firmwareVersion, Configuration configuration) throws IOException {
92126
if (connection.getTransport() == Transport.USB
93127
&& firmwareVersion.isAtLeast(4, 2, 0)
94128
&& firmwareVersion.isLessThan(4, 2, 7)) {
95129
//noinspection deprecation
96130
setEnableTouchWorkaround(true);
97-
} else if (firmwareVersion.isAtLeast(4, 0, 0) && !(processor instanceof ScpProcessor)) {
98-
extendedApdus = connection.isExtendedLengthApduSupported();
131+
} else if (firmwareVersion.isAtLeast(4, 0, 0)) {
132+
extendedApdus = !configuration.forceShortApdus && connection.isExtendedLengthApduSupported();
99133
maxApduSize = firmwareVersion.isAtLeast(4, 3, 0) ? MaxApduSize.YK4_3 : MaxApduSize.YK4;
100134
resetProcessor(null);
101135
}
@@ -234,11 +268,6 @@ public byte[] sendAndReceive(Apdu command) throws IOException, ApduException {
234268
} else {
235269
throw new IllegalArgumentException("Unsupported ScpKeyParams");
236270
}
237-
if (!connection.isExtendedLengthApduSupported()) {
238-
throw new IllegalStateException("SCP requires extended APDU support");
239-
}
240-
extendedApdus = true;
241-
maxApduSize = MaxApduSize.YK4_3;
242271
return state.getDataEncryptor();
243272
} catch (ApduException e) {
244273
if (e.getSw() == SW.CLASS_NOT_SUPPORTED) {
@@ -251,8 +280,10 @@ public byte[] sendAndReceive(Apdu command) throws IOException, ApduException {
251280
private ScpState initScp03(Scp03KeyParams keyParams)
252281
throws IOException, ApduException, BadResponseException {
253282
Pair<ScpState, byte[]> pair = ScpState.scp03Init(processor, keyParams, null);
283+
// SCP was introduced in FW 5.3, so we can use max APDU size for from YubiKey FW 4.3
254284
ScpProcessor processor =
255-
new ScpProcessor(connection, pair.first, MaxApduSize.YK4_3, insSendRemaining);
285+
new ScpProcessor(
286+
connection, extendedApdus, pair.first, MaxApduSize.YK4_3, insSendRemaining);
256287

257288
// Send EXTERNAL AUTHENTICATE
258289
// P1 = C-DECRYPTION, R-ENCRYPTION, C-MAC, and R-MAC
@@ -267,7 +298,9 @@ private ScpState initScp03(Scp03KeyParams keyParams)
267298
private ScpState initScp11(Scp11KeyParams keyParams)
268299
throws IOException, ApduException, BadResponseException {
269300
ScpState scp = ScpState.scp11Init(processor, keyParams);
270-
resetProcessor(new ScpProcessor(connection, scp, MaxApduSize.YK4_3, insSendRemaining));
301+
// SCP was introduced in FW 5.3, so we can use max APDU size for from YubiKey FW 4.3
302+
resetProcessor(
303+
new ScpProcessor(connection, extendedApdus, scp, MaxApduSize.YK4_3, insSendRemaining));
271304
return scp;
272305
}
273306
}

management/src/main/java/com/yubico/yubikit/management/ManagementSession.java

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -201,19 +201,8 @@ void deviceReset() throws IOException, CommandException {
201201
}
202202
};
203203
}
204-
205-
if (SessionVersionOverride.isDevelopmentVersion(version)) {
206-
try {
207-
logger.debug("Overriding development version...");
208-
version = readDeviceInfo().getVersionQualifier().getVersion();
209-
SessionVersionOverride.set(version);
210-
} catch (CommandException e) {
211-
// failed to read device info where it was expected
212-
throw new IOException("Failed reading device info.", e);
213-
}
214-
}
215-
216-
this.version = version;
204+
this.version = getQualifiedVersion(version);
205+
protocol.configure(version);
217206
logCtor(connection);
218207
}
219208

@@ -228,7 +217,7 @@ void deviceReset() throws IOException, CommandException {
228217
public ManagementSession(OtpConnection connection)
229218
throws IOException, ApplicationNotAvailableException {
230219
OtpProtocol protocol = new OtpProtocol(connection);
231-
version = Version.fromBytes(protocol.readStatus());
220+
Version version = Version.fromBytes(protocol.readStatus());
232221
if (version.isLessThan(3, 0, 0) && version.major != 0) {
233222
throw new ApplicationNotAvailableException(
234223
"Management Application requires YubiKey 3 or later");
@@ -261,6 +250,7 @@ void deviceReset() {
261250
throw new UnsupportedOperationException("deviceReset is only available over CCID");
262251
}
263252
};
253+
this.version = getQualifiedVersion(version);
264254
logCtor(connection);
265255
}
266256

@@ -273,14 +263,14 @@ void deviceReset() {
273263
*/
274264
public ManagementSession(FidoConnection connection) throws IOException {
275265
FidoProtocol protocol = new FidoProtocol(connection);
276-
Version protocolVersion = protocol.getVersion();
277-
if (protocolVersion.major < 4) {
266+
Version version = protocol.getVersion();
267+
if (version.major < 4) {
278268
// Prior to YK4 this was not firmware version
279-
if (!(protocolVersion.major == 0 && protocol.supports(FidoProtocol.Capability.CBOR))) {
280-
protocolVersion = new Version(3, 0, 0);
269+
if (!(version.major == 0 && protocol.supports(FidoProtocol.Capability.CBOR))) {
270+
version = new Version(3, 0, 0);
281271
}
282272
}
283-
version = protocolVersion;
273+
284274
backend =
285275
new Backend<FidoProtocol>(protocol) {
286276
@Override
@@ -304,6 +294,7 @@ void deviceReset() {
304294
throw new UnsupportedOperationException("deviceReset is only available over CCID");
305295
}
306296
};
297+
this.version = getQualifiedVersion(version);
307298
logCtor(connection);
308299
}
309300

@@ -529,4 +520,29 @@ private static byte[] pagePayload(int page) {
529520
}
530521
return new byte[] {(byte) page};
531522
}
523+
524+
/**
525+
* Retrieves the versionQualifier version of the YubiKey device.
526+
*
527+
* <p>If the provided version is identified as a development version, this method overrides it by
528+
* with version from the versionQualifier.
529+
*
530+
* @param version the initial version of the YubiKey device.
531+
* @return the version override.
532+
* @throws IOException if reading the device information fails.
533+
*/
534+
private Version getQualifiedVersion(Version version) throws IOException {
535+
if (!SessionVersionOverride.isDevelopmentVersion(version)) {
536+
return version;
537+
}
538+
try {
539+
logger.debug("Overriding development version...");
540+
Version result = readDeviceInfo().getVersionQualifier().getVersion();
541+
SessionVersionOverride.set(result);
542+
return result;
543+
} catch (CommandException e) {
544+
// failed to read device info where it was expected
545+
throw new IOException("Failed reading device info.", e);
546+
}
547+
}
532548
}

testing-android/src/androidTest/java/com/yubico/yubikit/testing/DeviceTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.yubico.yubikit.testing;
1818

19+
import com.yubico.yubikit.testing.core.SmartCardProtocolInstrumentedTests;
1920
import com.yubico.yubikit.testing.fido.FidoTests;
2021
import com.yubico.yubikit.testing.mpe.MultiProtocolResetTests;
2122
import com.yubico.yubikit.testing.oath.OathTests;
@@ -29,8 +30,6 @@
2930
* All integration tests for Security domain, PIV, OpenPGP, OATH, FIDO2 and MPE.
3031
*
3132
* <p>The YubiKey applications will be reset several times.
32-
*
33-
* <p>
3433
*/
3534
@RunWith(Suite.class)
3635
@Suite.SuiteClasses({
@@ -39,6 +38,7 @@
3938
OpenPgpTests.class,
4039
OathTests.class,
4140
MultiProtocolResetTests.class,
42-
FidoTests.class
41+
FidoTests.class,
42+
SmartCardProtocolInstrumentedTests.class
4343
})
4444
public class DeviceTests {}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (C) 2025 Yubico.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.yubico.yubikit.testing.core;
18+
19+
import com.yubico.yubikit.testing.SmokeTest;
20+
import com.yubico.yubikit.testing.framework.CoreInstrumentedTests;
21+
import org.junit.Test;
22+
import org.junit.experimental.categories.Category;
23+
24+
public class SmartCardProtocolInstrumentedTests extends CoreInstrumentedTests {
25+
@Test
26+
@Category(SmokeTest.class)
27+
public void testApduSizesOverScp() throws Throwable {
28+
withState(SmartCardProtocolTests::testApduSizesOverScp);
29+
}
30+
31+
@Test
32+
@Category(SmokeTest.class)
33+
public void testApduSizes() throws Throwable {
34+
withState(SmartCardProtocolTests::testApduSizes);
35+
}
36+
}

testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11Tests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2024 Yubico.
2+
* Copyright (C) 2024-2025 Yubico.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (C) 2025 Yubico.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.yubico.yubikit.testing.framework;
18+
19+
import com.yubico.yubikit.testing.TestState;
20+
import com.yubico.yubikit.testing.core.CoreTestState;
21+
22+
public class CoreInstrumentedTests extends YKInstrumentedTests {
23+
protected void withState(TestState.StatefulDeviceCallback<CoreTestState> callback)
24+
throws Throwable {
25+
final CoreTestState state =
26+
new CoreTestState.Builder(device, usbPid)
27+
.reconnectDeviceCallback(this::reconnectDevice)
28+
.build();
29+
30+
state.withDeviceCallback(callback);
31+
}
32+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (C) 2025 Yubico.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.yubico.yubikit.testing.desktop.core;
18+
19+
import com.yubico.yubikit.testing.core.SmartCardProtocolTests;
20+
import com.yubico.yubikit.testing.desktop.SmokeTest;
21+
import com.yubico.yubikit.testing.desktop.framework.CoreInstrumentedTests;
22+
import org.junit.Test;
23+
import org.junit.experimental.categories.Category;
24+
import org.junit.runner.RunWith;
25+
import org.junit.runners.Suite;
26+
27+
@RunWith(Suite.class)
28+
public class SmartCardProtocolInstrumentedTests extends CoreInstrumentedTests {
29+
@Test
30+
@Category(SmokeTest.class)
31+
public void testApduSizesOverScp() throws Throwable {
32+
withState(SmartCardProtocolTests::testApduSizesOverScp);
33+
}
34+
35+
@Test
36+
@Category(SmokeTest.class)
37+
public void testApduSizes() throws Throwable {
38+
withState(SmartCardProtocolTests::testApduSizes);
39+
}
40+
}

0 commit comments

Comments
 (0)