diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/DeviceTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/DeviceTests.java index f26e782f..4ae28c48 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/DeviceTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/DeviceTests.java @@ -16,6 +16,7 @@ package com.yubico.yubikit.testing; +import com.yubico.yubikit.testing.mpe.MultiProtocolResetTests; import com.yubico.yubikit.testing.oath.OathTests; import com.yubico.yubikit.testing.openpgp.OpenPgpTests; import com.yubico.yubikit.testing.piv.PivTests; @@ -30,7 +31,8 @@ @Suite.SuiteClasses({ PivTests.class, OpenPgpTests.class, - OathTests.class + OathTests.class, + MultiProtocolResetTests.class }) public class DeviceTests { } \ No newline at end of file diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/MultiProtocolResetTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/MultiProtocolResetTests.java deleted file mode 100644 index da1ec151..00000000 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/MultiProtocolResetTests.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2024 Yubico. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.yubico.yubikit.testing; - -import com.yubico.yubikit.testing.framework.YKInstrumentedTests; - -import org.junit.Before; -import org.junit.Test; - -public class MultiProtocolResetTests extends YKInstrumentedTests { - - @Before - public void setupDevice() throws Throwable { - withDevice(MultiProtocolResetDeviceTests::setupDevice); - } - - @Test - public void testSettingPivPinBlocksFidoReset() throws Throwable { - withDevice(MultiProtocolResetDeviceTests::testSettingPivPinBlocksFidoReset); - } - - @Test - public void testPivOperationBlocksFidoReset() throws Throwable { - withDevice(MultiProtocolResetDeviceTests::testPivOperationBlocksFidoReset); - } - - @Test - public void testSettingFidoPinBlocksPivReset() throws Throwable { - withDevice(MultiProtocolResetDeviceTests::testSettingFidoPinBlocksPivReset); - } -} diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinInstrumentedTests.java index 44c7d692..ddda00ef 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinInstrumentedTests.java @@ -22,7 +22,6 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2; -import com.yubico.yubikit.testing.PinComplexityDeviceTests; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -54,12 +53,6 @@ public void testSetPinProtocolV1() throws Throwable { ); } - @Test - public void testPinComplexityPin() throws Throwable { - withCtap2Session( - PinComplexityDeviceTests::testFidoPinComplexity); - } - @Test public void testSetPinProtocolV2() throws Throwable { final PinUvAuthProtocol pinUvAuthProtocol = new PinUvAuthProtocolV2(); diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetTests.java new file mode 100644 index 00000000..0fbb64d6 --- /dev/null +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetTests.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yubico.yubikit.testing.mpe; + +import com.yubico.yubikit.core.smartcard.scp.ScpKid; +import com.yubico.yubikit.testing.framework.MpeInstrumentedTests; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +import javax.annotation.Nullable; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + MultiProtocolResetTests.NoScpTests.class, + MultiProtocolResetTests.Scp11bTests.class, +}) +public class MultiProtocolResetTests { + public static class NoScpTests extends MpeInstrumentedTests { + @Test + public void testSettingPivPinBlocksFidoReset() throws Throwable { + withPivSession(MultiProtocolResetDeviceTests::testSettingPivPinBlocksFidoReset); + } + + @Test + public void testPivOperationBlocksFidoReset() throws Throwable { + withPivSession(MultiProtocolResetDeviceTests::testPivOperationBlocksFidoReset); + } + + @Test + public void testSettingFidoPinBlocksPivReset() throws Throwable { + withCtap2Session(MultiProtocolResetDeviceTests::testSettingFidoPinBlocksPivReset); + } + } + + public static class Scp11bTests extends NoScpTests { + @Nullable + @Override + protected Byte getScpKid() { + return ScpKid.SCP11b; + } + } +} diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/oath/OathTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/oath/OathTests.java index 03b3ea28..9fdcb707 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/oath/OathTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/oath/OathTests.java @@ -37,8 +37,7 @@ public static class NoScpTests extends OathInstrumentedTests { @Test @Category(SmokeTest.class) public void testChangePassword() throws Throwable { - withOathSession(OathDeviceTests::testChangePassword); - withOathSession(OathDeviceTests::testChangePasswordAfterReconnect); + withDevice(OathDeviceTests::testChangePassword); } @Test diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/openpgp/OpenPgpTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/openpgp/OpenPgpTests.java index 9cd59860..9c8343c2 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/openpgp/OpenPgpTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/openpgp/OpenPgpTests.java @@ -16,10 +16,7 @@ package com.yubico.yubikit.testing.openpgp; -import javax.annotation.Nullable; - import com.yubico.yubikit.core.smartcard.scp.ScpKid; -import com.yubico.yubikit.testing.PinComplexityDeviceTests; import com.yubico.yubikit.testing.SlowTest; import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.OpenPgpInstrumentedTests; @@ -29,6 +26,8 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; +import javax.annotation.Nullable; + @RunWith(Suite.class) @Suite.SuiteClasses({ OpenPgpTests.NoScpTests.class, @@ -141,7 +140,7 @@ public void testSetUif() throws Throwable { @Test public void testPinComplexity() throws Throwable { - withOpenPgpSession(PinComplexityDeviceTests::testOpenPgpPinComplexity); + withOpenPgpSession(OpenPgpDeviceTests::testPinComplexity); } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivTests.java index 505bc626..4d24bad6 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivTests.java @@ -17,7 +17,6 @@ package com.yubico.yubikit.testing.piv; import com.yubico.yubikit.core.smartcard.scp.ScpKid; -import com.yubico.yubikit.testing.PinComplexityDeviceTests; import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.PivInstrumentedTests; @@ -71,7 +70,7 @@ public void testPutCompressedCertificate() throws Throwable { @Test public void testPinComplexity() throws Throwable { - withPivSession(PinComplexityDeviceTests::testPivPinComplexity); + withPivSession(PivPinComplexityDeviceTests::testPinComplexity); } } diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/MpeInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/MpeInstrumentedTests.java new file mode 100755 index 00000000..a751989a --- /dev/null +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/MpeInstrumentedTests.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022-2024 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yubico.yubikit.testing.framework; + +import com.yubico.yubikit.fido.ctap.Ctap2Session; +import com.yubico.yubikit.piv.PivSession; +import com.yubico.yubikit.testing.TestState; +import com.yubico.yubikit.testing.mpe.MpeTestState; + +public class MpeInstrumentedTests extends YKInstrumentedTests { + + protected void withPivSession(TestState.StatefulSessionCallback callback) throws Throwable { + final MpeTestState state = new MpeTestState.Builder(device).scpKid(getScpKid()).build(); + state.withPiv(callback); + } + + protected void withCtap2Session(TestState.StatefulSessionCallback callback) throws Throwable { + final MpeTestState state = new MpeTestState.Builder(device).scpKid(getScpKid()).build(); + state.withCtap2(callback); + } +} \ No newline at end of file diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/OathInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/OathInstrumentedTests.java index 6631f85d..2b47c405 100644 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/OathInstrumentedTests.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/OathInstrumentedTests.java @@ -16,42 +16,23 @@ package com.yubico.yubikit.testing.framework; -import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.oath.OathSession; -import com.yubico.yubikit.testing.StaticTestState; -import com.yubico.yubikit.testing.oath.OathTestUtils; - -import org.junit.Before; +import com.yubico.yubikit.testing.TestState; +import com.yubico.yubikit.testing.oath.OathTestState; public class OathInstrumentedTests extends YKInstrumentedTests { - public interface Callback { - void invoke(OathSession value) throws Throwable; - } - - private boolean shouldVerifyAndSetupSession = true; + protected void withDevice(TestState.StatefulDeviceCallback callback) throws Throwable { + final OathTestState state = new OathTestState.Builder(device) + .scpKid(getScpKid()) + .reconnectDeviceCallback(this::reconnectDevice) + .build(); - @Before - public void initializeDeviceTest() { - shouldVerifyAndSetupSession = true; + state.withDeviceCallback(callback); } - /** - * This method can be called several times during one test. - *

- * It will reset and setup the OATH session only the first time it is called. - * The subsequent calls will not reset the device. This simulates YubiKey disconnecting/connecting. - */ - protected void withOathSession(Callback callback) throws Throwable { - if (shouldVerifyAndSetupSession) { - OathTestUtils.verifyAndSetup(device); - shouldVerifyAndSetupSession = false; - } else { - OathTestUtils.updateFipsApprovedValue(device); - } - - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - callback.invoke(new OathSession(connection, StaticTestState.scpParameters.getKeyParams())); - } + protected void withOathSession(TestState.StatefulSessionCallback callback) throws Throwable { + final OathTestState state = new OathTestState.Builder(device).scpKid(getScpKid()).build(); + state.withOath(callback); } } \ No newline at end of file diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/OpenPgpInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/OpenPgpInstrumentedTests.java index c7344500..ab29203c 100644 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/OpenPgpInstrumentedTests.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/OpenPgpInstrumentedTests.java @@ -16,23 +16,13 @@ package com.yubico.yubikit.testing.framework; -import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.openpgp.OpenPgpSession; -import com.yubico.yubikit.testing.StaticTestState; -import com.yubico.yubikit.testing.openpgp.OpenPgpTestUtils; +import com.yubico.yubikit.testing.TestState; +import com.yubico.yubikit.testing.openpgp.OpenPgpTestState; public class OpenPgpInstrumentedTests extends YKInstrumentedTests { - - public interface Callback { - void invoke(OpenPgpSession value) throws Throwable; - } - - protected void withOpenPgpSession(Callback callback) throws Throwable { - - OpenPgpTestUtils.verifyAndSetup(device); - - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - callback.invoke(new OpenPgpSession(connection, StaticTestState.scpParameters.getKeyParams())); - } + protected void withOpenPgpSession(TestState.StatefulSessionCallback callback) throws Throwable { + final OpenPgpTestState state = new OpenPgpTestState.Builder(device).scpKid(getScpKid()).build(); + state.withOpenPgp(callback); } } \ No newline at end of file diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/PivInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/PivInstrumentedTests.java index 6aec3a77..05d767bf 100755 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/PivInstrumentedTests.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/PivInstrumentedTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 Yubico. + * Copyright (C) 2022-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,25 +16,13 @@ package com.yubico.yubikit.testing.framework; -import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.piv.PivSession; -import com.yubico.yubikit.testing.StaticTestState; -import com.yubico.yubikit.testing.piv.PivTestUtils; - +import com.yubico.yubikit.testing.TestState; +import com.yubico.yubikit.testing.piv.PivTestState; public class PivInstrumentedTests extends YKInstrumentedTests { - - public interface Callback { - void invoke(PivSession value) throws Throwable; - } - - protected void withPivSession(Callback callback) throws Throwable { - - PivTestUtils.verifyAndSetup(device); - - try (SmartCardConnection c = device.openConnection(SmartCardConnection.class)) { - PivSession pivSession = new PivSession(c, StaticTestState.scpParameters.getKeyParams()); - callback.invoke(pivSession); - } + protected void withPivSession(TestState.StatefulSessionCallback callback) throws Throwable { + final PivTestState state = new PivTestState.Builder(device).scpKid(getScpKid()).build(); + state.withPiv(callback); } } \ No newline at end of file diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/YKInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/YKInstrumentedTests.java index 46bca52f..0fe5b597 100755 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/YKInstrumentedTests.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/YKInstrumentedTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 Yubico. + * Copyright (C) 2022-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,8 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule; +import com.yubico.yubikit.core.Transport; import com.yubico.yubikit.core.YubiKeyDevice; -import com.yubico.yubikit.testing.ScpParameters; -import com.yubico.yubikit.testing.StaticTestState; import com.yubico.yubikit.testing.TestActivity; import org.junit.After; @@ -45,8 +44,6 @@ public class YKInstrumentedTests { public void getYubiKey() throws InterruptedException { scenarioRule.getScenario().onActivity((TestActivity activity) -> this.activity = activity); device = activity.awaitSession(getClass().getSimpleName(), name.getMethodName()); - StaticTestState.scpParameters = new ScpParameters(device, getScpKid()); - StaticTestState.currentDevice = device; } @After @@ -58,16 +55,18 @@ public void releaseYubiKey() throws InterruptedException { activity.returnSession(device); device = null; activity = null; - StaticTestState.currentDevice = null; - StaticTestState.scpParameters = null; } - public interface Callback { - void invoke(YubiKeyDevice value) throws Throwable; - } - - protected void withDevice(Callback callback) throws Throwable { - callback.invoke(device); + protected YubiKeyDevice reconnectDevice() { + try { + if (device.getTransport() == Transport.NFC) { + releaseYubiKey(); + getYubiKey(); + } + return device; + } catch (InterruptedException e) { + throw new RuntimeException("Failure during reconnect", e); + } } @Nullable diff --git a/testing/src/main/java/com/yubico/yubikit/testing/StaticTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/MpeUtils.java similarity index 62% rename from testing/src/main/java/com/yubico/yubikit/testing/StaticTestState.java rename to testing/src/main/java/com/yubico/yubikit/testing/MpeUtils.java index e7b29d00..469be307 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/StaticTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/MpeUtils.java @@ -16,9 +16,13 @@ package com.yubico.yubikit.testing; -import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.management.DeviceInfo; +import com.yubico.yubikit.support.DeviceUtil; -public class StaticTestState { - public static YubiKeyDevice currentDevice; - public static ScpParameters scpParameters; +public class MpeUtils { + public static boolean isMpe(DeviceInfo deviceInfo) { + final String name = DeviceUtil.getName(deviceInfo, null); + return name.equals("YubiKey Bio - Multi-protocol Edition") || + name.equals("YubiKey C Bio - Multi-protocol Edition"); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/MultiProtocolResetDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/MultiProtocolResetDeviceTests.java deleted file mode 100755 index b09c19fc..00000000 --- a/testing/src/main/java/com/yubico/yubikit/testing/MultiProtocolResetDeviceTests.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2024 Yubico. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.yubico.yubikit.testing; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import com.yubico.yubikit.core.YubiKeyDevice; -import com.yubico.yubikit.core.application.CommandException; -import com.yubico.yubikit.core.fido.FidoConnection; -import com.yubico.yubikit.core.keys.PrivateKeyValues; -import com.yubico.yubikit.core.smartcard.SmartCardConnection; -import com.yubico.yubikit.fido.ctap.ClientPin; -import com.yubico.yubikit.fido.ctap.Ctap2Session; -import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; -import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2; -import com.yubico.yubikit.management.Capability; -import com.yubico.yubikit.management.DeviceInfo; -import com.yubico.yubikit.management.ManagementSession; -import com.yubico.yubikit.piv.KeyType; -import com.yubico.yubikit.piv.PinPolicy; -import com.yubico.yubikit.piv.PivSession; -import com.yubico.yubikit.piv.Slot; -import com.yubico.yubikit.piv.TouchPolicy; -import com.yubico.yubikit.support.DeviceUtil; -import com.yubico.yubikit.testing.piv.PivTestUtils; - -import org.bouncycastle.util.encoders.Hex; -import org.junit.Assume; - -import java.io.IOException; -import java.security.KeyPair; -import java.util.Objects; - -public class MultiProtocolResetDeviceTests { - - /** - * Verifies that this is a Bio multi-protocol device and resets it - */ - public static void setupDevice(YubiKeyDevice device) throws IOException, CommandException { - checkDevice(device); - resetDevice(device); - - assertFalse(isPivResetBlocked(device)); - assertFalse(isFidoResetBlocked(device)); - } - - public static void testSettingPivPinBlocksFidoReset(YubiKeyDevice device) throws IOException, CommandException { - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - PivSession piv = new PivSession(connection); - piv.changePin("123456".toCharArray(), "multipin".toCharArray()); - - assertFalse(isPivResetBlocked(device)); - assertTrue(isFidoResetBlocked(device)); - } - } - - public static void testPivOperationBlocksFidoReset(YubiKeyDevice device) throws IOException, CommandException { - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - PivSession piv = new PivSession(connection); - KeyPair rsaKeyPair = PivTestUtils.loadKey(KeyType.RSA1024); - piv.authenticate(Hex.decode("010203040506070801020304050607080102030405060708")); - piv.putKey(Slot.RETIRED1, PrivateKeyValues.fromPrivateKey(rsaKeyPair.getPrivate()), PinPolicy.DEFAULT, TouchPolicy.DEFAULT); - - assertFalse(isPivResetBlocked(device)); - assertTrue(isFidoResetBlocked(device)); - } - } - - public static void testSettingFidoPinBlocksPivReset(YubiKeyDevice device) throws IOException, CommandException { - try (FidoConnection connection = device.openConnection(FidoConnection.class)) { - Ctap2Session ctap2 = new Ctap2Session(connection); - - PinUvAuthProtocol pinUvAuthProtocol = new PinUvAuthProtocolV2(); - // note that max PIN length is 8 because it is shared with PIV - char[] defaultPin = "11234567".toCharArray(); - - Ctap2Session.InfoData info = ctap2.getCachedInfo(); - ClientPin pin = new ClientPin(ctap2, pinUvAuthProtocol); - boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); - assertFalse(pinSet); - pin.setPin(defaultPin); - - assertTrue(isPivResetBlocked(device)); - assertFalse(isFidoResetBlocked(device)); - } - } - - private static void checkDevice(YubiKeyDevice device) throws IOException, CommandException { - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - ManagementSession management = new ManagementSession(connection); - DeviceInfo deviceInfo = management.getDeviceInfo(); - String name = DeviceUtil.getName(deviceInfo, null); - Assume.assumeTrue("This device (" + name + ") is not suitable for this test", - name.equals("YubiKey Bio - Multi-protocol Edition") || - name.equals("YubiKey C Bio - Multi-protocol Edition")); - } - } - - private static void resetDevice(YubiKeyDevice device) throws IOException, CommandException { - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - ManagementSession management = new ManagementSession(connection); - management.deviceReset(); - } - } - - private static int getResetBlocked(YubiKeyDevice device) throws IOException, CommandException { - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - ManagementSession managementSession = new ManagementSession(connection); - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - return deviceInfo.getResetBlocked(); - } - } - - private static boolean isPivResetBlocked(YubiKeyDevice device) throws IOException, CommandException { - int resetBlocked = getResetBlocked(device); - return (resetBlocked & Capability.PIV.bit) == Capability.PIV.bit; - } - - private static boolean isFidoResetBlocked(YubiKeyDevice device) throws IOException, CommandException { - int resetBlocked = getResetBlocked(device); - return (resetBlocked & Capability.FIDO2.bit) == Capability.FIDO2.bit; - } - -} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java deleted file mode 100755 index bb5f1456..00000000 --- a/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2024 Yubico. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.yubico.yubikit.testing; - -import static com.yubico.yubikit.core.fido.CtapException.ERR_PIN_POLICY_VIOLATION; -import static com.yubico.yubikit.core.smartcard.SW.CONDITIONS_NOT_SATISFIED; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeTrue; - -import com.yubico.yubikit.core.application.CommandException; -import com.yubico.yubikit.core.fido.CtapException; -import com.yubico.yubikit.core.smartcard.ApduException; -import com.yubico.yubikit.fido.ctap.ClientPin; -import com.yubico.yubikit.fido.ctap.Ctap2Session; -import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; -import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2; -import com.yubico.yubikit.management.DeviceInfo; -import com.yubico.yubikit.openpgp.OpenPgpSession; -import com.yubico.yubikit.piv.PivSession; - -import org.bouncycastle.util.encoders.Hex; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; -import org.junit.Assert; - -import java.io.IOException; -import java.util.Objects; - -public class PinComplexityDeviceTests { - - private static void verifyDevice() { - final DeviceInfo deviceInfo = TestUtils.getDeviceInfo(StaticTestState.currentDevice); - assumeTrue("Device does not support PIN complexity", deviceInfo != null); - assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); - } - - /** - * For this test, one needs a key with PIN complexity set on. The test will change PINs. - *

- * The test will verify that trying to set a weak PIN for PIV produces expected exceptions. - * - * @see DeviceInfo#getPinComplexity() - */ - public static void testPivPinComplexity(PivSession piv) throws Throwable { - - verifyDevice(); - - piv.reset(); - piv.authenticate(Hex.decode("010203040506070801020304050607080102030405060708")); - - char[] defaultPin = "123456".toCharArray(); - - piv.verifyPin(defaultPin); - - MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(3)); - - // try to change to pin which breaks PIN complexity - char[] weakPin = "33333333".toCharArray(); - try { - piv.changePin(defaultPin, weakPin); - Assert.fail("Set weak PIN"); - } catch (ApduException apduException) { - if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { - Assert.fail("Unexpected exception:" + apduException.getMessage()); - } - } catch (Exception e) { - Assert.fail("Unexpected exception:" + e.getMessage()); - } - - piv.verifyPin(defaultPin); - - // change to complex pin - char[] complexPin = "11234567".toCharArray(); - try { - piv.changePin(defaultPin, complexPin); - } catch (Exception e) { - Assert.fail("Unexpected exception:" + e.getMessage()); - } - - piv.verifyPin(complexPin); - } - - - /** - * For this test, one needs a key with PIN complexity set on. The test will change PINs. - *

- * The test will verify that trying to set a weak user PIN for OpenPgp produces expected exceptions. - * - * @see DeviceInfo#getPinComplexity() - */ - public static void testOpenPgpPinComplexity(OpenPgpSession openpgp) throws Throwable { - - verifyDevice(); - - char[] currentPin = "123456".toCharArray(); - openpgp.reset(); - openpgp.verifyUserPin(currentPin, false); - - char[] weakPin = "33333333".toCharArray(); - try { - openpgp.changeUserPin(currentPin, weakPin); - } catch (ApduException apduException) { - if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { - fail("Unexpected exception"); - } - } catch (Exception e) { - fail("Unexpected exception"); - } - - // set complex pin - char[] complexPin = "11234567".toCharArray(); - try { - openpgp.changeUserPin(currentPin, complexPin); - } catch (Exception e) { - Assert.fail("Unexpected exception"); - } - } - - public static void testFidoPinComplexity(Ctap2Session ctap2, Object... ignoredArgs) - throws IOException, CommandException { - - PinUvAuthProtocol pinUvAuthProtocol = new PinUvAuthProtocolV2(); - - char[] defaultPin = "11234567".toCharArray(); - - Ctap2Session.InfoData info = ctap2.getCachedInfo(); - ClientPin pin = new ClientPin(ctap2, pinUvAuthProtocol); - boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); - - if (!pinSet) { - pin.setPin(defaultPin); - } else { - pin.getPinToken( - defaultPin, - ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA, - "localhost"); - } - - assertThat(pin.getPinUvAuth().getVersion(), is(pinUvAuthProtocol.getVersion())); - assertThat(pin.getPinRetries().getCount(), is(8)); - - char[] weakPin = "33333333".toCharArray(); - try { - pin.changePin(defaultPin, weakPin); - fail("Weak PIN was accepted"); - } catch (CtapException e) { - assertThat(e.getCtapError(), is(ERR_PIN_POLICY_VIOLATION)); - } - - char[] strongPin = "STRONG PIN".toCharArray(); - pin.changePin(defaultPin, strongPin); - pin.changePin(strongPin, defaultPin); - - assertThat(pin.getPinRetries().getCount(), is(8)); - } -} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java new file mode 100644 index 00000000..2b06152f --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2024 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yubico.yubikit.testing; + +import static org.junit.Assume.assumeTrue; + +import com.yubico.yubikit.core.Transport; +import com.yubico.yubikit.core.YubiKeyConnection; +import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.application.ApplicationNotAvailableException; +import com.yubico.yubikit.core.application.ApplicationSession; +import com.yubico.yubikit.core.application.CommandException; +import com.yubico.yubikit.core.fido.FidoConnection; +import com.yubico.yubikit.core.smartcard.ApduException; +import com.yubico.yubikit.core.smartcard.SmartCardConnection; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; +import com.yubico.yubikit.fido.ctap.Ctap2Session; +import com.yubico.yubikit.management.Capability; +import com.yubico.yubikit.management.DeviceInfo; +import com.yubico.yubikit.management.ManagementSession; +import com.yubico.yubikit.oath.OathSession; +import com.yubico.yubikit.openpgp.OpenPgpSession; +import com.yubico.yubikit.piv.PivSession; + +import java.io.IOException; + +import javax.annotation.Nullable; + +public class TestState { + + public static class Builder> { + final protected YubiKeyDevice device; + @Nullable + private Byte scpKid = null; + @Nullable + private ReconnectDeviceCallback reconnectDeviceCallback = null; + + public Builder(YubiKeyDevice device) { + this.device = device; + } + + public T scpKid(@Nullable Byte scpKid) { + this.scpKid = scpKid; + //noinspection unchecked + return (T) this; + } + + public T reconnectDeviceCallback(@Nullable ReconnectDeviceCallback reconnectDeviceCallback) { + this.reconnectDeviceCallback = reconnectDeviceCallback; + //noinspection unchecked + return (T) this; + } + + public TestState build() throws Throwable { + return new TestState(this); + } + } + + protected YubiKeyDevice currentDevice; + protected ScpParameters scpParameters; + @Nullable + public final Byte scpKid; + @Nullable + private final ReconnectDeviceCallback reconnectDeviceCallback; + private final boolean isUsbTransport; + + protected TestState(Builder builder) { + this.currentDevice = builder.device; + this.scpKid = builder.scpKid; + this.scpParameters = new ScpParameters(builder.device, this.scpKid); + this.reconnectDeviceCallback = builder.reconnectDeviceCallback; + this.isUsbTransport = builder.device.getTransport() == Transport.USB; + } + + public boolean isUsbTransport() { + return isUsbTransport; + } + + @SuppressWarnings("unused") + public interface DeviceCallback { + void invoke() throws Throwable; + } + + public interface StatefulDeviceCallback { + void invoke(S state) throws Throwable; + } + + public interface SessionCallback> { + void invoke(T session) throws Throwable; + } + + public interface StatefulSessionCallback, S extends TestState> { + void invoke(T session, S state) throws Throwable; + } + + public interface SessionCallbackT, R> { + R invoke(T session) throws Throwable; + } + + public interface StatefulSessionCallbackT, S extends TestState, R> { + R invoke(T session, S state) throws Throwable; + } + + public interface ReconnectDeviceCallback { + YubiKeyDevice invoke(); + } + + protected void reconnect() { + if (reconnectDeviceCallback != null) { + currentDevice = reconnectDeviceCallback.invoke(); + scpParameters = new ScpParameters(currentDevice, scpKid); + } + } + + public boolean isFipsCapable(DeviceInfo deviceInfo, Capability capability) { + return deviceInfo != null && + (deviceInfo.getFipsCapable() & capability.bit) == capability.bit; + } + + public boolean isFipsCapable(Capability capability) { + return isFipsCapable(getDeviceInfo(), capability); + } + + public boolean isFipsApproved(Capability capability) { + return isFipsApproved(getDeviceInfo(), capability); + } + + public boolean isFipsApproved(DeviceInfo deviceInfo, Capability capability) { + return deviceInfo != null && + (deviceInfo.getFipsApproved() & capability.bit) == capability.bit; + } + + // PIV helpers + public void withPiv(StatefulSessionCallback callback) + throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + final PivSession piv = getPivSession(connection, scpParameters); + assumeTrue("No PIV support", piv != null); + //noinspection unchecked + callback.invoke(piv, (T) this); + } + reconnect(); + } + + @Nullable + protected PivSession getPivSession(SmartCardConnection connection, ScpParameters scpParameters) + throws IOException { + try { + return new PivSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException | ApduException ignored) { + // no PIV support + } + return null; + } + + // CTAP2 helpers + public void withCtap2(StatefulSessionCallback callback) + throws Throwable { + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + //noinspection unchecked + callback.invoke(ctap2, (T) this); + } + reconnect(); + } + + @Nullable + protected Ctap2Session getCtap2Session(YubiKeyConnection connection) + throws IOException, CommandException { + return (connection instanceof FidoConnection) + ? new Ctap2Session((FidoConnection) connection) + : connection instanceof SmartCardConnection + ? new Ctap2Session((SmartCardConnection) connection) + : null; + } + + // OATH helpers + public void withOath(SessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getOathSession(connection, scpParameters)); + } + reconnect(); + } + + public void withOath(StatefulSessionCallback callback) + throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + //noinspection unchecked + callback.invoke(getOathSession(connection, scpParameters), (T) this); + } + reconnect(); + } + + @Nullable + private OathSession getOathSession(SmartCardConnection connection, ScpParameters scpParameters) + throws IOException { + try { + return new OathSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + // no OATH support + } + return null; + } + + // OpenPGP helpers + public void withOpenPgp(StatefulSessionCallback callback) + throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + //noinspection unchecked + callback.invoke(getOpenPgpSession(connection, scpParameters), (T) this); + } + reconnect(); + } + + @Nullable + protected OpenPgpSession getOpenPgpSession(SmartCardConnection connection, ScpParameters scpParameters) + throws IOException, CommandException { + try { + return new OpenPgpSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + // no OpenPgp support + } + return null; + } + + // device helper + public void withDeviceCallback(StatefulDeviceCallback callback) + throws Throwable { + //noinspection unchecked + callback.invoke((T) this); + } + + // connection helpers + protected SmartCardConnection openSmartCardConnection() throws IOException { + if (currentDevice.supportsConnection(SmartCardConnection.class)) { + return currentDevice.openConnection(SmartCardConnection.class); + } + return null; + } + + protected YubiKeyConnection openConnection() throws IOException { + if (currentDevice.supportsConnection(FidoConnection.class)) { + return currentDevice.openConnection(FidoConnection.class); + } + if (currentDevice.supportsConnection(SmartCardConnection.class)) { + return currentDevice.openConnection(SmartCardConnection.class); + } + throw new IllegalArgumentException("Device does not support FIDO or SmartCard connection"); + } + + // common utils + public DeviceInfo getDeviceInfo() { + DeviceInfo deviceInfo = null; + try (YubiKeyConnection connection = openConnection()) { + ManagementSession managementSession = getManagementSession(connection, null); + deviceInfo = managementSession.getDeviceInfo(); + } catch (IOException | CommandException ignored) { + + } + + return deviceInfo; + } + + protected ManagementSession getManagementSession(YubiKeyConnection connection, ScpParameters scpParameters) + throws IOException, CommandException { + ScpKeyParams keyParams = scpParameters != null ? scpParameters.getKeyParams() : null; + ManagementSession session = (connection instanceof FidoConnection) + ? new ManagementSession((FidoConnection) connection) + : connection instanceof SmartCardConnection + ? new ManagementSession((SmartCardConnection) connection, keyParams) + : null; + + if (session == null) { + throw new IllegalArgumentException("Connection does not support ManagementSession"); + } + + return session; + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/TestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/TestUtils.java deleted file mode 100644 index cf9e4254..00000000 --- a/testing/src/main/java/com/yubico/yubikit/testing/TestUtils.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2024 Yubico. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.yubico.yubikit.testing; - -import com.yubico.yubikit.core.YubiKeyConnection; -import com.yubico.yubikit.core.YubiKeyDevice; -import com.yubico.yubikit.core.application.ApplicationSession; -import com.yubico.yubikit.core.application.CommandException; -import com.yubico.yubikit.core.fido.FidoConnection; -import com.yubico.yubikit.core.smartcard.SmartCardConnection; -import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; -import com.yubico.yubikit.management.Capability; -import com.yubico.yubikit.management.DeviceInfo; -import com.yubico.yubikit.management.ManagementSession; - -import java.io.IOException; - -public class TestUtils { - - public static boolean isFipsCapable(DeviceInfo deviceInfo, Capability capability) { - return deviceInfo != null && - (deviceInfo.getFipsCapable() & capability.bit) == capability.bit; - } - - public static boolean isFipsCapable(YubiKeyDevice device, Capability capability) { - return isFipsCapable(TestUtils.getDeviceInfo(device), capability); - } - - public static boolean isFipsApproved(YubiKeyDevice device, Capability capability) { - return isFipsApproved(TestUtils.getDeviceInfo(device), capability); - } - - public static boolean isFipsApproved(DeviceInfo deviceInfo, Capability capability) { - return deviceInfo != null && - (deviceInfo.getFipsApproved() & capability.bit) == capability.bit; - } - - public static DeviceInfo getDeviceInfo(YubiKeyDevice device) { - DeviceInfo deviceInfo = null; - try (YubiKeyConnection connection = openConnection(device)) { - ManagementSession managementSession = getManagementSession(connection, null); - deviceInfo = managementSession.getDeviceInfo(); - } catch (IOException | CommandException ignored) { - - } - - return deviceInfo; - } - - static ManagementSession getManagementSession(YubiKeyConnection connection, ScpParameters scpParameters) - throws IOException, CommandException { - ScpKeyParams keyParams = scpParameters != null ? scpParameters.getKeyParams() : null; - ManagementSession session = (connection instanceof FidoConnection) - ? new ManagementSession((FidoConnection) connection) - : connection instanceof SmartCardConnection - ? new ManagementSession((SmartCardConnection) connection, keyParams) - : null; - - if (session == null) { - throw new IllegalArgumentException("Connection does not support ManagementSession"); - } - - return session; - } - - interface Callback> { - void invoke(T session) throws Throwable; - } - - public static YubiKeyConnection openConnection(YubiKeyDevice device) throws IOException { - if (device.supportsConnection(FidoConnection.class)) { - return device.openConnection(FidoConnection.class); - } - if (device.supportsConnection(SmartCardConnection.class)) { - return device.openConnection(SmartCardConnection.class); - } - throw new IllegalArgumentException("Device does not support FIDO or SmartCard connection"); - } -} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java new file mode 100644 index 00000000..79af5b8e --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yubico.yubikit.testing.mpe; + +import static com.yubico.yubikit.testing.MpeUtils.isMpe; +import static org.junit.Assert.assertFalse; +import static org.junit.Assume.assumeTrue; + +import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.smartcard.SmartCardConnection; +import com.yubico.yubikit.management.Capability; +import com.yubico.yubikit.management.DeviceInfo; +import com.yubico.yubikit.management.ManagementSession; +import com.yubico.yubikit.testing.TestState; + +public class MpeTestState extends TestState { + public static class Builder extends TestState.Builder { + + public Builder(YubiKeyDevice device) { + super(device); + } + + public MpeTestState build() throws Throwable { + return new MpeTestState(this); + } + } + + protected MpeTestState(MpeTestState.Builder builder) throws Throwable { + super(builder); + + DeviceInfo deviceInfo = getDeviceInfo(); + assumeTrue("Cannot get device information", deviceInfo != null); + assumeTrue("Not a MPE device", isMpe(deviceInfo)); + + try (SmartCardConnection connection = openSmartCardConnection()) { + final ManagementSession managementSession = getManagementSession(connection, scpParameters); + managementSession.deviceReset(); + } + + // PIV and FIDO2 should not be reset blocked + assertFalse(isPivResetBlocked()); + assertFalse(isFidoResetBlocked()); + } + + boolean isPivResetBlocked() { + final DeviceInfo deviceInfo = getDeviceInfo(); + return (deviceInfo.getResetBlocked() & Capability.PIV.bit) == Capability.PIV.bit; + } + + boolean isFidoResetBlocked() { + final DeviceInfo deviceInfo = getDeviceInfo(); + return (deviceInfo.getResetBlocked() & Capability.FIDO2.bit) == Capability.FIDO2.bit; + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetDeviceTests.java new file mode 100755 index 00000000..a1f94615 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetDeviceTests.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yubico.yubikit.testing.mpe; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.yubico.yubikit.core.application.CommandException; +import com.yubico.yubikit.core.keys.PrivateKeyValues; +import com.yubico.yubikit.fido.ctap.ClientPin; +import com.yubico.yubikit.fido.ctap.Ctap2Session; +import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; +import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2; +import com.yubico.yubikit.piv.KeyType; +import com.yubico.yubikit.piv.PinPolicy; +import com.yubico.yubikit.piv.PivSession; +import com.yubico.yubikit.piv.Slot; +import com.yubico.yubikit.piv.TouchPolicy; +import com.yubico.yubikit.testing.piv.PivTestUtils; + +import org.bouncycastle.util.encoders.Hex; + +import java.io.IOException; +import java.security.KeyPair; +import java.util.Objects; + +public class MultiProtocolResetDeviceTests { + + public static void testSettingPivPinBlocksFidoReset(PivSession piv, MpeTestState state) throws Throwable { + piv.changePin("123456".toCharArray(), "multipin".toCharArray()); + assertFalse(state.isPivResetBlocked()); + assertTrue(state.isFidoResetBlocked()); + } + + public static void testPivOperationBlocksFidoReset(PivSession piv, MpeTestState state) throws IOException, CommandException { + KeyPair rsaKeyPair = PivTestUtils.loadKey(KeyType.RSA1024); + piv.authenticate(Hex.decode("010203040506070801020304050607080102030405060708")); + piv.putKey(Slot.RETIRED1, + PrivateKeyValues.fromPrivateKey( + rsaKeyPair.getPrivate()), + PinPolicy.DEFAULT, + TouchPolicy.DEFAULT); + + assertFalse(state.isPivResetBlocked()); + assertTrue(state.isFidoResetBlocked()); + } + + public static void testSettingFidoPinBlocksPivReset(Ctap2Session ctap2, MpeTestState state) throws IOException, CommandException { + + PinUvAuthProtocol pinUvAuthProtocol = new PinUvAuthProtocolV2(); + // note that max PIN length is 8 because it is shared with PIV + char[] defaultPin = "11234567".toCharArray(); + + Ctap2Session.InfoData info = ctap2.getCachedInfo(); + ClientPin pin = new ClientPin(ctap2, pinUvAuthProtocol); + boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); + assertFalse(pinSet); + pin.setPin(defaultPin); + + assertTrue(state.isPivResetBlocked()); + assertFalse(state.isFidoResetBlocked()); + } +} + diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathDeviceTests.java index bad78b58..bcac4d47 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathDeviceTests.java @@ -33,33 +33,31 @@ public class OathDeviceTests { - // state of current test - static char[] OATH_PASSWORD = "".toCharArray(); - static boolean FIPS_APPROVED = false; - - // variables used by the test private static final char[] CHANGED_PASSWORD = "12341234".toCharArray(); - public static void testChangePassword(OathSession oath) throws Exception { - assertTrue(oath.isAccessKeySet()); - assertTrue(oath.isLocked()); - assertFalse(oath.unlock(CHANGED_PASSWORD)); - assertTrue(oath.unlock(OATH_PASSWORD)); - oath.setPassword(CHANGED_PASSWORD); - } + public static void testChangePassword(OathTestState state) throws Throwable { - public static void testChangePasswordAfterReconnect(OathSession oath) throws Exception { - assertTrue(oath.isAccessKeySet()); - assertTrue(oath.isLocked()); - assertTrue(oath.unlock(CHANGED_PASSWORD)); + state.withOath(oath -> { + assertTrue(oath.isAccessKeySet()); + assertTrue(oath.isLocked()); + assertFalse(oath.unlock(CHANGED_PASSWORD)); + assertTrue(oath.unlock(state.password)); + oath.setPassword(CHANGED_PASSWORD); + }); + + state.withOath(oath -> { + assertTrue(oath.isAccessKeySet()); + assertTrue(oath.isLocked()); + assertTrue(oath.unlock(CHANGED_PASSWORD)); + }); } - public static void testRemovePassword(OathSession oath) throws Exception { + public static void testRemovePassword(OathSession oath, OathTestState state) throws Exception { assertTrue(oath.isAccessKeySet()); assertTrue(oath.isLocked()); - assertTrue(oath.unlock(OATH_PASSWORD)); + assertTrue(oath.unlock(state.password)); - if (FIPS_APPROVED) { + if (state.isFipsApproved) { // trying remove password from a FIPS approved key throws specific ApduException ApduException apduException = assertThrows(ApduException.class, oath::deleteAccessKey); assertEquals(SW.CONDITIONS_NOT_SATISFIED, apduException.getSw()); @@ -72,8 +70,8 @@ public static void testRemovePassword(OathSession oath) throws Exception { } } - public static void testAccountManagement(OathSession oath) throws Exception { - assertTrue(oath.unlock(OATH_PASSWORD)); + public static void testAccountManagement(OathSession oath, OathTestState state) throws Exception { + assertTrue(oath.unlock(state.password)); List credentials = oath.getCredentials(); assertEquals(0, credentials.size()); final String uri = "otpauth://totp/foobar:bob@example.com?secret=abba"; diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java old mode 100755 new mode 100644 similarity index 66% rename from testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java rename to testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java index 1059d43f..8e2972b9 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java @@ -13,35 +13,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.yubico.yubikit.testing.oath; -import static com.yubico.yubikit.testing.StaticTestState.scpParameters; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; -import com.yubico.yubikit.core.Transport; import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.core.application.ApplicationNotAvailableException; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.management.Capability; import com.yubico.yubikit.oath.OathSession; -import com.yubico.yubikit.testing.TestUtils; +import com.yubico.yubikit.testing.TestState; + +public class OathTestState extends TestState { + public boolean isFipsApproved; + public char[] password; -public class OathTestUtils { + public static class Builder extends TestState.Builder { - public static void updateFipsApprovedValue(YubiKeyDevice device) throws Throwable { - OathDeviceTests.FIPS_APPROVED = TestUtils.isFipsApproved(device, Capability.OATH); + public Builder(YubiKeyDevice device) { + super(device); + } + + public OathTestState build() throws Throwable { + return new OathTestState(this); + } } - public static void verifyAndSetup(YubiKeyDevice device) throws Throwable { + protected OathTestState(OathTestState.Builder builder) throws Throwable { + super(builder); - OathDeviceTests.OATH_PASSWORD = "".toCharArray(); + password = "".toCharArray(); - boolean isOathFipsCapable = TestUtils.isFipsCapable(device, Capability.OATH); + boolean isOathFipsCapable = isFipsCapable(Capability.OATH); if (scpParameters.getKid() == null && isOathFipsCapable) { - assumeTrue("Trying to use OATH FIPS capable device over NFC without SCP", - device.getTransport() != Transport.NFC); + assumeTrue("Trying to use OATH FIPS capable device over NFC without SCP", isUsbTransport()); } if (scpParameters.getKid() != null) { @@ -52,7 +60,7 @@ public static void verifyAndSetup(YubiKeyDevice device) throws Throwable { ); } - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + try (SmartCardConnection connection = openSmartCardConnection()) { OathSession oath = null; try { oath = new OathSession(connection, scpParameters.getKeyParams()); @@ -63,16 +71,16 @@ public static void verifyAndSetup(YubiKeyDevice device) throws Throwable { assumeTrue("OATH not available", oath != null); oath.reset(); - final char[] oathPassword = "112345678".toCharArray(); - oath.setPassword(oathPassword); - OathDeviceTests.OATH_PASSWORD = oathPassword; + final char[] complexPassword = "11234567".toCharArray(); + oath.setPassword(complexPassword); + password = complexPassword; } - updateFipsApprovedValue(device); + isFipsApproved = isFipsApproved(Capability.OATH); // after changing the OATH password, we expect a FIPS capable device to be FIPS approved if (isOathFipsCapable) { - assertTrue("Device not OATH FIPS approved as expected", OathDeviceTests.FIPS_APPROVED); + assertTrue("Device not OATH FIPS approved as expected", isFipsApproved); } } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpDeviceTests.java index c4339819..41470ece 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpDeviceTests.java @@ -16,14 +16,16 @@ package com.yubico.yubikit.testing.openpgp; -import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.ADMIN_PIN; -import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.USER_PIN; +import static com.yubico.yubikit.core.smartcard.SW.CONDITIONS_NOT_SATISFIED; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import com.yubico.yubikit.core.application.InvalidPinException; import com.yubico.yubikit.core.keys.PrivateKeyValues; import com.yubico.yubikit.core.keys.PublicKeyValues; import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.core.smartcard.SW; +import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.openpgp.ExtendedCapabilityFlag; import com.yubico.yubikit.openpgp.Kdf; import com.yubico.yubikit.openpgp.KeyRef; @@ -75,14 +77,15 @@ public class OpenPgpDeviceTests { private static final Logger logger = LoggerFactory.getLogger(OpenPgpDeviceTests.class); private static final List ecdsaCurves = Stream.of(OpenPgpCurve.values()) - .filter(curve -> !Arrays.asList(OpenPgpCurve.Ed25519, OpenPgpCurve.X25519).contains(curve)) + .filter(curve -> !Arrays.asList(OpenPgpCurve.Ed25519, OpenPgpCurve.X25519) + .contains(curve)) .collect(Collectors.toList()); private static int[] getSupportedRsaKeySizes(OpenPgpSession openpgp) { return openpgp.supports(OpenPgpSession.FEATURE_RSA4096_KEYS) ? new int[]{2048, 3072, 4096} : new int[]{2048}; } - public static void testGenerateRequiresAdmin(OpenPgpSession openpgp) throws Exception { + public static void testGenerateRequiresAdmin(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { try { openpgp.generateEcKey(KeyRef.DEC, OpenPgpCurve.BrainpoolP256R1); @@ -91,7 +94,7 @@ public static void testGenerateRequiresAdmin(OpenPgpSession openpgp) throws Exce Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - if (!OpenPgpTestState.FIPS_APPROVED) { + if (!state.isFipsApproved) { try { openpgp.generateRsaKey(KeyRef.DEC, 2048); Assert.fail(); @@ -101,18 +104,18 @@ public static void testGenerateRequiresAdmin(OpenPgpSession openpgp) throws Exce } } - public static void testChangePin(OpenPgpSession openpgp) throws Exception { - openpgp.verifyUserPin(USER_PIN, false); + public static void testChangePin(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { + openpgp.verifyUserPin(state.defaultUserPin, false); Assert.assertThrows(InvalidPinException.class, () -> openpgp.verifyUserPin(CHANGED_PIN, false)); - Assert.assertThrows(InvalidPinException.class, () -> openpgp.changeUserPin(CHANGED_PIN, USER_PIN)); + Assert.assertThrows(InvalidPinException.class, () -> openpgp.changeUserPin(CHANGED_PIN, state.defaultUserPin)); - openpgp.changeUserPin(USER_PIN, CHANGED_PIN); + openpgp.changeUserPin(state.defaultUserPin, CHANGED_PIN); openpgp.verifyUserPin(CHANGED_PIN, false); - openpgp.changeUserPin(CHANGED_PIN, USER_PIN); - openpgp.verifyUserPin(USER_PIN, false); + openpgp.changeUserPin(CHANGED_PIN, state.defaultUserPin); + openpgp.verifyUserPin(state.defaultUserPin, false); } - public static void testResetPin(OpenPgpSession openpgp) throws Exception { + public static void testResetPin(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { int remaining = openpgp.getPinStatus().getAttemptsUser(); for (int i = remaining; i > 0; i--) { try { @@ -125,15 +128,15 @@ public static void testResetPin(OpenPgpSession openpgp) throws Exception { assert openpgp.getPinStatus().getAttemptsUser() == 0; try { - openpgp.resetPin(USER_PIN, null); + openpgp.resetPin(state.defaultUserPin, null); Assert.fail(); } catch (ApduException e) { Assert.assertEquals(e.getSw(), SW.SECURITY_CONDITION_NOT_SATISFIED); } // Reset PIN using Admin PIN - openpgp.verifyAdminPin(ADMIN_PIN); - openpgp.resetPin(USER_PIN, null); + openpgp.verifyAdminPin(state.defaultAdminPin); + openpgp.resetPin(state.defaultUserPin, null); remaining = openpgp.getPinStatus().getAttemptsUser(); assert remaining > 0; for (int i = remaining; i > 0; i--) { @@ -148,15 +151,15 @@ public static void testResetPin(OpenPgpSession openpgp) throws Exception { // Reset PIN using Reset Code openpgp.setResetCode(RESET_CODE); - Assert.assertThrows(InvalidPinException.class, () -> openpgp.resetPin(USER_PIN, CHANGED_PIN)); - openpgp.resetPin(USER_PIN, RESET_CODE); + Assert.assertThrows(InvalidPinException.class, () -> openpgp.resetPin(state.defaultUserPin, CHANGED_PIN)); + openpgp.resetPin(state.defaultUserPin, RESET_CODE); assert openpgp.getPinStatus().getAttemptsUser() > 0; } - public static void testSetPinAttempts(OpenPgpSession openpgp) throws Exception { + public static void testSetPinAttempts(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_PIN_ATTEMPTS)); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); openpgp.setPinAttempts(6, 3, 3); assert openpgp.getPinStatus().getAttemptsUser() == 6; @@ -171,52 +174,52 @@ public static void testSetPinAttempts(OpenPgpSession openpgp) throws Exception { assert openpgp.getPinStatus().getAttemptsUser() == 3; } - public static void testGenerateRsaKeys(OpenPgpSession openpgp) throws Exception { + public static void testGenerateRsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_RSA_GENERATION)); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); for (int keySize : getSupportedRsaKeySizes(openpgp)) { logger.info("RSA key size: {}", keySize); PublicKey publicKey = openpgp.generateRsaKey(KeyRef.SIG, keySize).toPublicKey(); - openpgp.verifyUserPin(USER_PIN, false); + openpgp.verifyUserPin(state.defaultUserPin, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("NONEwithRSA"); verifier.initVerify(publicKey); verifier.update(message); assert verifier.verify(signature); - if (!OpenPgpTestState.FIPS_APPROVED) { + if (!state.isFipsApproved) { publicKey = openpgp.generateRsaKey(KeyRef.DEC, keySize).toPublicKey(); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] cipherText = cipher.doFinal(message); - openpgp.verifyUserPin(USER_PIN, true); + openpgp.verifyUserPin(state.defaultUserPin, true); byte[] decrypted = openpgp.decrypt(cipherText); Assert.assertArrayEquals(message, decrypted); } } } - public static void testGenerateEcKeys(OpenPgpSession openpgp) throws Exception { + public static void testGenerateEcKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); for (OpenPgpCurve curve : ecdsaCurves) { - if (OpenPgpTestState.FIPS_APPROVED) { + if (state.isFipsApproved) { if (curve == OpenPgpCurve.SECP256K1) { continue; } } logger.info("Curve: {}", curve); PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, curve).toPublicKey(); - openpgp.verifyUserPin(USER_PIN, false); + openpgp.verifyUserPin(state.defaultUserPin, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("NONEwithECDSA"); @@ -229,7 +232,7 @@ public static void testGenerateEcKeys(OpenPgpSession openpgp) throws Exception { kpg.initialize(new ECGenParameterSpec(curve.name())); KeyPair pair = kpg.generateKeyPair(); - openpgp.verifyUserPin(USER_PIN, true); + openpgp.verifyUserPin(state.defaultUserPin, true); byte[] actual = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair.getPublic())); KeyAgreement ka = KeyAgreement.getInstance("ECDH"); ka.init(pair.getPrivate()); @@ -239,16 +242,16 @@ public static void testGenerateEcKeys(OpenPgpSession openpgp) throws Exception { } } - public static void testGenerateEd25519(OpenPgpSession openpgp) throws Exception { + public static void testGenerateEd25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, OpenPgpCurve.Ed25519).toPublicKey(); - openpgp.verifyUserPin(USER_PIN, false); + openpgp.verifyUserPin(state.defaultUserPin, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("Ed25519"); @@ -257,20 +260,20 @@ public static void testGenerateEd25519(OpenPgpSession openpgp) throws Exception assert verifier.verify(signature); } - public static void testGenerateX25519(OpenPgpSession openpgp) throws Exception { + public static void testGenerateX25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); - Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", OpenPgpTestState.FIPS_APPROVED); + Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", state.isFipsApproved); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); PublicKey publicKey = openpgp.generateEcKey(KeyRef.DEC, OpenPgpCurve.X25519).toPublicKey(); KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519"); KeyPair pair = kpg.generateKeyPair(); - openpgp.verifyUserPin(USER_PIN, true); + openpgp.verifyUserPin(state.defaultUserPin, true); byte[] actual = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair.getPublic())); KeyAgreement ka = KeyAgreement.getInstance("XDH"); ka.init(pair.getPrivate()); @@ -279,8 +282,8 @@ public static void testGenerateX25519(OpenPgpSession openpgp) throws Exception { Assert.assertArrayEquals(expected, actual); } - public static void testImportRsaKeys(OpenPgpSession openpgp) throws Exception { - openpgp.verifyAdminPin(ADMIN_PIN); + public static void testImportRsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { + openpgp.verifyAdminPin(state.defaultAdminPin); KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); @@ -294,7 +297,7 @@ public static void testImportRsaKeys(OpenPgpSession openpgp) throws Exception { Assert.assertArrayEquals(pair.getPublic().getEncoded(), encoded); PublicKey publicKey = openpgp.getPublicKey(KeyRef.SIG).toPublicKey(); - openpgp.verifyUserPin(USER_PIN, false); + openpgp.verifyUserPin(state.defaultUserPin, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("NONEwithRSA"); @@ -302,34 +305,34 @@ public static void testImportRsaKeys(OpenPgpSession openpgp) throws Exception { verifier.update(message); assert verifier.verify(signature); - if (!OpenPgpTestState.FIPS_APPROVED) { + if (!state.isFipsApproved) { openpgp.putKey(KeyRef.DEC, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] cipherText = cipher.doFinal(message); - openpgp.verifyUserPin(USER_PIN, true); + openpgp.verifyUserPin(state.defaultUserPin, true); byte[] decrypted = openpgp.decrypt(cipherText); Assert.assertArrayEquals(message, decrypted); } } } - public static void testImportEcDsaKeys(OpenPgpSession openpgp) throws Exception { + public static void testImportEcDsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDSA"); List curves = new ArrayList<>(Arrays.asList(OpenPgpCurve.values())); curves.remove(OpenPgpCurve.Ed25519); curves.remove(OpenPgpCurve.X25519); - if (OpenPgpTestState.FIPS_APPROVED) { + if (state.isFipsApproved) { curves.remove(OpenPgpCurve.SECP256K1); } @@ -342,7 +345,7 @@ public static void testImportEcDsaKeys(OpenPgpSession openpgp) throws Exception PublicKeyValues values = openpgp.getPublicKey(KeyRef.SIG); Assert.assertArrayEquals(pair.getPublic().getEncoded(), values.getEncoded()); PublicKey publicKey = values.toPublicKey(); - openpgp.verifyUserPin(USER_PIN, false); + openpgp.verifyUserPin(state.defaultUserPin, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("NONEwithECDSA"); @@ -357,20 +360,20 @@ public static void testImportEcDsaKeys(OpenPgpSession openpgp) throws Exception ka.doPhase(openpgp.getPublicKey(KeyRef.DEC).toPublicKey(), true); byte[] expected = ka.generateSecret(); - openpgp.verifyUserPin(USER_PIN, true); + openpgp.verifyUserPin(state.defaultUserPin, true); byte[] agreement = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair2.getPublic())); Assert.assertArrayEquals(expected, agreement); } } - public static void testImportEd25519(OpenPgpSession openpgp) throws Exception { + public static void testImportEd25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519"); KeyPair pair = kpg.generateKeyPair(); @@ -378,7 +381,7 @@ public static void testImportEd25519(OpenPgpSession openpgp) throws Exception { byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - openpgp.verifyUserPin(USER_PIN, false); + openpgp.verifyUserPin(state.defaultUserPin, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("Ed25519"); @@ -389,14 +392,14 @@ public static void testImportEd25519(OpenPgpSession openpgp) throws Exception { Assert.assertArrayEquals(pair.getPublic().getEncoded(), openpgp.getPublicKey(KeyRef.SIG).getEncoded()); } - public static void testImportX25519(OpenPgpSession openpgp) throws Exception { + public static void testImportX25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); - Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", OpenPgpTestState.FIPS_APPROVED); + Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", state.isFipsApproved); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519"); KeyPair pair = kpg.generateKeyPair(); @@ -409,27 +412,27 @@ public static void testImportX25519(OpenPgpSession openpgp) throws Exception { ka.doPhase(openpgp.getPublicKey(KeyRef.DEC).toPublicKey(), true); byte[] expected = ka.generateSecret(); - openpgp.verifyUserPin(USER_PIN, true); + openpgp.verifyUserPin(state.defaultUserPin, true); byte[] agreement = openpgp.decrypt(PublicKeyValues.Ec.fromPublicKey(pair2.getPublic())); Assert.assertArrayEquals(expected, agreement); } - public static void testAttestation(OpenPgpSession openpgp) throws Exception { + public static void testAttestation(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("Attestation support", openpgp.supports(OpenPgpSession.FEATURE_ATTESTATION)); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP256R1).toPublicKey(); - openpgp.verifyUserPin(USER_PIN, false); + openpgp.verifyUserPin(state.defaultUserPin, false); X509Certificate cert = openpgp.attestKey(KeyRef.SIG); Assert.assertEquals(publicKey, cert.getPublicKey()); } - public static void testSigPinPolicy(OpenPgpSession openpgp) throws Exception { - openpgp.verifyAdminPin(ADMIN_PIN); + public static void testSigPinPolicy(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { + openpgp.verifyAdminPin(state.defaultAdminPin); KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); @@ -446,7 +449,7 @@ public static void testSigPinPolicy(OpenPgpSession openpgp) throws Exception { } openpgp.setSignaturePinPolicy(PinPolicy.ALWAYS); - openpgp.verifyUserPin(USER_PIN, false); + openpgp.verifyUserPin(state.defaultUserPin, false); openpgp.sign(message); Assert.assertEquals(1, openpgp.getSignatureCounter()); try { @@ -458,14 +461,14 @@ public static void testSigPinPolicy(OpenPgpSession openpgp) throws Exception { Assert.assertEquals(1, openpgp.getSignatureCounter()); openpgp.setSignaturePinPolicy(PinPolicy.ONCE); - openpgp.verifyUserPin(USER_PIN, false); + openpgp.verifyUserPin(state.defaultUserPin, false); openpgp.sign(message); Assert.assertEquals(2, openpgp.getSignatureCounter()); openpgp.sign(message); Assert.assertEquals(3, openpgp.getSignatureCounter()); } - public static void testKdf(OpenPgpSession openpgp) throws Exception { + public static void testKdf(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("KDF Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.KDF)); // Test setting KDF without admin PIN verification @@ -477,8 +480,8 @@ public static void testKdf(OpenPgpSession openpgp) throws Exception { } // Set a non-default PINs to ensure that they reset - openpgp.changeUserPin(USER_PIN, CHANGED_PIN); - openpgp.changeAdminPin(ADMIN_PIN, CHANGED_PIN); + openpgp.changeUserPin(state.defaultUserPin, CHANGED_PIN); + openpgp.changeAdminPin(state.defaultAdminPin, CHANGED_PIN); openpgp.verifyAdminPin(CHANGED_PIN); openpgp.setKdf( @@ -499,14 +502,14 @@ public static void testKdf(OpenPgpSession openpgp) throws Exception { openpgp.verifyUserPin(Pw.DEFAULT_USER_PIN, false); } - public static void testUnverifyPin(OpenPgpSession openpgp) throws Exception { + public static void testUnverifyPin(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("Unverify PIN Support", openpgp.supports(OpenPgpSession.FEATURE_UNVERIFY_PIN)); KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); KeyPair pair = kpg.generateKeyPair(); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); openpgp.setSignaturePinPolicy(PinPolicy.ONCE); @@ -519,7 +522,7 @@ public static void testUnverifyPin(OpenPgpSession openpgp) throws Exception { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - openpgp.verifyUserPin(USER_PIN, false); + openpgp.verifyUserPin(state.defaultUserPin, false); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); openpgp.sign(message); @@ -533,15 +536,15 @@ public static void testUnverifyPin(OpenPgpSession openpgp) throws Exception { } } - public static void testDeleteKey(OpenPgpSession openpgp) throws Exception { + public static void testDeleteKey(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); KeyPair pair = kpg.generateKeyPair(); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - openpgp.verifyUserPin(USER_PIN, false); + openpgp.verifyUserPin(state.defaultUserPin, false); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); openpgp.sign(message); @@ -554,7 +557,7 @@ public static void testDeleteKey(OpenPgpSession openpgp) throws Exception { } } - public static void testCertificateManagement(OpenPgpSession openpgp) throws Exception { + public static void testCertificateManagement(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); KeyPair pair = kpg.generateKeyPair(); @@ -575,7 +578,7 @@ public static void testCertificateManagement(OpenPgpSession openpgp) throws Exce CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(stream); - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); openpgp.putCertificate(KeyRef.SIG, cert); X509Certificate actual = openpgp.getCertificate(KeyRef.SIG); @@ -586,7 +589,7 @@ public static void testCertificateManagement(OpenPgpSession openpgp) throws Exce Assert.assertNull(openpgp.getCertificate(KeyRef.SIG)); } - public static void testGetChallenge(OpenPgpSession openpgp) throws Exception { + public static void testGetChallenge(OpenPgpSession openpgp, OpenPgpTestState ignored) throws Exception { Assume.assumeTrue("Get Challenge Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.GET_CHALLENGE)); byte[] challenge = openpgp.getChallenge(1); @@ -603,7 +606,7 @@ public static void testGetChallenge(OpenPgpSession openpgp) throws Exception { Assert.assertEquals(255, challenge.length); } - public static void testSetUif(OpenPgpSession openpgp) throws Exception { + public static void testSetUif(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { Assume.assumeTrue("UIF Support", openpgp.supports(OpenPgpSession.FEATURE_UIF)); try { @@ -613,7 +616,7 @@ public static void testSetUif(OpenPgpSession openpgp) throws Exception { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.verifyAdminPin(state.defaultAdminPin); openpgp.setUif(KeyRef.SIG, Uif.ON); Assert.assertEquals(Uif.ON, openpgp.getUif(KeyRef.SIG)); @@ -626,4 +629,46 @@ public static void testSetUif(OpenPgpSession openpgp) throws Exception { // Reset to remove FIXED UIF. openpgp.reset(); } + + /** + * For this test, one needs a key with PIN complexity set on. The test will change PINs. + *

+ * The test will verify that trying to set a weak user PIN for OpenPgp produces expected exceptions. + * + * @see DeviceInfo#getPinComplexity() + */ + public static void testPinComplexity(OpenPgpSession openpgp, OpenPgpTestState state) throws Throwable { + + final DeviceInfo deviceInfo = state.getDeviceInfo(); + assumeTrue("Device does not support PIN complexity", deviceInfo != null); + assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); + + openpgp.reset(); + + // after reset we cannot use the pin values from the state as it has been updated based + // on the connected device + openpgp.verifyUserPin(Pw.DEFAULT_USER_PIN, false); + + char[] weakPin = "33333333".toCharArray(); + try { + openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, weakPin); + } catch (ApduException apduException) { + if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { + fail("Unexpected exception"); + } + } catch (Exception e) { + fail("Unexpected exception"); + } + + // set complex pin + char[] complexPin = "CMPLXPIN".toCharArray(); + try { + openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, complexPin); + } catch (Exception e) { + Assert.fail("Unexpected exception"); + } + + // change the user pin to value stated in the state as it is correct for PIN complexity + openpgp.changeUserPin(complexPin, state.defaultUserPin); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestState.java index d45bccad..b3582e3a 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestState.java @@ -16,10 +16,86 @@ package com.yubico.yubikit.testing.openpgp; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.smartcard.SmartCardConnection; +import com.yubico.yubikit.management.Capability; +import com.yubico.yubikit.management.DeviceInfo; +import com.yubico.yubikit.openpgp.OpenPgpSession; import com.yubico.yubikit.openpgp.Pw; +import com.yubico.yubikit.testing.TestState; + +import org.junit.Assume; + +public class OpenPgpTestState extends TestState { + + private static final char[] COMPLEX_USER_PIN = "112345678".toCharArray(); + private static final char[] COMPLEX_ADMIN_PIN = "112345678".toCharArray(); + + public final boolean isFipsApproved; + public char[] defaultUserPin; + public char[] defaultAdminPin; + + public static class Builder extends TestState.Builder { + + public Builder(YubiKeyDevice device) { + super(device); + } + + public OpenPgpTestState build() throws Throwable { + return new OpenPgpTestState(this); + } + } + + protected OpenPgpTestState(OpenPgpTestState.Builder builder) throws Throwable { + super(builder); + + defaultUserPin = Pw.DEFAULT_USER_PIN; + defaultAdminPin = Pw.DEFAULT_ADMIN_PIN; + + DeviceInfo deviceInfo = getDeviceInfo(); + boolean isOpenPgpFipsCapable = isFipsCapable(deviceInfo, Capability.OPENPGP); + boolean hasPinComplexity = deviceInfo != null && deviceInfo.getPinComplexity(); + + if (scpParameters.getKid() == null && isOpenPgpFipsCapable) { + Assume.assumeTrue("Trying to use OpenPgp FIPS capable device over NFC without SCP", + isUsbTransport()); + } + + if (scpParameters.getKid() != null) { + // skip the test if the connected key does not provide matching SCP keys + Assume.assumeTrue( + "No matching key params found for required kid", + scpParameters.getKeyParams() != null + ); + } + + try (SmartCardConnection connection = openSmartCardConnection()) { + OpenPgpSession openPgp = getOpenPgpSession(connection, scpParameters); + + assumeTrue("OpenPGP not available", openPgp != null); + openPgp.reset(); + + if (hasPinComplexity) { + // only use complex pins if pin complexity is required + openPgp.changeUserPin(defaultUserPin, COMPLEX_USER_PIN); + openPgp.changeAdminPin(defaultAdminPin, COMPLEX_ADMIN_PIN); + defaultUserPin = COMPLEX_USER_PIN; + defaultAdminPin = COMPLEX_ADMIN_PIN; + } + } + + deviceInfo = getDeviceInfo(); + isFipsApproved = isFipsApproved(deviceInfo, Capability.OPENPGP); -class OpenPgpTestState { - static char[] USER_PIN = Pw.DEFAULT_USER_PIN; - static char[] ADMIN_PIN = Pw.DEFAULT_ADMIN_PIN; - static boolean FIPS_APPROVED = false; + // after changing the user and admin PINs, we expect a FIPS capable device + // to be FIPS approved + if (isOpenPgpFipsCapable) { + assertNotNull(deviceInfo); + assertTrue("Device not OpenPgp FIPS approved as expected", isFipsApproved); + } + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java deleted file mode 100755 index a7d940a8..00000000 --- a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2024 Yubico. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.yubico.yubikit.testing.openpgp; - -import static com.yubico.yubikit.testing.StaticTestState.scpParameters; -import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.ADMIN_PIN; -import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.FIPS_APPROVED; -import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.USER_PIN; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeTrue; - -import com.yubico.yubikit.core.Transport; -import com.yubico.yubikit.core.YubiKeyDevice; -import com.yubico.yubikit.core.application.ApplicationNotAvailableException; -import com.yubico.yubikit.core.smartcard.SmartCardConnection; -import com.yubico.yubikit.management.Capability; -import com.yubico.yubikit.management.DeviceInfo; -import com.yubico.yubikit.openpgp.OpenPgpSession; -import com.yubico.yubikit.openpgp.Pw; -import com.yubico.yubikit.testing.TestUtils; - -import org.junit.Assume; - -public class OpenPgpTestUtils { - - private static final char[] COMPLEX_USER_PIN = "112345678".toCharArray(); - private static final char[] COMPLEX_ADMIN_PIN = "112345678".toCharArray(); - - public static void verifyAndSetup(YubiKeyDevice device) throws Throwable { - - OpenPgpTestState.USER_PIN = Pw.DEFAULT_USER_PIN; - OpenPgpTestState.ADMIN_PIN = Pw.DEFAULT_ADMIN_PIN; - - DeviceInfo deviceInfo = TestUtils.getDeviceInfo(device); - boolean isOpenPgpFipsCapable = TestUtils.isFipsCapable(deviceInfo, Capability.OPENPGP); - boolean hasPinComplexity = deviceInfo != null && deviceInfo.getPinComplexity(); - - if (scpParameters.getKid() == null && isOpenPgpFipsCapable) { - Assume.assumeTrue("Trying to use OpenPgp FIPS capable device over NFC without SCP", - device.getTransport() != Transport.NFC); - } - - if (scpParameters.getKid() != null) { - // skip the test if the connected key does not provide matching SCP keys - Assume.assumeTrue( - "No matching key params found for required kid", - scpParameters.getKeyParams() != null - ); - } - - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - OpenPgpSession openPgp = null; - try { - openPgp = new OpenPgpSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - - } - - assumeTrue("OpenPGP not available", openPgp != null); - openPgp.reset(); - - if (hasPinComplexity) { - // only use complex pins if pin complexity is required - openPgp.changeUserPin(USER_PIN, COMPLEX_USER_PIN); - openPgp.changeAdminPin(ADMIN_PIN, COMPLEX_ADMIN_PIN); - OpenPgpTestState.USER_PIN = COMPLEX_USER_PIN; - OpenPgpTestState.ADMIN_PIN = COMPLEX_ADMIN_PIN; - } - } - - deviceInfo = TestUtils.getDeviceInfo(device); - FIPS_APPROVED = TestUtils.isFipsApproved(deviceInfo, Capability.OPENPGP); - - // after changing the user and admin PINs, we expect a FIPS capable device - // to be FIPS approved - if (isOpenPgpFipsCapable) { - assertNotNull(deviceInfo); - assertTrue("Device not OpenPgp FIPS approved as expected", FIPS_APPROVED); - } - } -} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivBioMultiProtocolDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivBioMultiProtocolDeviceTests.java index ba4c88b0..e147d855 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivBioMultiProtocolDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivBioMultiProtocolDeviceTests.java @@ -37,7 +37,7 @@ public class PivBioMultiProtocolDeviceTests { * To run the test, create a PIN and enroll at least one fingerprint. The test will ask twice * for fingerprint authentication. */ - public static void testAuthenticate(PivSession piv) throws IOException, ApduException, InvalidPinException { + public static void testAuthenticate(PivSession piv, PivTestState ignored) throws IOException, ApduException, InvalidPinException { try { BioMetadata bioMetadata = piv.getBioMetadata(); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivCertificateTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivCertificateTests.java index c44763c1..9c8cce67 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivCertificateTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivCertificateTests.java @@ -17,7 +17,6 @@ package com.yubico.yubikit.testing.piv; import static com.yubico.yubikit.piv.PivSession.FEATURE_RSA3072_RSA4096; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_MANAGEMENT_KEY; import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.smartcard.ApduException; @@ -38,16 +37,16 @@ public class PivCertificateTests { private static final Logger logger = LoggerFactory.getLogger(PivCertificateTests.class); - public static void putUncompressedCertificate(PivSession piv) throws IOException, ApduException, CertificateException, BadResponseException { - putCertificate(piv, false); + public static void putUncompressedCertificate(PivSession piv, PivTestState state) throws IOException, ApduException, CertificateException, BadResponseException { + putCertificate(piv, state, false); } - public static void putCompressedCertificate(PivSession piv) throws IOException, ApduException, CertificateException, BadResponseException { - putCertificate(piv, true); + public static void putCompressedCertificate(PivSession piv, PivTestState state) throws IOException, ApduException, CertificateException, BadResponseException { + putCertificate(piv, state, true); } - private static void putCertificate(PivSession piv, boolean compressed) throws IOException, ApduException, CertificateException, BadResponseException { - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + private static void putCertificate(PivSession piv, PivTestState state, boolean compressed) throws IOException, ApduException, CertificateException, BadResponseException { + piv.authenticate(state.defaultManagementKey); for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384, KeyType.RSA1024, KeyType.RSA2048, KeyType.RSA3072, KeyType.RSA4096)) { diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivDeviceTests.java index 13faef74..2bd9e168 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivDeviceTests.java @@ -16,9 +16,6 @@ package com.yubico.yubikit.testing.piv; import static com.yubico.yubikit.piv.PivSession.FEATURE_AES_KEY; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_MANAGEMENT_KEY; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_PIN; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_PUK; import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.smartcard.ApduException; @@ -41,7 +38,7 @@ public class PivDeviceTests { private static final Logger logger = LoggerFactory.getLogger(PivDeviceTests.class); - public static void testManagementKey(PivSession piv) throws BadResponseException, IOException, ApduException { + public static void testManagementKey(PivSession piv, PivTestState state) throws BadResponseException, IOException, ApduException { byte[] key2 = Hex.decode("010203040102030401020304010203040102030401020304"); ManagementKeyType managementKeyType = piv.getManagementKeyType(); @@ -55,12 +52,12 @@ public static void testManagementKey(PivSession piv) throws BadResponseException } logger.debug("Change management key"); - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + piv.authenticate(state.defaultManagementKey); piv.setManagementKey(managementKeyType, key2, false); logger.debug("Authenticate with the old key"); try { - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + piv.authenticate(state.defaultManagementKey); Assert.fail("Authenticated with wrong key"); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); @@ -68,22 +65,22 @@ public static void testManagementKey(PivSession piv) throws BadResponseException logger.debug("Change management key"); piv.authenticate(key2); - piv.setManagementKey(managementKeyType, DEFAULT_MANAGEMENT_KEY, false); + piv.setManagementKey(managementKeyType, state.defaultManagementKey, false); } - public static void testManagementKeyType(PivSession piv) throws BadResponseException, IOException, ApduException { + public static void testManagementKeyType(PivSession piv, PivTestState state) throws BadResponseException, IOException, ApduException { Assume.assumeTrue("No AES key support", piv.supports(FEATURE_AES_KEY)); ManagementKeyType managementKeyType = piv.getManagementKeyType(); byte[] aes128Key = Hex.decode("01020304010203040102030401020304"); logger.debug("Change management key type"); - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + piv.authenticate(state.defaultManagementKey); piv.setManagementKey(ManagementKeyType.AES128, aes128Key, false); Assert.assertEquals(ManagementKeyType.AES128, piv.getManagementKeyType()); try { - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + piv.authenticate(state.defaultManagementKey); Assert.fail("Authenticated with wrong key type"); } catch (IllegalArgumentException e) { // ignored @@ -91,16 +88,16 @@ public static void testManagementKeyType(PivSession piv) throws BadResponseExcep // set original management key type piv.authenticate(aes128Key); - piv.setManagementKey(managementKeyType, DEFAULT_MANAGEMENT_KEY, false); + piv.setManagementKey(managementKeyType, state.defaultManagementKey, false); } - public static void testPin(PivSession piv) throws ApduException, InvalidPinException, IOException, BadResponseException { + public static void testPin(PivSession piv, PivTestState state) throws ApduException, InvalidPinException, IOException, BadResponseException { // Ensure we only try this if the default management key is set. - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + piv.authenticate(state.defaultManagementKey); logger.debug("Verify PIN"); char[] pin2 = "11231123".toCharArray(); - piv.verifyPin(DEFAULT_PIN); + piv.verifyPin(state.defaultPin); MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(3)); logger.debug("Verify with wrong PIN"); @@ -114,7 +111,7 @@ public static void testPin(PivSession piv) throws ApduException, InvalidPinExcep logger.debug("Change PIN with wrong PIN"); try { - piv.changePin(pin2, DEFAULT_PIN); + piv.changePin(pin2, state.defaultPin); Assert.fail("Change PIN with wrong PIN"); } catch (InvalidPinException e) { MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(1)); @@ -122,12 +119,12 @@ public static void testPin(PivSession piv) throws ApduException, InvalidPinExcep } logger.debug("Change PIN"); - piv.changePin(DEFAULT_PIN, pin2); + piv.changePin(state.defaultPin, pin2); piv.verifyPin(pin2); logger.debug("Verify with wrong PIN"); try { - piv.verifyPin(DEFAULT_PIN); + piv.verifyPin(state.defaultPin); Assert.fail("Verify with wrong PIN"); } catch (InvalidPinException e) { MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); @@ -135,17 +132,17 @@ public static void testPin(PivSession piv) throws ApduException, InvalidPinExcep } logger.debug("Change PIN"); - piv.changePin(pin2, DEFAULT_PIN); + piv.changePin(pin2, state.defaultPin); } - public static void testPuk(PivSession piv) throws ApduException, InvalidPinException, IOException, BadResponseException { + public static void testPuk(PivSession piv, PivTestState state) throws ApduException, InvalidPinException, IOException, BadResponseException { // Ensure we only try this if the default management key is set. - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + piv.authenticate(state.defaultManagementKey); // Change PUK char[] puk2 = "12341234".toCharArray(); - piv.changePuk(DEFAULT_PUK, puk2); - piv.verifyPin(DEFAULT_PIN); + piv.changePuk(state.defaultPuk, puk2); + piv.verifyPin(state.defaultPin); // Block PIN while (piv.getPinAttempts() > 0) { @@ -158,7 +155,7 @@ public static void testPuk(PivSession piv) throws ApduException, InvalidPinExcep // Verify PIN blocked try { - piv.verifyPin(DEFAULT_PIN); + piv.verifyPin(state.defaultPin); } catch (InvalidPinException e) { MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(0)); MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(0)); @@ -166,24 +163,24 @@ public static void testPuk(PivSession piv) throws ApduException, InvalidPinExcep // Try unblock with wrong PUK try { - piv.unblockPin(DEFAULT_PUK, DEFAULT_PIN); + piv.unblockPin(state.defaultPuk, state.defaultPin); Assert.fail("Unblock with wrong PUK"); } catch (InvalidPinException e) { MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); } // Unblock PIN - piv.unblockPin(puk2, DEFAULT_PIN); + piv.unblockPin(puk2, state.defaultPin); // Try to change PUK with wrong PUK try { - piv.changePuk(DEFAULT_PUK, puk2); + piv.changePuk(state.defaultPuk, puk2); Assert.fail("Change PUK with wrong PUK"); } catch (InvalidPinException e) { MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); } // Change PUK - piv.changePuk(puk2, DEFAULT_PUK); + piv.changePuk(puk2, state.defaultPuk); } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDecryptTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDecryptTests.java index d10d3563..367db54a 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDecryptTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDecryptTests.java @@ -19,8 +19,6 @@ import static com.yubico.yubikit.piv.PivSession.FEATURE_RSA3072_RSA4096; import static com.yubico.yubikit.testing.piv.PivJcaUtils.setupJca; import static com.yubico.yubikit.testing.piv.PivJcaUtils.tearDownJca; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_MANAGEMENT_KEY; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_PIN; import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.smartcard.ApduException; @@ -54,33 +52,33 @@ public class PivJcaDecryptTests { private static final Logger logger = LoggerFactory.getLogger(PivJcaDecryptTests.class); - public static void testDecrypt(PivSession piv) throws BadResponseException, IOException, ApduException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException { + public static void testDecrypt(PivSession piv, PivTestState state) throws BadResponseException, IOException, ApduException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException { setupJca(piv); for (KeyType keyType : KeyType.values()) { if (((keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096) && !piv.supports(FEATURE_RSA3072_RSA4096))) { continue; // Run only on compatible keys } if (keyType.params.algorithm.name().equals("RSA")) { - testDecrypt(piv, keyType); + testDecrypt(piv, state, keyType); } } tearDownJca(); } - public static void testDecrypt(PivSession piv, KeyType keyType) throws BadResponseException, IOException, ApduException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException { + public static void testDecrypt(PivSession piv, PivTestState state, KeyType keyType) throws BadResponseException, IOException, ApduException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException { if (keyType.params.algorithm != KeyType.Algorithm.RSA) { throw new IllegalArgumentException("Unsupported"); } - if (PivTestState.isInvalidKeyType(keyType)) { + if (state.isInvalidKeyType(keyType)) { return; } - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + piv.authenticate(state.defaultManagementKey); logger.debug("Generate key: {}", keyType); KeyPairGenerator kpg = KeyPairGenerator.getInstance("YKPivRSA"); - kpg.initialize(new PivAlgorithmParameterSpec(Slot.KEY_MANAGEMENT, keyType, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, DEFAULT_PIN)); + kpg.initialize(new PivAlgorithmParameterSpec(Slot.KEY_MANAGEMENT, keyType, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, state.defaultPin)); KeyPair pair = kpg.generateKeyPair(); testDecrypt(pair, Cipher.getInstance("RSA/ECB/PKCS1Padding")); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDeviceTests.java index 75160052..405ee18a 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDeviceTests.java @@ -19,8 +19,6 @@ import static com.yubico.yubikit.piv.PivSession.FEATURE_RSA3072_RSA4096; import static com.yubico.yubikit.testing.piv.PivJcaUtils.setupJca; import static com.yubico.yubikit.testing.piv.PivJcaUtils.tearDownJca; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_MANAGEMENT_KEY; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_PIN; import com.yubico.yubikit.piv.KeyType; import com.yubico.yubikit.piv.PinPolicy; @@ -48,16 +46,16 @@ public class PivJcaDeviceTests { @SuppressWarnings("NewApi") // casting to Destroyable is supported from API 26 - public static void testImportKeys(PivSession piv) throws Exception { + public static void testImportKeys(PivSession piv, PivTestState state) throws Exception { setupJca(piv); - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + piv.authenticate(state.defaultManagementKey); KeyStore keyStore = KeyStore.getInstance("YKPiv"); keyStore.load(null); for (KeyType keyType : Arrays.asList(KeyType.RSA1024, KeyType.RSA2048, KeyType.RSA3072, KeyType.RSA4096)) { - if (PivTestState.isInvalidKeyType(keyType)) { + if (state.isInvalidKeyType(keyType)) { continue; } @@ -70,7 +68,7 @@ public static void testImportKeys(PivSession piv) throws Exception { KeyPair keyPair = PivTestUtils.loadKey(keyType); X509Certificate cert = PivTestUtils.createCertificate(keyPair); keyStore.setEntry(alias, new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[]{cert}), new PivKeyStoreKeyParameters(PinPolicy.DEFAULT, TouchPolicy.DEFAULT)); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, DEFAULT_PIN); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.defaultPin); PivTestUtils.rsaEncryptAndDecrypt(privateKey, keyPair.getPublic()); PivTestUtils.rsaSignAndVerify(privateKey, keyPair.getPublic()); @@ -86,7 +84,7 @@ public static void testImportKeys(PivSession piv) throws Exception { X509Certificate cert = PivTestUtils.createCertificate(keyPair); keyStore.setEntry(alias, new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[]{cert}), new PivKeyStoreKeyParameters(PinPolicy.DEFAULT, TouchPolicy.DEFAULT)); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, DEFAULT_PIN); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.defaultPin); PivTestUtils.ecKeyAgreement(privateKey, keyPair.getPublic()); PivTestUtils.ecSignAndVerify(privateKey, keyPair.getPublic()); @@ -100,7 +98,7 @@ public static void testImportKeys(PivSession piv) throws Exception { if (piv.supports(FEATURE_CV25519)) { for (KeyType keyType : Arrays.asList(KeyType.ED25519, KeyType.X25519)) { - if (PivTestState.isInvalidKeyType(keyType)) { + if (state.isInvalidKeyType(keyType)) { continue; } @@ -110,7 +108,7 @@ public static void testImportKeys(PivSession piv) throws Exception { X509Certificate cert = PivTestUtils.createCertificate(keyPair); keyStore.setEntry(alias, new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[]{cert}), new PivKeyStoreKeyParameters(PinPolicy.DEFAULT, TouchPolicy.DEFAULT)); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, DEFAULT_PIN); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.defaultPin); if (keyType == KeyType.X25519) { PivTestUtils.x25519KeyAgreement(privateKey, keyPair.getPublic()); @@ -130,13 +128,13 @@ public static void testImportKeys(PivSession piv) throws Exception { tearDownJca(); } - public static void testGenerateKeys(PivSession piv) throws Exception { + public static void testGenerateKeys(PivSession piv, PivTestState state) throws Exception { setupJca(piv); - generateKeys(piv); + generateKeys(piv, state); tearDownJca(); } - public static void testGenerateKeysPreferBC(PivSession piv) throws Exception { + public static void testGenerateKeysPreferBC(PivSession piv, PivTestState state) throws Exception { // following is an alternate version of setupJca method // the Bouncy Castle provider is set on second position and will provide Ed25519 and X25519 // cryptographic services on the host. @@ -144,17 +142,17 @@ public static void testGenerateKeysPreferBC(PivSession piv) throws Exception { Security.insertProviderAt(new BouncyCastleProvider(), 1); Security.insertProviderAt(new PivProvider(piv), 1); - generateKeys(piv); + generateKeys(piv, state); tearDownJca(); } - private static void generateKeys(PivSession piv) throws Exception { - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + private static void generateKeys(PivSession piv, PivTestState state) throws Exception { + piv.authenticate(state.defaultManagementKey); KeyPairGenerator ecGen = KeyPairGenerator.getInstance("YKPivEC"); for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384, KeyType.ED25519, KeyType.X25519)) { - if (PivTestState.isInvalidKeyType(keyType)) { + if (state.isInvalidKeyType(keyType)) { continue; } @@ -162,7 +160,7 @@ private static void generateKeys(PivSession piv) throws Exception { continue; } - ecGen.initialize(new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, DEFAULT_PIN)); + ecGen.initialize(new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, state.defaultPin)); KeyPair keyPair = ecGen.generateKeyPair(); if (keyType == KeyType.ED25519) { @@ -185,7 +183,7 @@ private static void generateKeys(PivSession piv) throws Exception { KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("YKPivRSA"); for (KeyType keyType : Arrays.asList(KeyType.RSA1024, KeyType.RSA2048, KeyType.RSA3072, KeyType.RSA4096)) { - if (PivTestState.isInvalidKeyType(keyType)) { + if (state.isInvalidKeyType(keyType)) { continue; } @@ -193,7 +191,7 @@ private static void generateKeys(PivSession piv) throws Exception { continue; } - rsaGen.initialize(new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, DEFAULT_PIN)); + rsaGen.initialize(new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, state.defaultPin)); KeyPair keyPair = rsaGen.generateKeyPair(); PivTestUtils.rsaEncryptAndDecrypt(keyPair.getPrivate(), keyPair.getPublic()); PivTestUtils.rsaSignAndVerify(keyPair.getPrivate(), keyPair.getPublic()); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaSigningTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaSigningTests.java index d09c4270..907736b3 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaSigningTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaSigningTests.java @@ -20,8 +20,6 @@ import static com.yubico.yubikit.piv.PivSession.FEATURE_CV25519; import static com.yubico.yubikit.testing.piv.PivJcaUtils.setupJca; import static com.yubico.yubikit.testing.piv.PivJcaUtils.tearDownJca; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_MANAGEMENT_KEY; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_PIN; import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.smartcard.ApduException; @@ -60,23 +58,23 @@ public class PivJcaSigningTests { private static Set signatureAlgorithmsWithPss = new HashSet<>(); - public static void testSign(PivSession piv) throws NoSuchAlgorithmException, IOException, ApduException, InvalidKeyException, BadResponseException, InvalidAlgorithmParameterException, SignatureException { + public static void testSign(PivSession piv, PivTestState state) throws NoSuchAlgorithmException, IOException, ApduException, InvalidKeyException, BadResponseException, InvalidAlgorithmParameterException, SignatureException { setupJca(piv); for (KeyType keyType : KeyType.values()) { if (((keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096) && !piv.supports(FEATURE_RSA3072_RSA4096))) { continue; // Run only on compatible keys } - testSign(piv, keyType); + testSign(piv, state, keyType); } tearDownJca(); } - public static void testSign(PivSession piv, KeyType keyType) throws NoSuchAlgorithmException, IOException, ApduException, InvalidKeyException, BadResponseException, InvalidAlgorithmParameterException, SignatureException { + public static void testSign(PivSession piv, PivTestState state, KeyType keyType) throws NoSuchAlgorithmException, IOException, ApduException, InvalidKeyException, BadResponseException, InvalidAlgorithmParameterException, SignatureException { if (!piv.supports(FEATURE_CV25519) && (keyType == KeyType.ED25519 || keyType == KeyType.X25519)) { return; } - if (PivTestState.isInvalidKeyType(keyType)) { + if (state.isInvalidKeyType(keyType)) { return; } @@ -84,11 +82,11 @@ public static void testSign(PivSession piv, KeyType keyType) throws NoSuchAlgori logger.debug("Ignoring keyType: {}", keyType); return; } - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + piv.authenticate(state.defaultManagementKey); logger.debug("Generate key: {}", keyType); KeyPairGenerator kpg = KeyPairGenerator.getInstance("YKPiv" + keyType.params.algorithm.name()); - kpg.initialize(new PivAlgorithmParameterSpec(Slot.SIGNATURE, keyType, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, DEFAULT_PIN)); + kpg.initialize(new PivAlgorithmParameterSpec(Slot.SIGNATURE, keyType, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, state.defaultPin)); KeyPair keyPair = kpg.generateKeyPair(); signatureAlgorithmsWithPss = getAllSignatureAlgorithmsWithPSS(); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivMoveKeyTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivMoveKeyTests.java index 763a2e2b..27fa79d6 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivMoveKeyTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivMoveKeyTests.java @@ -23,8 +23,6 @@ import static com.yubico.yubikit.testing.piv.PivJcaSigningTests.testSign; import static com.yubico.yubikit.testing.piv.PivJcaUtils.setupJca; import static com.yubico.yubikit.testing.piv.PivJcaUtils.tearDownJca; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_MANAGEMENT_KEY; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_PIN; import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.keys.PrivateKeyValues; @@ -54,18 +52,18 @@ public class PivMoveKeyTests { - static void moveKey(PivSession piv) + static void moveKey(PivSession piv, PivTestState state) throws IOException, ApduException, BadResponseException, NoSuchAlgorithmException { Assume.assumeTrue("Key does not support move instruction", piv.supports(FEATURE_MOVE_KEY)); setupJca(piv); Slot srcSlot = Slot.RETIRED1; Slot dstSlot = Slot.RETIRED2; - piv.authenticate(DEFAULT_MANAGEMENT_KEY); + piv.authenticate(state.defaultManagementKey); for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384, KeyType.RSA1024, KeyType.RSA2048, KeyType.ED25519, KeyType.X25519)) { - if (PivTestState.isInvalidKeyType(keyType)) { + if (state.isInvalidKeyType(keyType)) { continue; } @@ -89,7 +87,7 @@ static void moveKey(PivSession piv) keyStore.load(null); PublicKey publicKey = keyPair.getPublic(); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(dstSlot.getStringAlias(), DEFAULT_PIN); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(dstSlot.getStringAlias(), state.defaultPin); KeyPair signingKeyPair = new KeyPair(publicKey, privateKey); if (keyType != KeyType.X25519) { diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java new file mode 100755 index 00000000..f29d5dd8 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yubico.yubikit.testing.piv; + +import static com.yubico.yubikit.core.smartcard.SW.CONDITIONS_NOT_SATISFIED; +import static org.junit.Assume.assumeTrue; + +import com.yubico.yubikit.core.smartcard.ApduException; +import com.yubico.yubikit.management.DeviceInfo; +import com.yubico.yubikit.piv.PivSession; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; + +public class PivPinComplexityDeviceTests { + + /** + * For this test, one needs a key with PIN complexity set on. The test will change PINs. + *

+ * The test will verify that trying to set a weak PIN for PIV produces expected exceptions. + * + * @see DeviceInfo#getPinComplexity() + */ + static void testPinComplexity(PivSession piv, PivTestState state) throws Throwable { + + final DeviceInfo deviceInfo = state.getDeviceInfo(); + assumeTrue("Device does not support PIN complexity", deviceInfo != null); + assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); + + piv.reset(); + + // the values in state are not valid after reset and we need to use + // the default values + piv.authenticate(PivTestState.DEFAULT_MANAGEMENT_KEY); + piv.verifyPin(PivTestState.DEFAULT_PIN); + + MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(3)); + + // try to change to pin which breaks PIN complexity + char[] weakPin = "33333333".toCharArray(); + try { + piv.changePin(PivTestState.DEFAULT_PIN, weakPin); + Assert.fail("Set weak PIN"); + } catch (ApduException apduException) { + if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { + Assert.fail("Unexpected exception:" + apduException.getMessage()); + } + } catch (Exception e) { + Assert.fail("Unexpected exception:" + e.getMessage()); + } + + piv.verifyPin(PivTestState.DEFAULT_PIN); + + // change to complex pin + char[] complexPin = "CMPLXPIN".toCharArray(); + try { + piv.changePin(PivTestState.DEFAULT_PIN, complexPin); + } catch (Exception e) { + Assert.fail("Unexpected exception:" + e.getMessage()); + } + + piv.verifyPin(complexPin); + + // the value of default PIN in the state is correct for pin complexity settings + piv.changePin(complexPin, state.defaultPin); + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestConstants.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestConstants.java deleted file mode 100644 index 4919e5f1..00000000 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestConstants.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2022 Yubico. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.yubico.yubikit.testing.piv; - -import org.bouncycastle.util.encoders.Hex; - -public class PivTestConstants { - static final byte[] DEFAULT_MANAGEMENT_KEY = Hex.decode("010203040506070801020304050607080102030405060708"); - static final char[] DEFAULT_PIN = "123456".toCharArray(); - static final char[] DEFAULT_PUK = "12345678".toCharArray(); -} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestState.java index 7517670b..cfb77863 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestState.java @@ -16,15 +16,119 @@ package com.yubico.yubikit.testing.piv; +import static com.yubico.yubikit.testing.MpeUtils.isMpe; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.smartcard.SmartCardConnection; +import com.yubico.yubikit.management.Capability; +import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.piv.KeyType; +import com.yubico.yubikit.piv.ManagementKeyType; +import com.yubico.yubikit.piv.PivSession; +import com.yubico.yubikit.testing.TestState; + +public class PivTestState extends TestState { + + static final char[] DEFAULT_PIN = "123456".toCharArray(); + static final char[] DEFAULT_PUK = "12345678".toCharArray(); + static final byte[] DEFAULT_MANAGEMENT_KEY = new byte[]{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + }; + + private static final char[] COMPLEX_PIN = "11234567".toCharArray(); + private static final char[] COMPLEX_PUK = "11234567".toCharArray(); + private static final byte[] COMPLEX_MANAGEMENT_KEY = new byte[]{ + 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + }; + + public final boolean isFipsApproved; + public char[] defaultPin; + public char[] defaultPuk; + public byte[] defaultManagementKey; + + public static class Builder extends TestState.Builder { + + public Builder(YubiKeyDevice device) { + super(device); + } + + public PivTestState build() throws Throwable { + return new PivTestState(this); + } + } + + protected PivTestState(Builder builder) throws Throwable { + super(builder); + + defaultPin = DEFAULT_PIN; + defaultPuk = DEFAULT_PUK; + defaultManagementKey = DEFAULT_MANAGEMENT_KEY; + + assumeTrue("No SmartCard support", currentDevice.supportsConnection(SmartCardConnection.class)); -class PivTestState { - static char[] DEFAULT_PIN = PivTestConstants.DEFAULT_PIN; - static char[] DEFAULT_PUK = PivTestConstants.DEFAULT_PUK; - static byte[] DEFAULT_MANAGEMENT_KEY = PivTestConstants.DEFAULT_MANAGEMENT_KEY; - static boolean FIPS_APPROVED = false; + DeviceInfo deviceInfo = getDeviceInfo(); + + // skip MPE devices + assumeFalse("Ignoring MPE device", isMpe(deviceInfo)); + + boolean isPivFipsCapable = isFipsCapable(deviceInfo, Capability.PIV); + boolean hasPinComplexity = deviceInfo != null && deviceInfo.getPinComplexity(); + + if (scpParameters.getKid() == null && isPivFipsCapable) { + assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", isUsbTransport()); + } + + if (scpParameters.getKid() != null) { + // skip the test if the connected key does not provide matching SCP keys + assumeTrue( + "No matching key params found for required kid", + scpParameters.getKeyParams() != null + ); + } + + try (SmartCardConnection connection = openSmartCardConnection()) { + PivSession pivSession = getPivSession(connection, scpParameters); + assumeTrue("PIV not available", pivSession != null); + + try { + pivSession.reset(); + } catch (Exception ignored) { + } + + if (hasPinComplexity) { + // only use complex pins if pin complexity is required + pivSession.changePin(defaultPin, COMPLEX_PIN); + pivSession.changePuk(defaultPuk, COMPLEX_PUK); + pivSession.authenticate(defaultManagementKey); + + pivSession.setManagementKey(ManagementKeyType.AES192, COMPLEX_MANAGEMENT_KEY, false); + + defaultPin = COMPLEX_PIN; + defaultPuk = COMPLEX_PUK; + defaultManagementKey = COMPLEX_MANAGEMENT_KEY; + } + } + + deviceInfo = getDeviceInfo(); + isFipsApproved = isFipsApproved(deviceInfo, Capability.PIV); + + // after changing PIN, PUK and management key, we expect a FIPS capable device + // to be FIPS approved + if (isPivFipsCapable) { + assertNotNull(deviceInfo); + assertTrue("Device not PIV FIPS approved as expected", isFipsApproved); + } + } - static boolean isInvalidKeyType(KeyType keyType) { - return FIPS_APPROVED && (keyType == KeyType.RSA1024 || keyType == KeyType.X25519); + boolean isInvalidKeyType(KeyType keyType) { + return isFipsApproved && (keyType == KeyType.RSA1024 || keyType == KeyType.X25519); } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestUtils.java index e56627a3..ac7d2a42 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestUtils.java @@ -15,27 +15,8 @@ */ package com.yubico.yubikit.testing.piv; -import static com.yubico.yubikit.testing.StaticTestState.scpParameters; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_MANAGEMENT_KEY; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_PIN; -import static com.yubico.yubikit.testing.piv.PivTestState.DEFAULT_PUK; -import static com.yubico.yubikit.testing.piv.PivTestState.FIPS_APPROVED; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeTrue; - -import com.yubico.yubikit.core.Transport; -import com.yubico.yubikit.core.YubiKeyDevice; -import com.yubico.yubikit.core.application.ApplicationNotAvailableException; import com.yubico.yubikit.core.internal.codec.Base64; -import com.yubico.yubikit.core.smartcard.SmartCardConnection; -import com.yubico.yubikit.management.Capability; -import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.piv.KeyType; -import com.yubico.yubikit.piv.ManagementKeyType; -import com.yubico.yubikit.piv.PivSession; -import com.yubico.yubikit.testing.StaticTestState; -import com.yubico.yubikit.testing.TestUtils; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.x500.X500Name; @@ -354,7 +335,8 @@ public static X509Certificate createCertificate(KeyPair keyPair) throws IOExcept new Date(), new Date(), name, - SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(keyPair.getPublic().getEncoded())) + SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(keyPair.getPublic() + .getEncoded())) ); String algorithm; @@ -491,6 +473,7 @@ public static void ecKeyAgreement(PrivateKey privateKey, PublicKey publicKey) th Assert.assertArrayEquals("Secret mismatch", secret, peerSecret); } + public static void x25519KeyAgreement(PrivateKey privateKey, PublicKey publicKey) throws Exception { KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519"); kpg.initialize(255); @@ -510,70 +493,4 @@ public static void x25519KeyAgreement(PrivateKey privateKey, PublicKey publicKey Assert.assertArrayEquals("Secret mismatch", secret, peerSecret); } - - public static void verifyAndSetup(YubiKeyDevice device) throws Throwable { - - PivTestState.DEFAULT_PIN = PivTestConstants.DEFAULT_PIN; - PivTestState.DEFAULT_PUK = PivTestConstants.DEFAULT_PUK; - PivTestState.DEFAULT_MANAGEMENT_KEY = PivTestConstants.DEFAULT_MANAGEMENT_KEY; - - assumeTrue("No SmartCard support", device.supportsConnection(SmartCardConnection.class)); - - DeviceInfo deviceInfo = TestUtils.getDeviceInfo(device); - boolean isPivFipsCapable = deviceInfo != null && - (deviceInfo.getFipsCapable() & Capability.PIV.bit) == Capability.PIV.bit; - boolean hasPinComplexity = deviceInfo != null && - deviceInfo.getPinComplexity(); - - if (scpParameters.getKid() == null && isPivFipsCapable) { - assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", - device.getTransport() != Transport.NFC); - } - - if (scpParameters.getKid() != null) { - // skip the test if the connected key does not provide matching SCP keys - assumeTrue( - "No matching key params found for required kid", - scpParameters.getKeyParams() != null - ); - } - - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - PivSession pivSession = null; - try { - pivSession = new PivSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - - } - assumeTrue("PIV not available", pivSession != null); - try { - pivSession.reset(); - } catch (Exception e) { - - } - - if (hasPinComplexity) { - // only use complex pins if pin complexity is required - pivSession.changePin(DEFAULT_PIN, COMPLEX_PIN); - pivSession.changePuk(DEFAULT_PUK, COMPLEX_PUK); - pivSession.authenticate(DEFAULT_MANAGEMENT_KEY); - - pivSession.setManagementKey(ManagementKeyType.AES192, COMPLEX_MANAGEMENT_KEY, false); - - PivTestState.DEFAULT_PIN = COMPLEX_PIN; - PivTestState.DEFAULT_PUK = COMPLEX_PUK; - PivTestState.DEFAULT_MANAGEMENT_KEY = COMPLEX_MANAGEMENT_KEY; - } - } - - deviceInfo = TestUtils.getDeviceInfo(device); - FIPS_APPROVED = deviceInfo != null && (deviceInfo.getFipsApproved() & Capability.PIV.bit) == Capability.PIV.bit; - - // after changing PIN, PUK and management key, we expect a FIPS capable device - // to be FIPS approved - if (isPivFipsCapable) { - assertNotNull(deviceInfo); - assertTrue("Device not PIV FIPS approved as expected", FIPS_APPROVED); - } - } }