From 49b96f561422a70108dc7ce629a653ba341514af Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 2 Jul 2024 14:23:15 +0200 Subject: [PATCH 01/46] Add initial support for SCP --- .../core/smartcard/ApduFormatProcessor.java | 35 ++ .../yubikit/core/smartcard/ApduProcessor.java | 25 ++ .../yubico/yubikit/core/smartcard/AppId.java | 4 +- .../smartcard/ChainedResponseProcessor.java | 55 +++ .../core/smartcard/ExtendedApduProcessor.java | 48 +++ .../yubikit/core/smartcard/MaxApduSize.java | 27 ++ .../com/yubico/yubikit/core/smartcard/SW.java | 1 + .../yubikit/core/smartcard/ScpProcessor.java | 66 ++++ .../core/smartcard/ShortApduProcessor.java | 65 ++++ .../core/smartcard/SmartCardProtocol.java | 157 ++++---- .../smartcard/TouchWorkaroundProcessor.java | 46 +++ .../yubikit/core/smartcard/scp/KeyRef.java | 72 ++++ .../core/smartcard/scp/Scp03KeyParams.java | 55 +++ .../core/smartcard/scp/Scp11KeyParams.java | 89 +++++ .../core/smartcard/scp/ScpKeyParams.java | 29 ++ .../yubikit/core/smartcard/scp/ScpKid.java | 31 ++ .../yubikit/core/smartcard/scp/ScpState.java | 339 +++++++++++++++++ .../smartcard/scp/SecurityDomainSession.java | 358 ++++++++++++++++++ .../core/smartcard/scp/SessionKeys.java | 59 +++ .../core/smartcard/scp/StaticKeys.java | 105 +++++ .../core/smartcard/scp/package-info.java | 19 + .../yubikit/management/ManagementSession.java | 19 +- .../com/yubico/yubikit/oath/OathSession.java | 40 ++ .../yubikit/openpgp/OpenPgpSession.java | 23 ++ .../com/yubico/yubikit/piv/PivSession.java | 22 ++ 25 files changed, 1703 insertions(+), 86 deletions(-) create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/MaxApduSize.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/scp/KeyRef.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java create mode 100755 core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java new file mode 100644 index 00000000..67c10fdb --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java @@ -0,0 +1,35 @@ +/* + * 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.core.smartcard; + +import java.io.IOException; + +abstract class ApduFormatProcessor implements ApduProcessor { + protected final SmartCardConnection connection; + + ApduFormatProcessor(SmartCardConnection connection) { + this.connection = connection; + } + + abstract byte[] formatApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, int length, int le); + + @Override + public byte[] sendApdu(Apdu apdu) throws IOException { + byte[] data = apdu.getData(); + return connection.sendAndReceive(formatApdu(apdu.getCla(), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, 0, data.length, apdu.getLe())); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java new file mode 100644 index 00000000..2977d4f4 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java @@ -0,0 +1,25 @@ +/* + * 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.core.smartcard; + +import com.yubico.yubikit.core.application.BadResponseException; + +import java.io.IOException; + +public interface ApduProcessor { + byte[] sendApdu(Apdu apdu) throws IOException, BadResponseException; +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java index ce89b2fb..dccc759f 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java @@ -17,11 +17,13 @@ package com.yubico.yubikit.core.smartcard; public final class AppId { - public static final byte[] MANAGEMENT = {(byte)0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17}; + public static final byte[] MANAGEMENT = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17}; public static final byte[] OTP = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01}; public static final byte[] OATH = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01, 0x01}; public static final byte[] PIV = {(byte) 0xa0, 0x00, 0x00, 0x03, 0x08}; public static final byte[] FIDO = {(byte) 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01}; public static final byte[] OPENPGP = {(byte) 0xd2, 0x76, 0x00, 0x01, 0x24, 0x01}; public static final byte[] HSMAUTH = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x07, 0x01}; + public static final byte[] SECURITYDOMAIN = {(byte) 0xa0, 0x00, 0x00, 0x01, 0x51, 0x00, 0x00, 0x00}; + } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java new file mode 100644 index 00000000..f9465822 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java @@ -0,0 +1,55 @@ +/* + * 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.core.smartcard; + +import com.yubico.yubikit.core.application.BadResponseException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +class ChainedResponseProcessor implements ApduProcessor { + private static final byte SW1_HAS_MORE_DATA = 0x61; + + private final SmartCardConnection connection; + protected final ApduFormatProcessor processor; + private final byte[] getData; + + ChainedResponseProcessor(SmartCardConnection connection, boolean extendedApdus, int maxApduSize, byte insSendRemaining) { + this.connection = connection; + if (extendedApdus) { + processor = new ExtendedApduProcessor(connection, maxApduSize); + } else { + processor = new ShortApduProcessor(connection); + } + getData = processor.formatApdu((byte)0, insSendRemaining, (byte)0, (byte)0, new byte[0], 0, 0, 0); + } + + @Override + public byte[] sendApdu(Apdu apdu) throws IOException, BadResponseException { + ApduResponse response = new ApduResponse(processor.sendApdu(apdu)); + // Read full response + ByteArrayOutputStream readBuffer = new ByteArrayOutputStream(); + while (response.getSw() >> 8 == SW1_HAS_MORE_DATA) { + readBuffer.write(response.getData()); + response = new ApduResponse(connection.sendAndReceive(getData)); + } + readBuffer.write(response.getData()); + readBuffer.write(response.getSw() >> 8); + readBuffer.write(response.getSw() & 0xff); + return readBuffer.toByteArray(); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java new file mode 100644 index 00000000..6850be41 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java @@ -0,0 +1,48 @@ +/* + * 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.core.smartcard; + +import java.nio.ByteBuffer; + +class ExtendedApduProcessor extends ApduFormatProcessor { + private final int maxApduSize; + + ExtendedApduProcessor(SmartCardConnection connection, int maxApduSize) { + super(connection); + this.maxApduSize = maxApduSize; + } + + @Override + byte[] formatApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, int length, int le) { + ByteBuffer buf = ByteBuffer.allocate(5 + (data.length > 0 ? 2 : 0) + data.length + (le > 0 ? 2 : 0)) + .put(cla) + .put(ins) + .put(p1) + .put(p2) + .put((byte) 0x00); + if (data.length > 0) { + buf.putShort((short) data.length).put(data); + } + if (le > 0) { + buf.putShort((short) le); + } + if (buf.limit() > maxApduSize) { + throw new UnsupportedOperationException("APDU length exceeds YubiKey capability"); + } + return buf.array(); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/MaxApduSize.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/MaxApduSize.java new file mode 100644 index 00000000..7b655f42 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/MaxApduSize.java @@ -0,0 +1,27 @@ +/* + * 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.core.smartcard; + +final class MaxApduSize { + static final int NEO = 1390; + static final int YK4 = 2038; + static final int YK4_3 = 3062; + + private MaxApduSize() { + throw new IllegalStateException(); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/SW.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/SW.java index f3c2cf75..a3b5cac3 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/SW.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/SW.java @@ -34,6 +34,7 @@ public final class SW { public static final short REFERENCED_DATA_NOT_FOUND = 0x6A88; public static final short WRONG_PARAMETERS_P1P2 = 0x6B00; public static final short INVALID_INSTRUCTION = 0x6D00; + public static final short CLASS_NOT_SUPPORTED = 0x6E00; public static final short COMMAND_ABORTED = 0x6F00; public static final short OK = (short) 0x9000; diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java new file mode 100644 index 00000000..d9bb7c57 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java @@ -0,0 +1,66 @@ +/* + * 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.core.smartcard; + +import com.yubico.yubikit.core.application.BadResponseException; +import com.yubico.yubikit.core.smartcard.scp.ScpState; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +public class ScpProcessor extends ChainedResponseProcessor { + private final ScpState state; + + ScpProcessor(SmartCardConnection connection, ScpState state, int maxApduSize, byte insSendRemaining) { + super(connection, true, maxApduSize, insSendRemaining); + this.state = state; + } + + @Override + public byte[] sendApdu(Apdu apdu) throws IOException, BadResponseException { + return sendApdu(apdu, true); + } + + public byte[] sendApdu(Apdu apdu, boolean encrypt) throws IOException, BadResponseException { + byte[] data = apdu.getData(); + if (encrypt) { + data = state.encrypt(data); + } + byte cla = (byte) (apdu.getCla() | 0x04); + + // Calculate and add MAC to data + byte[] macedData = new byte[data.length + 8]; + System.arraycopy(data, 0, macedData, 0, data.length); + byte[] apduData = processor.formatApdu(cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, 0, macedData.length, apdu.getLe()); + byte[] mac = state.mac(Arrays.copyOf(apduData, apduData.length - 8)); + System.arraycopy(mac, 0, macedData, macedData.length - 8, 8); + + ApduResponse resp = new ApduResponse(super.sendApdu(new Apdu(cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, apdu.getLe()))); + byte[] respData = resp.getData(); + + // Un-MAC and decrypt, if needed + if (respData.length > 0) { + respData = state.unmac(respData, resp.getSw()); + } + if (respData.length > 0) { + respData = state.decrypt(respData); + } + + return ByteBuffer.allocate(respData.length + 2).put(respData).putShort(resp.getSw()).array(); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java new file mode 100644 index 00000000..9a62de2f --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java @@ -0,0 +1,65 @@ +/* + * 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.core.smartcard; + +import java.io.IOException; +import java.nio.ByteBuffer; + +class ShortApduProcessor extends ApduFormatProcessor { + private static final int SHORT_APDU_MAX_CHUNK = 0xff; + + ShortApduProcessor(SmartCardConnection connection) { + super(connection); + } + + @Override + byte[] formatApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, int length, int le) { + if (length > SHORT_APDU_MAX_CHUNK) { + throw new IllegalArgumentException("Length must be no greater than " + SHORT_APDU_MAX_CHUNK); + } + if (le < 0 || le > SHORT_APDU_MAX_CHUNK) { + throw new IllegalArgumentException("Le must be between 0 and " + SHORT_APDU_MAX_CHUNK); + } + + ByteBuffer buf = ByteBuffer.allocate(4 + (length > 0 ? 1 : 0) + length + (le > 0 ? 1 : 0)) + .put(cla) + .put(ins) + .put(p1) + .put(p2); + if (length > 0) { + buf.put((byte) length).put(data, offset, length); + } + if (le > 0) { + buf.put((byte) le); + } + return buf.array(); + } + + @Override + public byte[] sendApdu(Apdu apdu) throws IOException { + byte[] data = apdu.getData(); + int offset = 0; + while (data.length - offset > SHORT_APDU_MAX_CHUNK) { + byte[] response = connection.sendAndReceive(formatApdu((byte) (apdu.getCla() | 0x10), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, offset, SHORT_APDU_MAX_CHUNK, apdu.getLe())); + if (new ApduResponse(response).getSw() != SW.OK) { + return response; + } + offset += SHORT_APDU_MAX_CHUNK; + } + return connection.sendAndReceive(formatApdu(apdu.getCla(), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, offset, data.length - offset, apdu.getLe())); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java index ab71662b..4e7fc870 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java @@ -19,11 +19,15 @@ import com.yubico.yubikit.core.Transport; import com.yubico.yubikit.core.Version; import com.yubico.yubikit.core.application.ApplicationNotAvailableException; +import com.yubico.yubikit.core.application.BadResponseException; +import com.yubico.yubikit.core.smartcard.scp.Scp03KeyParams; +import com.yubico.yubikit.core.smartcard.scp.Scp11KeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpState; +import com.yubico.yubikit.core.util.Pair; -import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; -import java.nio.ByteBuffer; /** * Support class for communication over a SmartCardConnection. @@ -36,9 +40,6 @@ public class SmartCardProtocol implements Closeable { private static final byte P2_SELECT = (byte) 0x00; private static final byte INS_SEND_REMAINING = (byte) 0xc0; - private static final byte SW1_HAS_MORE_DATA = 0x61; - - private static final int SHORT_APDU_MAX_CHUNK = 0xff; private final byte insSendRemaining; @@ -46,8 +47,9 @@ public class SmartCardProtocol implements Closeable { private ApduFormat apduFormat = ApduFormat.SHORT; - private boolean useTouchWorkaround = false; - private long lastLongResponse = 0; + private int maxApduSize = MaxApduSize.NEO; + + private ApduProcessor processor; /** * Create new instance of {@link SmartCardProtocol} @@ -62,6 +64,11 @@ public SmartCardProtocol(SmartCardConnection connection) { public SmartCardProtocol(SmartCardConnection connection, byte insSendRemaining) { this.connection = connection; this.insSendRemaining = insSendRemaining; + processor = resetProcessor(); + } + + private ApduProcessor resetProcessor() { + return new ChainedResponseProcessor(connection, apduFormat == ApduFormat.EXTENDED, maxApduSize, insSendRemaining); } @Override @@ -78,7 +85,12 @@ public void enableWorkarounds(Version firmwareVersion) { if (connection.getTransport() == Transport.USB && firmwareVersion.isAtLeast(4, 2, 0) && firmwareVersion.isLessThan(4, 2, 7)) { + //noinspection deprecation setEnableTouchWorkaround(true); + } else if (firmwareVersion.isAtLeast(4, 0, 0) && !(processor instanceof ScpProcessor)) { + apduFormat = ApduFormat.EXTENDED; + maxApduSize = firmwareVersion.isAtLeast(4, 3, 0) ? MaxApduSize.YK4_3 : MaxApduSize.YK4; + processor = resetProcessor(); } } @@ -87,9 +99,17 @@ public void enableWorkarounds(Version firmwareVersion) { * on such devices to trigger sending a dummy command which mitigates the issue. * * @param enableTouchWorkaround true to enable the workaround, false to disable it + * @deprecated use {@link #enableWorkarounds} instead. */ + @Deprecated public void setEnableTouchWorkaround(boolean enableTouchWorkaround) { - this.useTouchWorkaround = enableTouchWorkaround; + if (enableTouchWorkaround) { + apduFormat = ApduFormat.EXTENDED; + maxApduSize = MaxApduSize.YK4; + processor = new TouchWorkaroundProcessor(connection, insSendRemaining); + } else { + processor = resetProcessor(); + } } /** @@ -98,7 +118,14 @@ public void setEnableTouchWorkaround(boolean enableTouchWorkaround) { * @param apduFormat the APDU encoding to use when sending commands */ public void setApduFormat(ApduFormat apduFormat) { + if (this.apduFormat == apduFormat) { + return; + } + if (apduFormat != ApduFormat.EXTENDED) { + throw new UnsupportedOperationException("Cannot change from EXTENDED to SHORT APDU format"); + } this.apduFormat = apduFormat; + processor = resetProcessor(); } /** @@ -117,6 +144,7 @@ public SmartCardConnection getConnection() { * @throws ApplicationNotAvailableException in case the AID doesn't match an available application */ public byte[] select(byte[] aid) throws IOException, ApplicationNotAvailableException { + processor = resetProcessor(); try { return sendAndReceive(new Apdu(0, INS_SELECT, P1_SELECT, P2_SELECT, aid)); } catch (ApduException e) { @@ -138,90 +166,51 @@ public byte[] select(byte[] aid) throws IOException, ApplicationNotAvailableExce * @throws ApduException in case if received error in APDU response */ public byte[] sendAndReceive(Apdu command) throws IOException, ApduException { - if (useTouchWorkaround && lastLongResponse > 0 && System.currentTimeMillis() - lastLongResponse < 2000) { - connection.sendAndReceive(new byte[5]); // Dummy APDU; returns an error - lastLongResponse = 0; - } - ApduResponse response; - byte[] getData; - byte[] data = command.getData(); - switch (apduFormat) { - case SHORT: - int offset = 0; - while (data.length - offset > SHORT_APDU_MAX_CHUNK) { - response = new ApduResponse(connection.sendAndReceive(encodeShortApdu((byte) (command.getCla() | 0x10), command.getIns(), command.getP1(), command.getP2(), data, offset, SHORT_APDU_MAX_CHUNK, command.getLe()))); - if (response.getSw() != SW.OK) { - throw new ApduException(response.getSw()); - } - offset += SHORT_APDU_MAX_CHUNK; - } - response = new ApduResponse(connection.sendAndReceive(encodeShortApdu(command.getCla(), command.getIns(), command.getP1(), command.getP2(), data, offset, data.length - offset, command.getLe()))); - getData = new byte[]{0x00, insSendRemaining, 0x00, 0x00, 0x00}; - break; - case EXTENDED: - response = new ApduResponse(connection.sendAndReceive(encodeExtendedApdu(command.getCla(), command.getIns(), command.getP1(), command.getP2(), data, command.getLe()))); - getData = new byte[]{0x00, insSendRemaining, 0x00, 0x00, 0x00, 0x00, 0x00}; - break; - default: - throw new IllegalStateException("Invalid APDU format"); - } - - // Read full response - ByteArrayOutputStream readBuffer = new ByteArrayOutputStream(); - while (response.getSw() >> 8 == SW1_HAS_MORE_DATA) { - readBuffer.write(response.getData()); - response = new ApduResponse(connection.sendAndReceive(getData)); - } - - if (response.getSw() != SW.OK) { - throw new ApduException(response.getSw()); + try { + ApduResponse response = new ApduResponse(processor.sendApdu(command)); + if (response.getSw() != SW.OK) { + throw new ApduException(response.getSw()); + } + return response.getData(); + } catch (BadResponseException e) { + throw new IOException(e); } - readBuffer.write(response.getData()); - byte[] responseData = readBuffer.toByteArray(); + } - if (useTouchWorkaround && responseData.length > 54) { - lastLongResponse = System.currentTimeMillis(); - } else { - lastLongResponse = 0; + public void initScp(ScpKeyParams keyParams) throws IOException, ApduException, BadResponseException { + try { + if (keyParams instanceof Scp03KeyParams) { + initScp03((Scp03KeyParams) keyParams); + } else if (keyParams instanceof Scp11KeyParams) { + initScp11((Scp11KeyParams) keyParams); + } else { + throw new IllegalArgumentException("Unsupported ScpKeyParams"); + } + apduFormat = ApduFormat.EXTENDED; + maxApduSize = MaxApduSize.YK4_3; + } catch (ApduException e) { + if (e.getSw() == SW.CLASS_NOT_SUPPORTED) { + throw new UnsupportedOperationException("This YubiKey does not support secure messaging"); + } + throw e; } - return responseData; } - private static byte[] encodeShortApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, int length, int le) { - if (length > SHORT_APDU_MAX_CHUNK) { - throw new IllegalArgumentException("Length must be no greater than " + SHORT_APDU_MAX_CHUNK); - } - if (le < 0 || le > SHORT_APDU_MAX_CHUNK) { - throw new IllegalArgumentException("Le must be between 0 and " + SHORT_APDU_MAX_CHUNK); - } + private void initScp03(Scp03KeyParams keyParams) throws IOException, ApduException, BadResponseException { + Pair pair = ScpState.scp03Init(processor, keyParams, null); + ScpProcessor processor = new ScpProcessor(connection, pair.first, MaxApduSize.YK4_3, insSendRemaining); - ByteBuffer buf = ByteBuffer.allocate(4 + (length > 0 ? 1 : 0) + length + (le > 0 ? 1 : 0)) - .put(cla) - .put(ins) - .put(p1) - .put(p2); - if (length > 0) { - buf.put((byte) length).put(data, offset, length); + // Send EXTERNAL AUTHENTICATE + // P1 = C-DECRYPTION, R-ENCRYPTION, C-MAC, and R-MAC + ApduResponse resp = new ApduResponse(processor.sendApdu(new Apdu(0x84, 0x82, 0x33, 0, pair.second), false)); + if (resp.getSw() != SW.OK) { + throw new ApduException(resp.getSw()); } - if (le > 0) { - buf.put((byte) le); - } - return buf.array(); + this.processor = processor; } - private static byte[] encodeExtendedApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int le) { - ByteBuffer buf = ByteBuffer.allocate(5 + (data.length > 0 ? 2 : 0) + data.length + (le > 0 ? 2 : 0)) - .put(cla) - .put(ins) - .put(p1) - .put(p2) - .put((byte) 0x00); - if (data.length > 0) { - buf.putShort((short) data.length).put(data); - } - if (le > 0) { - buf.putShort((short) le); - } - return buf.array(); + private void initScp11(Scp11KeyParams keyParams) throws IOException, ApduException, BadResponseException { + ScpState scp = ScpState.scp11Init(processor, keyParams); + processor = new ScpProcessor(connection, scp, MaxApduSize.YK4_3, insSendRemaining); } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java new file mode 100644 index 00000000..ea0ed41d --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java @@ -0,0 +1,46 @@ +/* + * 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.core.smartcard; + +import com.yubico.yubikit.core.application.BadResponseException; + +import java.io.IOException; + +class TouchWorkaroundProcessor extends ChainedResponseProcessor { + private long lastLongResponse = 0; + + TouchWorkaroundProcessor(SmartCardConnection connection, byte insSendRemaining) { + super(connection, true, MaxApduSize.YK4, insSendRemaining); + } + + @Override + public byte[] sendApdu(Apdu apdu) throws IOException, BadResponseException { + if (lastLongResponse > 0 && System.currentTimeMillis() - lastLongResponse < 2000) { + super.sendApdu(new Apdu(0, 0, 0, 0, null)); // Dummy APDU; returns an error + lastLongResponse = 0; + } + byte[] response = super.sendApdu(apdu); + + if (response.length > 54) { + lastLongResponse = System.currentTimeMillis(); + } else { + lastLongResponse = 0; + } + + return response; + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/KeyRef.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/KeyRef.java new file mode 100644 index 00000000..e8fd75c5 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/KeyRef.java @@ -0,0 +1,72 @@ +/* + * 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.core.smartcard.scp; + +import java.util.Locale; +import java.util.Objects; + +/** + * Reference to an SCP key. Each key is uniquely identified by the combination of KID and KVN. Related keys typically share a KVN. + */ +public class KeyRef { + private final byte kid; + private final byte kvn; + + public KeyRef(byte kid, byte kvn) { + this.kid = kid; + this.kvn = kvn; + } + + /** + * @return the KID of the SCP key + */ + public byte getKid() { + return kid; + } + + /** + * @return the KVN of the SCP key. + */ + public byte getKvn() { + return kvn; + } + + /** + * @return the byte[] representation of the KID-KVN pair. + */ + public byte[] getBytes() { + return new byte[]{kid, kvn}; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + KeyRef keyRef = (KeyRef) o; + return kid == keyRef.kid && kvn == keyRef.kvn; + } + + @Override + public int hashCode() { + return Objects.hash(kid, kvn); + } + + @Override + public String toString() { + return String.format(Locale.ROOT, "KeyRef{kid=0x%02x, kvn=0x%02x}", 0xff & kid, 0xff & kvn); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java new file mode 100644 index 00000000..5c1271e8 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java @@ -0,0 +1,55 @@ +/* + * 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.core.smartcard.scp; + +import javax.security.auth.DestroyFailedException; + +/** + * SCP key parameters for performing an SCP03 authentication. + * SCP03 uses a set of three keys, each with their own KID, but a shared KVN. + */ +public class Scp03KeyParams implements ScpKeyParams { + private final KeyRef keyRef; + final StaticKeys keys; + + /** + * @param keyRef the reference to the key set to authenticat with. + * @param keys the key material for authentication. + */ + public Scp03KeyParams(KeyRef keyRef, StaticKeys keys) { + if ((0xff & keyRef.getKid()) > 3) { + throw new IllegalArgumentException("Invalid KID for SCP03"); + } + this.keyRef = keyRef; + this.keys = keys; + } + + @Override + public KeyRef getKeyRef() { + return keyRef; + } + + @Override + public void destroy() throws DestroyFailedException { + keys.destroy(); + } + + @Override + public boolean isDestroyed() { + return keys.isDestroyed(); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java new file mode 100644 index 00000000..ffe7ef88 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java @@ -0,0 +1,89 @@ +/* + * 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.core.smartcard.scp; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nullable; +import javax.security.auth.DestroyFailedException; + +/** + * SCP key parameters for performing SCP11 authentication. + * For SCP11b only keyRef and pkSdEcka are required. Note that this does not authenticate the off-card entity. + * For SCP11a and SCP11c the off-card entity CA key reference must be provided, as well as the off-card entity secret key and certificate chain. + */ +public class Scp11KeyParams implements ScpKeyParams { + private final KeyRef keyRef; + final PublicKey pkSdEcka; + @Nullable + final KeyRef oceKeyRef; + @Nullable + final PrivateKey skOceEcka; + final List certificates; + + public Scp11KeyParams(KeyRef keyRef, PublicKey pkSdEcka, @Nullable KeyRef oceKeyRef, @Nullable PrivateKey skOceEcka, List certificates) { + this.keyRef = keyRef; + this.pkSdEcka = pkSdEcka; + this.oceKeyRef = oceKeyRef; + this.skOceEcka = skOceEcka; + this.certificates = Collections.unmodifiableList(new ArrayList<>(certificates)); + switch (keyRef.getKid()) { + case ScpKid.SCP11b: + if (oceKeyRef != null || skOceEcka != null || !certificates.isEmpty()) { + throw new IllegalArgumentException("Cannot provide oceKeyRef, skOceEcka or certificates for SCP11b"); + } + break; + case ScpKid.SCP11a: + case ScpKid.SCP11c: + if (oceKeyRef == null || skOceEcka == null || certificates.isEmpty()) { + throw new IllegalArgumentException("Must provide oceKeyRef, skOceEcka or certificates for SCP11a/c"); + } + break; + default: + throw new IllegalArgumentException("KID must be 0x11, 0x13, or 0x15 for SCP11"); + } + } + + public Scp11KeyParams(KeyRef keyRef, PublicKey pkSdEcka) { + this(keyRef, pkSdEcka, null, null, Collections.emptyList()); + } + + @Override + public KeyRef getKeyRef() { + return keyRef; + } + + @Override + public void destroy() throws DestroyFailedException { + if (skOceEcka != null) { + skOceEcka.destroy(); + } + } + + @Override + public boolean isDestroyed() { + if (skOceEcka != null) { + return skOceEcka.isDestroyed(); + } + return false; + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java new file mode 100644 index 00000000..d29f3186 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java @@ -0,0 +1,29 @@ +/* + * 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.core.smartcard.scp; + +import javax.security.auth.Destroyable; + +/** + * SCP key parameters for performing an SCP authentication with a YubiKey. + */ +public interface ScpKeyParams extends Destroyable { + /** + * @return the identifier of the SCP key to target on the YubiKey. + */ + KeyRef getKeyRef(); +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java new file mode 100644 index 00000000..1edf1cf3 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java @@ -0,0 +1,31 @@ +/* + * 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.core.smartcard.scp; + +/** + * Named + */ +public final class ScpKid { + public static final byte SCP03 = 0x1; + public static final byte SCP11a = 0x11; + public static final byte SCP11b = 0x13; + public static final byte SCP11c = 0x15; + + private ScpKid() { + throw new IllegalStateException(); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java new file mode 100644 index 00000000..283c57ef --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java @@ -0,0 +1,339 @@ +/* + * 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.core.smartcard.scp; + +import com.yubico.yubikit.core.application.BadResponseException; +import com.yubico.yubikit.core.keys.PublicKeyValues; +import com.yubico.yubikit.core.smartcard.Apdu; +import com.yubico.yubikit.core.smartcard.ApduException; +import com.yubico.yubikit.core.smartcard.ApduProcessor; +import com.yubico.yubikit.core.smartcard.ApduResponse; +import com.yubico.yubikit.core.smartcard.SW; +import com.yubico.yubikit.core.util.Pair; +import com.yubico.yubikit.core.util.RandomUtils; +import com.yubico.yubikit.core.util.Tlv; +import com.yubico.yubikit.core.util.Tlvs; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.interfaces.ECPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nullable; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyAgreement; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.security.auth.DestroyFailedException; + + +/** + * Internal SCP state class for managing SCP state, handling encryption/decryption and MAC. + */ +public class ScpState { + private final SessionKeys keys; + private byte[] macChain; + private int encCounter = 1; + + public ScpState(SessionKeys keys, byte[] macChain) { + this.keys = keys; + this.macChain = macChain; + } + + public byte[] encrypt(byte[] data) { + // Pad the data + int padLen = 16 - (data.length % 16); + byte[] msg = new byte[data.length + padLen]; + System.arraycopy(data, 0, msg, 0, data.length); + msg[data.length] = (byte) 0x80; + + // Encrypt + try { + @SuppressWarnings("GetInstance") Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, keys.senc); + byte[] ivData = ByteBuffer.allocate(16).put(new byte[12]).putInt(encCounter++).array(); + byte[] iv = cipher.doFinal(ivData); + + cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, keys.senc, new IvParameterSpec(iv)); + return cipher.doFinal(msg); + } catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException | + IllegalBlockSizeException | BadPaddingException | + InvalidAlgorithmParameterException e) { + //This should never happen + throw new RuntimeException(e); + } + } + + public byte[] decrypt(byte[] encrypted) throws BadResponseException { + // Decrypt + try { + @SuppressWarnings("GetInstance") Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, keys.senc); + byte[] ivData = ByteBuffer.allocate(16).put((byte) 0x80).put(new byte[11]).putInt(encCounter - 1).array(); + byte[] iv = cipher.doFinal(ivData); + + cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, keys.senc, new IvParameterSpec(iv)); + byte[] decrypted = cipher.doFinal(encrypted); + for (int i = decrypted.length - 1; i > 0; i--) { + if (decrypted[i] == (byte) 0x80) { + return Arrays.copyOf(decrypted, i); + } else if (decrypted[i] != 0x00) { + break; + } + } + throw new BadResponseException("Bad padding"); + } catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException | + IllegalBlockSizeException | BadPaddingException | + InvalidAlgorithmParameterException e) { + //This should never happen + throw new RuntimeException(e); + } + } + + public byte[] mac(byte[] data) { + try { + Mac mac = Mac.getInstance("AESCMAC"); + mac.init(keys.smac); + mac.update(macChain); + macChain = mac.doFinal(data); + return Arrays.copyOf(macChain, 8); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new UnsupportedOperationException("Cryptography provider does not support AESCMAC", e); + } + } + + public byte[] unmac(byte[] data, short sw) throws BadResponseException { + byte[] msg = ByteBuffer.allocate(data.length - 8 + 2).put(data, 0, data.length - 8).putShort(sw).array(); + + try { + Mac mac = Mac.getInstance("AESCMAC"); + mac.init(keys.srmac); + mac.update(macChain); + + byte[] rmac = Arrays.copyOf(mac.doFinal(msg), 8); + if (MessageDigest.isEqual(rmac, Arrays.copyOfRange(data, data.length - 8, data.length))) { + return Arrays.copyOf(msg, msg.length - 2); + } + throw new BadResponseException("Wrong MAC"); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new UnsupportedOperationException("Cryptography provider does not support AESCMAC", e); + } + } + + public static Pair scp03Init(ApduProcessor processor, Scp03KeyParams keyParams, @Nullable byte[] hostChallenge) throws BadResponseException, IOException, ApduException { + if (hostChallenge == null) { + hostChallenge = RandomUtils.getRandomBytes(8); + } + + ApduResponse resp = new ApduResponse(processor.sendApdu(new Apdu(0x80, 0x50, keyParams.getKeyRef().getKvn(), 0x00, hostChallenge))); + if (resp.getSw() != SW.OK) { + throw new ApduException(resp.getSw()); + } + + byte[] diversificationData = new byte[10]; + byte[] keyInfo = new byte[3]; + byte[] cardChallenge = new byte[8]; + byte[] cardCryptogram = new byte[8]; + ByteBuffer.wrap(resp.getData()) + .get(diversificationData) + .get(keyInfo) + .get(cardChallenge) + .get(cardCryptogram); + + byte[] context = ByteBuffer.allocate(16).put(hostChallenge).put(cardChallenge).array(); + SessionKeys sessionKeys = keyParams.keys.derive(context); + + byte[] genCardCryptogram = StaticKeys.deriveKey(sessionKeys.smac, (byte) 0x00, context, (byte) 0x40).getEncoded(); + if (!MessageDigest.isEqual(genCardCryptogram, cardCryptogram)) { + throw new BadResponseException("Wrong SCP03 key set"); + } + + byte[] hostCryptogram = StaticKeys.deriveKey(sessionKeys.smac, (byte) 0x01, context, (byte) 0x40).getEncoded(); + return new Pair<>(new ScpState(sessionKeys, new byte[16]), hostCryptogram); + } + + public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyParams) throws BadResponseException, IOException, ApduException { + // GPC v2.3 Amendment F (SCP11) v1.4 §7.1.1 + byte params; + byte kid = keyParams.getKeyRef().getKid(); + switch (kid) { + case ScpKid.SCP11a: + params = 0b01; + break; + case ScpKid.SCP11b: + params = 0b00; + break; + case ScpKid.SCP11c: + params = 0b11; + break; + default: + throw new IllegalArgumentException("Invalid SCP11 KID"); + } + + if (kid == ScpKid.SCP11a || kid == ScpKid.SCP11c) { + // GPC v2.3 Amendment F (SCP11) v1.4 §7.5 + Objects.requireNonNull(keyParams.skOceEcka); + int n = keyParams.certificates.size() - 1; + if (n < 0) { + throw new IllegalArgumentException("SCP11a and SCP11c require a certificate chain"); + } + KeyRef oceRef = keyParams.oceKeyRef != null ? keyParams.oceKeyRef : new KeyRef((byte) 0, (byte) 0); + for (int i = 0; i <= n; i++) { + try { + byte[] data = keyParams.certificates.get(i).getEncoded(); + byte p2 = (byte) (oceRef.getKid() | (i < n ? 0x80 : 0x00)); + ApduResponse resp = new ApduResponse(processor.sendApdu(new Apdu(0x80, 0x2A, oceRef.getKvn(), p2, data))); + if (resp.getSw() != SW.OK) { + throw new ApduException(resp.getSw()); + } + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException("Invalid certificate encoding", e); + } + } + } + + byte[] keyUsage = new byte[]{0x3C}; // AUTHENTICATED | C_MAC | C_DECRYPTION | R_MAC | R_ENCRYPTION + byte[] keyType = new byte[]{(byte) 0x88}; // AES + byte[] keyLen = new byte[]{16}; // 128-bit + + // Host ephemeral key + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + ECPublicKey pk = (ECPublicKey) keyParams.pkSdEcka; + kpg.initialize(pk.getParams()); + KeyPair ephemeralOceEcka = kpg.generateKeyPair(); + PublicKeyValues.Ec epkOceEcka = (PublicKeyValues.Ec) PublicKeyValues.fromPublicKey(ephemeralOceEcka.getPublic()); + + // GPC v2.3 Amendment F (SCP11) v1.4 §7.6.2.3 + byte[] data = Tlvs.encodeList( + Arrays.asList( + new Tlv( + 0xA6, + Tlvs.encodeList( + Arrays.asList( + new Tlv(0x90, new byte[]{0x11, params}), + new Tlv(0x95, keyUsage), + new Tlv(0x80, keyType), + new Tlv(0x81, keyLen) + ) + ) + ), + new Tlv( + 0x5F49, + epkOceEcka.getEncodedPoint() + ) + ) + ); + + // Static host key (SCP11a/c), or ephemeral key again (SCP11b) + PrivateKey skOceEcka = keyParams.skOceEcka != null ? keyParams.skOceEcka : ephemeralOceEcka.getPrivate(); + int ins = keyParams.getKeyRef().getKid() == ScpKid.SCP11b ? 0x88 : 0x82; + ApduResponse resp = new ApduResponse(processor.sendApdu(new Apdu(0x80, ins, keyParams.getKeyRef().getKvn(), keyParams.getKeyRef().getKid(), data))); + if (resp.getSw() != SW.OK) { + throw new ApduException(resp.getSw()); + } + List tlvs = Tlvs.decodeList(resp.getData()); + Tlv epkSdEckaTlv = tlvs.get(0); + byte[] epkSdEckaEncodedPoint = Tlvs.unpackValue(0x5F49, epkSdEckaTlv.getBytes()); + byte[] receipt = Tlvs.unpackValue(0x86, tlvs.get(1).getBytes()); + + // GPC v2.3 Amendment F (SCP11) v1.3 §3.1.2 Key Derivation + byte[] keyAgreementData = ByteBuffer.allocate(data.length + epkSdEckaTlv.getBytes().length) + .put(data) + .put(epkSdEckaTlv.getBytes()) + .array(); + byte[] sharedInfo = ByteBuffer.allocate(keyUsage.length + keyType.length + keyLen.length) + .put(keyUsage) + .put(keyType) + .put(keyLen) + .array(); + + KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH"); + + keyAgreement.init(ephemeralOceEcka.getPrivate()); + keyAgreement.doPhase(PublicKeyValues.Ec.fromEncodedPoint(epkOceEcka.getCurveParams(), epkSdEckaEncodedPoint).toPublicKey(), true); + byte[] ka1 = keyAgreement.generateSecret(); + + keyAgreement.init(skOceEcka); + keyAgreement.doPhase(pk, true); + byte[] ka2 = keyAgreement.generateSecret(); + + byte[] keyMaterial = ByteBuffer.allocate(ka1.length + ka2.length).put(ka1).put(ka2).array(); + + List keys = new ArrayList<>(); + int counter = 1; + // We need 5 16-byte keys, which requires 3 iterations of SHA256 + for (int i = 0; i < 3; i++) { + MessageDigest hash = MessageDigest.getInstance("SHA256"); + hash.update(keyMaterial); + hash.update(ByteBuffer.allocate(4).putInt(counter++).array()); + hash.update(sharedInfo); + // Each iteration gives us 2 keys + byte[] digest = hash.digest(); + keys.add(new SecretKeySpec(digest, 0, 16, "AES")); + keys.add(new SecretKeySpec(digest, 16, 16, "AES")); + Arrays.fill(digest, (byte)0); + } + + // 6 keys were derived. one for verification of receipt, 4 keys to use, and 1 which is discarded + SecretKey key = keys.get(0); + try { + Mac mac = Mac.getInstance("AESCMAC"); + mac.init(key); + byte[] genReceipt = mac.doFinal(keyAgreementData); + if (!MessageDigest.isEqual(receipt, genReceipt)) { + throw new BadResponseException("Receipt does not match"); + } + return new ScpState(new SessionKeys( + keys.get(1), + keys.get(2), + keys.get(3), + keys.get(4) + ), receipt); + } finally { + try { + key.destroy(); + keys.get(5).destroy(); + } catch (DestroyFailedException e) { + // TODO: Log error + } + } + } catch (NoSuchAlgorithmException | InvalidKeySpecException | + InvalidAlgorithmParameterException | InvalidKeyException e) { + throw new RuntimeException(e); + } + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java new file mode 100644 index 00000000..0fed0f5c --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java @@ -0,0 +1,358 @@ +/* + * 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.core.smartcard.scp; + +import com.yubico.yubikit.core.Version; +import com.yubico.yubikit.core.application.ApplicationNotAvailableException; +import com.yubico.yubikit.core.application.ApplicationSession; +import com.yubico.yubikit.core.application.BadResponseException; +import com.yubico.yubikit.core.internal.Logger; +import com.yubico.yubikit.core.keys.EllipticCurveValues; +import com.yubico.yubikit.core.keys.PrivateKeyValues; +import com.yubico.yubikit.core.keys.PublicKeyValues; +import com.yubico.yubikit.core.smartcard.Apdu; +import com.yubico.yubikit.core.smartcard.ApduException; +import com.yubico.yubikit.core.smartcard.AppId; +import com.yubico.yubikit.core.smartcard.SW; +import com.yubico.yubikit.core.smartcard.SmartCardConnection; +import com.yubico.yubikit.core.smartcard.SmartCardProtocol; +import com.yubico.yubikit.core.util.StringUtils; +import com.yubico.yubikit.core.util.Tlv; +import com.yubico.yubikit.core.util.Tlvs; + +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.annotation.Nullable; + +public class SecurityDomainSession extends ApplicationSession { + private static final byte INS_GET_DATA = (byte) 0xCA; + private static final byte INS_PUT_KEY = (byte) 0xD8; + private static final byte INS_STORE_DATA = (byte) 0xE2; + private static final byte INS_DELETE = (byte) 0xE4; + private static final byte INS_GENERATE_KEY = (byte) 0xF1; + + private static final short TAG_KEY_INFORMATION = 0xE0; + private static final short TAG_CARD_RECOGNITION_DATA = 0x66; + private static final short TAG_CA_KLOC_IDENTIFIERS = (short) 0xFF33; + private static final short TAG_CA_KLCC_IDENTIFIERS = (short) 0xFF34; + private static final short TAG_CERTIFICATE_STORE = (short) 0xBF21; + + private static final byte KEY_TYPE_AES = (byte) 0x88; + private static final byte KEY_TYPE_ECC_PUBLIC_KEY = (byte) 0xB0; + private static final byte KEY_TYPE_ECC_PRIVATE_KEY = (byte) 0xB1; + private static final byte KEY_TYPE_ECC_KEY_PARAMS = (byte) 0xF0; + + private final SmartCardProtocol protocol; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(SecurityDomainSession.class); + + public SecurityDomainSession(SmartCardConnection connection) throws IOException, ApplicationNotAvailableException { + protocol = new SmartCardProtocol(connection); + protocol.select(AppId.SECURITYDOMAIN); + // We don't know the version, but we know it's at least 5.3.0 + protocol.enableWorkarounds(new Version(5, 3, 0)); + Logger.debug(logger, "Security Domain session initialized"); + } + + @Override + public Version getVersion() { + throw new UnsupportedOperationException("Version cannot be read from Security Domain application"); + } + + @Override + public void close() throws IOException { + protocol.close(); + } + + /** + * Initialize SCP and authenticate the session. + * SCP11b does not authenticate the off-card entity, and will not allow the usage of commands which require such authentication. + */ + public void authenticate(ScpKeyParams keyParams) throws BadResponseException, ApduException, IOException { + protocol.initScp(keyParams); + } + + public byte[] getData(short tag, @Nullable byte[] data) throws ApduException, IOException { + return protocol.sendAndReceive(new Apdu(0, INS_GET_DATA, tag >> 8, tag & 0xff, data)); + } + + public byte[] getCardRecognitionData() throws ApduException, IOException, BadResponseException { + return Tlvs.unpackValue(0x73, getData(TAG_CARD_RECOGNITION_DATA, null)); + } + + public Map> getKeyInformation() throws ApduException, IOException, BadResponseException { + Map> keys = new HashMap<>(); + for (Tlv tlv : Tlvs.decodeList(getData(TAG_KEY_INFORMATION, null))) { + ByteBuffer data = ByteBuffer.wrap(Tlvs.unpackValue(0xC0, tlv.getBytes())); + KeyRef keyRef = new KeyRef(data.get(), data.get()); + Map components = new HashMap<>(); + while (data.hasRemaining()) { + components.put(data.get(), data.get()); + } + keys.put(keyRef, components); + } + return keys; + } + + public List getCertificateBundle(KeyRef keyRef) throws ApduException, IOException, CertificateException { + Logger.debug(logger, "Getting certificate bundle for key={}", keyRef); + List certificates = new ArrayList<>(); + try { + byte[] resp = getData(TAG_CERTIFICATE_STORE, new Tlv(0xA6, new Tlv(0x83, keyRef.getBytes()).getBytes()).getBytes()); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + for (Tlv der : Tlvs.decodeList(resp)) { + InputStream stream = new ByteArrayInputStream(der.getBytes()); + certificates.add((X509Certificate) cf.generateCertificate(stream)); + } + } catch (ApduException e) { + // On REFERENCED_DATA_NOT_FOUND return empty list + if (e.getSw() != SW.REFERENCED_DATA_NOT_FOUND) { + throw e; + } + } + return certificates; + } + + public Map getSupportedCaIdentifiers(boolean kloc, boolean klcc) throws ApduException, IOException { + if (!kloc && !klcc) { + throw new IllegalArgumentException("At least one of kloc and klcc must be true"); + } + Logger.debug(logger, "Getting CA identifiers KLOC={}, KLCC={}", kloc, klcc); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + if (kloc) { + try { + data.write(getData(TAG_CA_KLOC_IDENTIFIERS, null)); + } catch (ApduException e) { + if (e.getSw() != SW.REFERENCED_DATA_NOT_FOUND) { + throw e; + } + } + } + if (klcc) { + try { + data.write(getData(TAG_CA_KLCC_IDENTIFIERS, null)); + } catch (ApduException e) { + if (e.getSw() != SW.REFERENCED_DATA_NOT_FOUND) { + throw e; + } + } + } + List tlvs = Tlvs.decodeList(data.toByteArray()); + Map identifiers = new HashMap<>(); + for (int i = 0; i < tlvs.size(); i += 2) { + ByteBuffer ref = ByteBuffer.wrap(tlvs.get(i + 1).getValue()); + identifiers.put(new KeyRef(ref.get(), ref.get()), tlvs.get(i).getValue()); + } + return identifiers; + } + + public void storeData(byte[] data) throws ApduException, IOException { + protocol.sendAndReceive(new Apdu(0, INS_STORE_DATA, 0x90, 0x00, data)); + } + + /** + * Store the certificate chain for a given key. + * Requires off-card entity verification. + * Certificates should be in order, with the leaf certificate last. + * + * @param keyRef a reference to the key for which to store the certificates + * @param certificates the certificates to store + */ + public void storeCertificateBundle(KeyRef keyRef, List certificates) throws ApduException, IOException { + Logger.debug(logger, "Storing certificate bundle for {}", keyRef); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + for (X509Certificate cert : certificates) { + try { + data.write(cert.getEncoded()); + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException("Failed to get encoded version of certificate", e); + } + } + storeData(Tlvs.encodeList(Arrays.asList( + new Tlv(0xA6, new Tlv(0x83, keyRef.getBytes()).getBytes()), + new Tlv(TAG_CERTIFICATE_STORE, data.toByteArray()) + ))); + Logger.info(logger, "Certificate bundle stored"); + } + + /** + * Store which certificate serial numbers that can be used for a given key. + * Requires off-card entity verification. + * If no allowlist is stored, any certificate signed by the CA can be used. + * + * @param keyRef a reference to the key for which to store the allowlist + * @param serials the list of serial numbers to store + */ + public void storeAllowlist(KeyRef keyRef, List serials) throws ApduException, IOException { + Logger.debug(logger, "Storing serial allowlist for {}", keyRef); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + for (BigInteger serial : serials) { + data.write(serial.toByteArray()); + } + storeData(Tlvs.encodeList(Arrays.asList( + new Tlv(0xA6, new Tlv(0x83, keyRef.getBytes()).getBytes()), + new Tlv(0x70, data.toByteArray()) + ))); + Logger.info(logger, "Serial allowlist stored"); + } + + /** + * Store the SKI (Subject Key Identifier) for the CA of a given key. + * Requires off-card entity verification. + * + * @param keyRef a reference to the key for which to store the CA issuer + * @param ski the Subject Key Identifier to store + */ + public void storeCaIssuer(KeyRef keyRef, byte[] ski) throws ApduException, IOException { + Logger.debug(logger, "Storing CA issuer SKI for {}: {}", keyRef, StringUtils.bytesToHex(ski)); + byte klcc = 0; + switch (keyRef.getKid()) { + case ScpKid.SCP11a: + case ScpKid.SCP11b: + case ScpKid.SCP11c: + klcc = 1; + } + storeData(new Tlv(0xA6, Tlvs.encodeList(Arrays.asList( + new Tlv(0x80, new byte[]{klcc}), + new Tlv(0x42, ski), + new Tlv(0x83, keyRef.getBytes()) + ))).getBytes()); + Logger.info(logger, "CA issuer SKI stored"); + } + + /** + * Delete one (or more) keys. + * Requires off-card entity verification. + * All keys matching the given KID and/or KVN will be deleted (0 is treated as a wildcard). + * To delete the final key you must set deleteLast = true. + * + * @param keyRef a reference to the key to delete + * @param deleteLast must be true if deleting the final key, false otherwise + */ + public void deleteKey(KeyRef keyRef, boolean deleteLast) throws ApduException, IOException { + byte kid = keyRef.getKid(); + byte kvn = keyRef.getKvn(); + if (kid == 0 && kvn == 0) { + throw new IllegalArgumentException("At least one of KID, KVN must be nonzero"); + } + if (kid == 1 || kid == 2 || kid == 3) { + if (kvn != 0) { + kid = 0; + } else { + throw new IllegalArgumentException("SCP03 keys can only be deleted by KVN"); + } + } + Logger.debug(logger, "Deleting keys matching {}", keyRef); + List tlvs = new ArrayList<>(); + if (kid != 0) { + tlvs.add(new Tlv(0xD0, new byte[]{kid})); + } + if (kvn != 0) { + tlvs.add(new Tlv(0xD2, new byte[]{kvn})); + } + protocol.sendAndReceive(new Apdu(0x80, INS_DELETE, 0, deleteLast ? 1 : 0, Tlvs.encodeList(tlvs))); + Logger.info(logger, "Keys deleted"); + } + + /** + * Generate a new SCP11 key. + * Requires off-card entity verification. + * + * @param keyRef the KID-KVN pair to assign the new key + * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN + * @return the public key from the generated key pair + */ + public PublicKeyValues.Ec generateEcKey(KeyRef keyRef, int replaceKvn) throws ApduException, IOException, BadResponseException { + Logger.debug(logger, "Generating new key for {}" + + (replaceKvn == 0 ? "" : String.format(Locale.ROOT, ", replacing KVN=0x%02x", replaceKvn)), keyRef); + + byte[] params = new Tlv(KEY_TYPE_ECC_KEY_PARAMS, new byte[]{0}).getBytes(); + byte[] data = ByteBuffer.allocate(params.length + 1).put(keyRef.getKvn()).put(params).array(); + byte[] resp = protocol.sendAndReceive(new Apdu(0x80, INS_GENERATE_KEY, replaceKvn, keyRef.getKid(), data)); + byte[] encodedPoint = Tlvs.unpackValue(KEY_TYPE_ECC_PUBLIC_KEY, resp); + return PublicKeyValues.Ec.fromEncodedPoint(EllipticCurveValues.SECP256R1, encodedPoint); + } + + /** + * Imports an SCP03 key set. + * Requires off-card entity verification. + * + * @param keyRef the KID-KVN pair to assign the new key set, KID must be 1 + * @param keys the key material to import + * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN + */ + public void putKey(KeyRef keyRef, StaticKeys keys, int replaceKvn) { + if (keyRef.getKid() != ScpKid.SCP11a) { + throw new IllegalArgumentException("KID must be 0x01 for SCP03 key sets"); + } + // TODO + throw new UnsupportedOperationException(); + } + + /** + * Imports a secret key for SCP11. + * Requires off-card entity verification. + * + * @param keyRef the KID-KVN pair to assign the new secret key, KID must be 0x11, 0x13, or 0x15 + * @param secretKey a private EC key used to authenticate the SD + * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN + */ + public void putKey(KeyRef keyRef, PrivateKeyValues secretKey, int replaceKvn) { + // TODO + throw new UnsupportedOperationException(); + } + + /** + * Imports a public key for authentication of the off-card entity for SCP11a/c. + * Requires off-card entity verification. + * + * @param keyRef the KID-KVN pair to assign the new public key + * @param publicKey a public EC key used as CA to authenticate the off-card entity + * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN + */ + public void putKey(KeyRef keyRef, PublicKeyValues publicKey, int replaceKvn) { + // TODO + throw new UnsupportedOperationException(); + } + + /** + * Perform a factory reset of the Security Domain. + * This will remove all keys and associated data, as well as restore the default SCP03 static keys, + * and generate a new (attestable) SCP11b key. + */ + public void reset() { + // TODO + throw new UnsupportedOperationException(); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java new file mode 100644 index 00000000..fc7f2838 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java @@ -0,0 +1,59 @@ +/* + * 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.core.smartcard.scp; + +import javax.annotation.Nullable; +import javax.crypto.SecretKey; +import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; + +/** + * Session keys for SCP. DEK only needs to be provided if you need to call {@link SecurityDomainSession#putKey}. + */ +public class SessionKeys implements Destroyable { + final SecretKey senc; + final SecretKey smac; + final SecretKey srmac; + @Nullable + final SecretKey dek; + + private boolean destroyed = false; + + public SessionKeys(SecretKey senc, SecretKey smac, SecretKey srmac, @Nullable SecretKey dek) { + this.senc = senc; + this.smac = smac; + this.srmac = srmac; + this.dek = dek; + } + + @Override + public void destroy() throws DestroyFailedException { + senc.destroy(); + smac.destroy(); + ; + srmac.destroy(); + if (dek != null) { + dek.destroy(); + } + destroyed = true; + } + + @Override + public boolean isDestroyed() { + return destroyed; + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java new file mode 100644 index 00000000..89754d26 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java @@ -0,0 +1,105 @@ +/* + * 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.core.smartcard.scp; + +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.annotation.Nullable; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; + +public class StaticKeys implements Destroyable { + private static final byte[] DEFAULT_KEY = new byte[]{0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f}; + + final SecretKey enc; + final SecretKey mac; + @Nullable + final SecretKey dek; + + private boolean destroyed = false; + + public StaticKeys(byte[] enc, byte[] mac, @Nullable byte[] dek) { + this.enc = new SecretKeySpec(enc, "AES"); + this.mac = new SecretKeySpec(mac, "AES"); + this.dek = dek != null ? new SecretKeySpec(dek, "AES") : null; + } + + @Override + public void destroy() throws DestroyFailedException { + enc.destroy(); + mac.destroy(); + if (dek != null) { + dek.destroy(); + } + destroyed = true; + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + public SessionKeys derive(byte[] context) { + if (destroyed) { + throw new SecurityException("StaticKeys has been destroyed"); + } + // TODO: Implement + return new SessionKeys( + deriveKey(enc, (byte) 0x4, context, (short) 0x80), + deriveKey(mac, (byte) 0x6, context, (short) 0x80), + deriveKey(mac, (byte) 0x7, context, (short) 0x80), + dek + ); + } + + public static StaticKeys getDefaultKeys() { + return new StaticKeys(DEFAULT_KEY, DEFAULT_KEY, DEFAULT_KEY); + } + + static SecretKey deriveKey(SecretKey key, byte t, byte[] context, short l) { + if (!(l == 0x40 || l == 0x80)) { + throw new IllegalArgumentException("l must be 0x40 or 0x80"); + } + byte[] i = ByteBuffer.allocate(16 + context.length) + .put(new byte[11]) + .put(t).put((byte) 0) + .putShort(l) + .put((byte) 1) + .put(context) + .array(); + + byte[] digest = null; + try { + Mac mac = Mac.getInstance("AESCMAC"); + mac.init(key); + digest = mac.doFinal(i); + return new SecretKeySpec(digest, 0, l / 8, "AES"); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new UnsupportedOperationException("Cryptography provider does not support AESCMAC", e); + } finally { + if (digest != null) { + Arrays.fill(digest, (byte) 0); + } + } + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java new file mode 100755 index 00000000..2892dcc9 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020-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. + */ +@PackageNonnullByDefault +package com.yubico.yubikit.core.smartcard.scp; + +import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file diff --git a/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java b/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java index 889821f7..f5eff1b8 100755 --- a/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java +++ b/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java @@ -37,6 +37,7 @@ import com.yubico.yubikit.core.smartcard.AppId; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.core.smartcard.SmartCardProtocol; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Result; import com.yubico.yubikit.core.util.Tlvs; @@ -114,11 +115,25 @@ public boolean isSupportedBy(Version version) { * @throws ApplicationNotAvailableException in case the application is missing/disabled */ public ManagementSession(SmartCardConnection connection) throws IOException, ApplicationNotAvailableException { + this(connection, null); + } + + /** + * Establishes a new session with a YubiKeys Management application, over a {@link SmartCardConnection}. + * + * @param connection connection with YubiKey + * @param scpKeyParams SCP key parameters to establish a secure connection + * @throws IOException in case of connection error + * @throws ApplicationNotAvailableException in case the application is missing/disabled + */ + public ManagementSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) throws IOException, ApplicationNotAvailableException { SmartCardProtocol protocol = new SmartCardProtocol(connection); Version version; try { version = Version.parse(new String(protocol.select(AppId.MANAGEMENT), StandardCharsets.UTF_8)); - if (version.major == 3) { + if (scpKeyParams != null) { + protocol.initScp(scpKeyParams); + } else if (version.major == 3) { // Workaround to "de-select" on NEO connection.sendAndReceive(new byte[]{(byte) 0xa4, 0x04, 0x00, 0x08}); protocol.select(AppId.OTP); @@ -130,6 +145,8 @@ public ManagementSession(SmartCardConnection connection) throws IOException, App } else { throw e; } + } catch (BadResponseException | ApduException e) { + throw new IOException("Failed setting up SCP session", e); } this.version = version; diff --git a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java index ebd97a12..32d8419f 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java @@ -29,6 +29,7 @@ import com.yubico.yubikit.core.smartcard.SW; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.core.smartcard.SmartCardProtocol; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.util.RandomUtils; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; @@ -79,6 +80,11 @@ public class OathSession extends ApplicationSession { */ public static final Feature FEATURE_RENAME = new Feature.Versioned<>("Rename Credential", 5, 3, 0); + /** + * Support for secure messaging. + */ + public static final Feature FEATURE_SCP = new Feature.Versioned<>("SCP", 5, 6, 3); + // Tlv tags YKOATH data private static final int TAG_NAME = 0x71; private static final int TAG_KEY = 0x73; @@ -109,6 +115,8 @@ public class OathSession extends ApplicationSession { private final SmartCardProtocol protocol; private final Version version; + @Nullable + private final ScpKeyParams scpKeyParams; private String deviceId; private byte[] salt; @@ -126,8 +134,29 @@ public class OathSession extends ApplicationSession { * @throws ApplicationNotAvailableException if the application is missing or disabled */ public OathSession(SmartCardConnection connection) throws IOException, ApplicationNotAvailableException { + this(connection, null); + } + + /** + * Establishes a new session with a YubiKeys OATH application. + * + * @param connection to the YubiKey + * @param scpKeyParams SCP key parameters to establish a secure connection + * @throws IOException in case of connection error + * @throws ApplicationNotAvailableException if the application is missing or disabled + */ + public OathSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) throws IOException, ApplicationNotAvailableException { protocol = new SmartCardProtocol(connection, INS_SEND_REMAINING); SelectResponse selectResponse = new SelectResponse(protocol.select(AppId.OATH)); + this.scpKeyParams = scpKeyParams; + if (scpKeyParams != null) { + require(FEATURE_SCP); + try { + protocol.initScp(scpKeyParams); + } catch (ApduException | BadResponseException e) { + throw new IOException("Failed setting up SCP session", e); + } + } version = selectResponse.version; deviceId = selectResponse.getDeviceId(); salt = selectResponse.salt; @@ -171,6 +200,17 @@ public void reset() throws IOException, ApduException { salt = selectResponse.salt; challenge = null; isAccessKeySet = false; + if (scpKeyParams != null) { + if (!scpKeyParams.isDestroyed()) { + try { + protocol.initScp(scpKeyParams); + } catch (BadResponseException e) { + throw new IOException("Failed setting up SCP session", e); + } + } else { + Logger.warn(logger, "SCP session cannot be re-initialized, ScpKeyParams have been destroyed"); + } + } Logger.info(logger, "OATH application data reset performed"); } catch (ApplicationNotAvailableException e) { throw new IllegalStateException(e); // This shouldn't happen diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java index 10145c27..d4609f5d 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java @@ -35,6 +35,7 @@ import com.yubico.yubikit.core.smartcard.SW; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.core.smartcard.SmartCardProtocol; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; @@ -159,6 +160,20 @@ public boolean isSupportedBy(Version version) { */ public OpenPgpSession(SmartCardConnection connection) throws IOException, ApplicationNotAvailableException, ApduException { + this(connection, null); + } + + /** + * Create new instance of {@link OpenPgpSession} and selects the application for use. + * + * @param connection a smart card connection to a YubiKey + * @param scpKeyParams SCP key parameters to establish a secure connection + * @throws IOException in case of communication error + * @throws ApduException in case of an error response from the YubiKey + * @throws ApplicationNotAvailableException if the application is missing or disabled + */ + public OpenPgpSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) throws + IOException, ApplicationNotAvailableException, ApduException { protocol = new SmartCardProtocol(connection); try { @@ -168,6 +183,14 @@ public OpenPgpSession(SmartCardConnection connection) throws activate(e); } + if (scpKeyParams != null) { + try { + protocol.initScp(scpKeyParams); + } catch (BadResponseException e) { + throw new IOException("Failed setting up SCP session", e); + } + } + Logger.debug(logger, "Getting version number"); byte[] versionBcd = protocol.sendAndReceive(new Apdu(0, INS_GET_VERSION, 0, 0, null)); byte[] versionBytes = new byte[3]; diff --git a/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java b/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java index 1132d4b9..ab7c5e6b 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java @@ -34,6 +34,7 @@ import com.yubico.yubikit.core.smartcard.SW; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.core.smartcard.SmartCardProtocol; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.util.RandomUtils; import com.yubico.yubikit.core.util.StringUtils; import com.yubico.yubikit.core.util.Tlv; @@ -220,8 +221,29 @@ public boolean isSupportedBy(Version version) { * @throws ApplicationNotAvailableException if the application is missing or disabled */ public PivSession(SmartCardConnection connection) throws IOException, ApduException, ApplicationNotAvailableException { + this(connection, null); + } + + /** + * Create new instance of {@link PivSession} + * and selects the application for use + * + * @param connection connection with YubiKey + * @throws IOException in case of communication error + * @throws ApduException in case of an error response from the YubiKey + * @throws ApplicationNotAvailableException if the application is missing or disabled + */ + public PivSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) throws IOException, ApduException, ApplicationNotAvailableException { protocol = new SmartCardProtocol(connection); protocol.select(AppId.PIV); + if(scpKeyParams != null) { + try { + protocol.initScp(scpKeyParams); + } catch (BadResponseException e) { + throw new IllegalStateException(e); + } + } + version = Version.fromBytes(protocol.sendAndReceive(new Apdu(0, INS_GET_VERSION, 0, 0, null))); protocol.enableWorkarounds(version); From 31844e06250efa9cf062dbf30c6bf58713531f74 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 2 Jul 2024 16:51:20 +0200 Subject: [PATCH 02/46] Ensure version is set prior to requirement check --- .../java/com/yubico/yubikit/oath/OathSession.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java index 32d8419f..90188197 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java @@ -149,6 +149,12 @@ public OathSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKey protocol = new SmartCardProtocol(connection, INS_SEND_REMAINING); SelectResponse selectResponse = new SelectResponse(protocol.select(AppId.OATH)); this.scpKeyParams = scpKeyParams; + version = selectResponse.version; + deviceId = selectResponse.getDeviceId(); + salt = selectResponse.salt; + challenge = selectResponse.challenge; + isAccessKeySet = challenge != null && challenge.length != 0; + protocol.enableWorkarounds(version); if (scpKeyParams != null) { require(FEATURE_SCP); try { @@ -157,12 +163,6 @@ public OathSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKey throw new IOException("Failed setting up SCP session", e); } } - version = selectResponse.version; - deviceId = selectResponse.getDeviceId(); - salt = selectResponse.salt; - challenge = selectResponse.challenge; - isAccessKeySet = challenge != null && challenge.length != 0; - protocol.enableWorkarounds(version); Logger.debug(logger, "OATH session initialized (version={}, isAccessKeySet={})", version, isAccessKeySet); } From 3bedfa70c74e4c9fdb92e6458701396e3d06f12c Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 3 Jul 2024 09:46:46 +0200 Subject: [PATCH 03/46] Return ApduResponse instead of byte[] --- .../yubikit/core/smartcard/ApduFormatProcessor.java | 5 +++-- .../com/yubico/yubikit/core/smartcard/ApduProcessor.java | 2 +- .../yubikit/core/smartcard/ChainedResponseProcessor.java | 6 +++--- .../com/yubico/yubikit/core/smartcard/ScpProcessor.java | 8 ++++---- .../yubico/yubikit/core/smartcard/ShortApduProcessor.java | 8 ++++---- .../yubico/yubikit/core/smartcard/SmartCardProtocol.java | 4 ++-- .../yubikit/core/smartcard/TouchWorkaroundProcessor.java | 6 +++--- .../com/yubico/yubikit/core/smartcard/scp/ScpState.java | 6 +++--- 8 files changed, 23 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java index 67c10fdb..676d332d 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java @@ -28,8 +28,9 @@ abstract class ApduFormatProcessor implements ApduProcessor { abstract byte[] formatApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, int length, int le); @Override - public byte[] sendApdu(Apdu apdu) throws IOException { + public ApduResponse sendApdu(Apdu apdu) throws IOException { byte[] data = apdu.getData(); - return connection.sendAndReceive(formatApdu(apdu.getCla(), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, 0, data.length, apdu.getLe())); + byte[] payload = formatApdu(apdu.getCla(), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, 0, data.length, apdu.getLe()); + return new ApduResponse(connection.sendAndReceive(payload)); } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java index 2977d4f4..9dedc1ba 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java @@ -21,5 +21,5 @@ import java.io.IOException; public interface ApduProcessor { - byte[] sendApdu(Apdu apdu) throws IOException, BadResponseException; + ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException; } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java index f9465822..c4c2d094 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java @@ -39,8 +39,8 @@ class ChainedResponseProcessor implements ApduProcessor { } @Override - public byte[] sendApdu(Apdu apdu) throws IOException, BadResponseException { - ApduResponse response = new ApduResponse(processor.sendApdu(apdu)); + public ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException { + ApduResponse response = processor.sendApdu(apdu); // Read full response ByteArrayOutputStream readBuffer = new ByteArrayOutputStream(); while (response.getSw() >> 8 == SW1_HAS_MORE_DATA) { @@ -50,6 +50,6 @@ public byte[] sendApdu(Apdu apdu) throws IOException, BadResponseException { readBuffer.write(response.getData()); readBuffer.write(response.getSw() >> 8); readBuffer.write(response.getSw() & 0xff); - return readBuffer.toByteArray(); + return new ApduResponse(readBuffer.toByteArray()); } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java index d9bb7c57..028aa00e 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java @@ -32,11 +32,11 @@ public class ScpProcessor extends ChainedResponseProcessor { } @Override - public byte[] sendApdu(Apdu apdu) throws IOException, BadResponseException { + public ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException { return sendApdu(apdu, true); } - public byte[] sendApdu(Apdu apdu, boolean encrypt) throws IOException, BadResponseException { + public ApduResponse sendApdu(Apdu apdu, boolean encrypt) throws IOException, BadResponseException { byte[] data = apdu.getData(); if (encrypt) { data = state.encrypt(data); @@ -50,7 +50,7 @@ public byte[] sendApdu(Apdu apdu, boolean encrypt) throws IOException, BadRespon byte[] mac = state.mac(Arrays.copyOf(apduData, apduData.length - 8)); System.arraycopy(mac, 0, macedData, macedData.length - 8, 8); - ApduResponse resp = new ApduResponse(super.sendApdu(new Apdu(cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, apdu.getLe()))); + ApduResponse resp = super.sendApdu(new Apdu(cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, apdu.getLe())); byte[] respData = resp.getData(); // Un-MAC and decrypt, if needed @@ -61,6 +61,6 @@ public byte[] sendApdu(Apdu apdu, boolean encrypt) throws IOException, BadRespon respData = state.decrypt(respData); } - return ByteBuffer.allocate(respData.length + 2).put(respData).putShort(resp.getSw()).array(); + return new ApduResponse(ByteBuffer.allocate(respData.length + 2).put(respData).putShort(resp.getSw()).array()); } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java index 9a62de2f..cc395ced 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java @@ -50,16 +50,16 @@ byte[] formatApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, } @Override - public byte[] sendApdu(Apdu apdu) throws IOException { + public ApduResponse sendApdu(Apdu apdu) throws IOException { byte[] data = apdu.getData(); int offset = 0; while (data.length - offset > SHORT_APDU_MAX_CHUNK) { - byte[] response = connection.sendAndReceive(formatApdu((byte) (apdu.getCla() | 0x10), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, offset, SHORT_APDU_MAX_CHUNK, apdu.getLe())); - if (new ApduResponse(response).getSw() != SW.OK) { + ApduResponse response = new ApduResponse(connection.sendAndReceive(formatApdu((byte) (apdu.getCla() | 0x10), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, offset, SHORT_APDU_MAX_CHUNK, apdu.getLe()))); + if (response.getSw() != SW.OK) { return response; } offset += SHORT_APDU_MAX_CHUNK; } - return connection.sendAndReceive(formatApdu(apdu.getCla(), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, offset, data.length - offset, apdu.getLe())); + return new ApduResponse(connection.sendAndReceive(formatApdu(apdu.getCla(), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, offset, data.length - offset, apdu.getLe()))); } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java index 4e7fc870..f2d62505 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java @@ -167,7 +167,7 @@ public byte[] select(byte[] aid) throws IOException, ApplicationNotAvailableExce */ public byte[] sendAndReceive(Apdu command) throws IOException, ApduException { try { - ApduResponse response = new ApduResponse(processor.sendApdu(command)); + ApduResponse response = processor.sendApdu(command); if (response.getSw() != SW.OK) { throw new ApduException(response.getSw()); } @@ -202,7 +202,7 @@ private void initScp03(Scp03KeyParams keyParams) throws IOException, ApduExcepti // Send EXTERNAL AUTHENTICATE // P1 = C-DECRYPTION, R-ENCRYPTION, C-MAC, and R-MAC - ApduResponse resp = new ApduResponse(processor.sendApdu(new Apdu(0x84, 0x82, 0x33, 0, pair.second), false)); + ApduResponse resp = processor.sendApdu(new Apdu(0x84, 0x82, 0x33, 0, pair.second), false); if (resp.getSw() != SW.OK) { throw new ApduException(resp.getSw()); } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java index ea0ed41d..eccf0f00 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java @@ -28,14 +28,14 @@ class TouchWorkaroundProcessor extends ChainedResponseProcessor { } @Override - public byte[] sendApdu(Apdu apdu) throws IOException, BadResponseException { + public ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException { if (lastLongResponse > 0 && System.currentTimeMillis() - lastLongResponse < 2000) { super.sendApdu(new Apdu(0, 0, 0, 0, null)); // Dummy APDU; returns an error lastLongResponse = 0; } - byte[] response = super.sendApdu(apdu); + ApduResponse response = super.sendApdu(apdu); - if (response.length > 54) { + if (response.getBytes().length > 54) { lastLongResponse = System.currentTimeMillis(); } else { lastLongResponse = 0; diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java index 283c57ef..5ce90c26 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java @@ -158,7 +158,7 @@ public static Pair scp03Init(ApduProcessor processor, Scp03Key hostChallenge = RandomUtils.getRandomBytes(8); } - ApduResponse resp = new ApduResponse(processor.sendApdu(new Apdu(0x80, 0x50, keyParams.getKeyRef().getKvn(), 0x00, hostChallenge))); + ApduResponse resp = processor.sendApdu(new Apdu(0x80, 0x50, keyParams.getKeyRef().getKvn(), 0x00, hostChallenge)); if (resp.getSw() != SW.OK) { throw new ApduException(resp.getSw()); } @@ -215,7 +215,7 @@ public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyPara try { byte[] data = keyParams.certificates.get(i).getEncoded(); byte p2 = (byte) (oceRef.getKid() | (i < n ? 0x80 : 0x00)); - ApduResponse resp = new ApduResponse(processor.sendApdu(new Apdu(0x80, 0x2A, oceRef.getKvn(), p2, data))); + ApduResponse resp = processor.sendApdu(new Apdu(0x80, 0x2A, oceRef.getKvn(), p2, data)); if (resp.getSw() != SW.OK) { throw new ApduException(resp.getSw()); } @@ -261,7 +261,7 @@ public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyPara // Static host key (SCP11a/c), or ephemeral key again (SCP11b) PrivateKey skOceEcka = keyParams.skOceEcka != null ? keyParams.skOceEcka : ephemeralOceEcka.getPrivate(); int ins = keyParams.getKeyRef().getKid() == ScpKid.SCP11b ? 0x88 : 0x82; - ApduResponse resp = new ApduResponse(processor.sendApdu(new Apdu(0x80, ins, keyParams.getKeyRef().getKvn(), keyParams.getKeyRef().getKid(), data))); + ApduResponse resp = processor.sendApdu(new Apdu(0x80, ins, keyParams.getKeyRef().getKvn(), keyParams.getKeyRef().getKid(), data)); if (resp.getSw() != SW.OK) { throw new ApduException(resp.getSw()); } From 9ff4ed0ce4375256550a27cc2b19f295f4f81a5e Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 5 Jul 2024 12:18:12 +0200 Subject: [PATCH 04/46] Implement SecurityDomain.reset() --- .../yubikit/core/smartcard/scp/ScpState.java | 8 +-- .../smartcard/scp/SecurityDomainSession.java | 54 +++++++++++++++++-- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java index 5ce90c26..c8d5bc42 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java @@ -158,7 +158,7 @@ public static Pair scp03Init(ApduProcessor processor, Scp03Key hostChallenge = RandomUtils.getRandomBytes(8); } - ApduResponse resp = processor.sendApdu(new Apdu(0x80, 0x50, keyParams.getKeyRef().getKvn(), 0x00, hostChallenge)); + ApduResponse resp = processor.sendApdu(new Apdu(0x80, SecurityDomainSession.INS_INITIALIZE_UPDATE, keyParams.getKeyRef().getKvn(), 0x00, hostChallenge)); if (resp.getSw() != SW.OK) { throw new ApduException(resp.getSw()); } @@ -215,7 +215,7 @@ public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyPara try { byte[] data = keyParams.certificates.get(i).getEncoded(); byte p2 = (byte) (oceRef.getKid() | (i < n ? 0x80 : 0x00)); - ApduResponse resp = processor.sendApdu(new Apdu(0x80, 0x2A, oceRef.getKvn(), p2, data)); + ApduResponse resp = processor.sendApdu(new Apdu(0x80, SecurityDomainSession.INS_PERFORM_SECURITY_OPERATION, oceRef.getKvn(), p2, data)); if (resp.getSw() != SW.OK) { throw new ApduException(resp.getSw()); } @@ -260,7 +260,7 @@ public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyPara // Static host key (SCP11a/c), or ephemeral key again (SCP11b) PrivateKey skOceEcka = keyParams.skOceEcka != null ? keyParams.skOceEcka : ephemeralOceEcka.getPrivate(); - int ins = keyParams.getKeyRef().getKid() == ScpKid.SCP11b ? 0x88 : 0x82; + int ins = keyParams.getKeyRef().getKid() == ScpKid.SCP11b ? SecurityDomainSession.INS_INTERNAL_AUTHENTICATE : SecurityDomainSession.INS_EXTERNAL_AUTHENTICATE; ApduResponse resp = processor.sendApdu(new Apdu(0x80, ins, keyParams.getKeyRef().getKvn(), keyParams.getKeyRef().getKid(), data)); if (resp.getSw() != SW.OK) { throw new ApduException(resp.getSw()); @@ -305,7 +305,7 @@ public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyPara byte[] digest = hash.digest(); keys.add(new SecretKeySpec(digest, 0, 16, "AES")); keys.add(new SecretKeySpec(digest, 16, 16, "AES")); - Arrays.fill(digest, (byte)0); + Arrays.fill(digest, (byte) 0); } // 6 keys were derived. one for verification of receipt, 4 keys to use, and 1 which is discarded diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java index 0fed0f5c..21072a72 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java @@ -62,6 +62,11 @@ public class SecurityDomainSession extends ApplicationSession Date: Fri, 5 Jul 2024 12:59:39 +0200 Subject: [PATCH 05/46] Refactor SCP Deprecate individual configuration methods on SmartCardProtocol in favor of a single .configure() method. Make ApduProcessor Closable. Add SCP support to YubiOtpSession. --- .../yubikit/core/smartcard/ApduProcessor.java | 3 +- .../smartcard/ChainedResponseProcessor.java | 5 + .../core/smartcard/ExtendedApduProcessor.java | 5 + .../yubikit/core/smartcard/ScpProcessor.java | 12 +++ .../core/smartcard/ShortApduProcessor.java | 4 + .../core/smartcard/SmartCardProtocol.java | 95 +++++++++++++------ .../yubikit/core/smartcard/scp/ScpState.java | 29 ++++-- .../smartcard/scp/SecurityDomainSession.java | 2 +- .../com/yubico/yubikit/oath/OathSession.java | 2 +- .../yubikit/openpgp/OpenPgpSession.java | 7 +- .../com/yubico/yubikit/piv/PivSession.java | 7 +- .../yubikit/yubiotp/YubiOtpSession.java | 24 ++++- 12 files changed, 146 insertions(+), 49 deletions(-) diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java index 9dedc1ba..4303bf9e 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java @@ -18,8 +18,9 @@ import com.yubico.yubikit.core.application.BadResponseException; +import java.io.Closeable; import java.io.IOException; -public interface ApduProcessor { +public interface ApduProcessor extends Closeable { ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException; } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java index c4c2d094..d3147a1b 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java @@ -52,4 +52,9 @@ public ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException readBuffer.write(response.getSw() & 0xff); return new ApduResponse(readBuffer.toByteArray()); } + + @Override + public void close() throws IOException { + processor.close(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java index 6850be41..d235d3f9 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java @@ -16,6 +16,7 @@ package com.yubico.yubikit.core.smartcard; +import java.io.IOException; import java.nio.ByteBuffer; class ExtendedApduProcessor extends ApduFormatProcessor { @@ -45,4 +46,8 @@ byte[] formatApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, } return buf.array(); } + + @Override + public void close() throws IOException { + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java index 028aa00e..d554a2e7 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java @@ -23,6 +23,8 @@ import java.nio.ByteBuffer; import java.util.Arrays; +import javax.security.auth.DestroyFailedException; + public class ScpProcessor extends ChainedResponseProcessor { private final ScpState state; @@ -63,4 +65,14 @@ public ApduResponse sendApdu(Apdu apdu, boolean encrypt) throws IOException, Bad return new ApduResponse(ByteBuffer.allocate(respData.length + 2).put(respData).putShort(resp.getSw()).array()); } + + @Override + public void close() throws IOException { + try { + state.destroy(); + } catch (DestroyFailedException e) { + throw new IOException(e); + } + super.close(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java index cc395ced..20088bc6 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java @@ -62,4 +62,8 @@ public ApduResponse sendApdu(Apdu apdu) throws IOException { } return new ApduResponse(connection.sendAndReceive(formatApdu(apdu.getCla(), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, offset, data.length - offset, apdu.getLe()))); } + + @Override + public void close() throws IOException { + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java index f2d62505..37a35548 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java @@ -29,6 +29,8 @@ import java.io.Closeable; import java.io.IOException; +import javax.annotation.Nullable; + /** * Support class for communication over a SmartCardConnection. *

@@ -45,7 +47,7 @@ public class SmartCardProtocol implements Closeable { private final SmartCardConnection connection; - private ApduFormat apduFormat = ApduFormat.SHORT; + private boolean extendedApdus = false; private int maxApduSize = MaxApduSize.NEO; @@ -64,33 +66,54 @@ public SmartCardProtocol(SmartCardConnection connection) { public SmartCardProtocol(SmartCardConnection connection, byte insSendRemaining) { this.connection = connection; this.insSendRemaining = insSendRemaining; - processor = resetProcessor(); + processor = new ChainedResponseProcessor(connection, false, maxApduSize, insSendRemaining); } - private ApduProcessor resetProcessor() { - return new ChainedResponseProcessor(connection, apduFormat == ApduFormat.EXTENDED, maxApduSize, insSendRemaining); + private void resetProcessor(@Nullable ApduProcessor processor) throws IOException { + this.processor.close(); + if (processor != null) { + this.processor = processor; + } else { + this.processor = new ChainedResponseProcessor(connection, extendedApdus, maxApduSize, insSendRemaining); + } } @Override public void close() throws IOException { + processor.close(); connection.close(); } /** - * Enable all relevant workarounds given the firmware version of the YubiKey. + * Enable all relevant settings and workarounds given the firmware version of the YubiKey. * - * @param firmwareVersion the firmware version to use for detection to enable the workarounds + * @param firmwareVersion the firmware version to use to configure relevant settings */ - public void enableWorkarounds(Version firmwareVersion) { + public void configure(Version firmwareVersion) throws IOException { if (connection.getTransport() == Transport.USB && firmwareVersion.isAtLeast(4, 2, 0) && firmwareVersion.isLessThan(4, 2, 7)) { //noinspection deprecation setEnableTouchWorkaround(true); } else if (firmwareVersion.isAtLeast(4, 0, 0) && !(processor instanceof ScpProcessor)) { - apduFormat = ApduFormat.EXTENDED; + extendedApdus = connection.isExtendedLengthApduSupported(); maxApduSize = firmwareVersion.isAtLeast(4, 3, 0) ? MaxApduSize.YK4_3 : MaxApduSize.YK4; - processor = resetProcessor(); + resetProcessor(null); + } + } + + /** + * Enable all relevant workarounds given the firmware version of the YubiKey. + * + * @param firmwareVersion the firmware version to use for detection to enable the workarounds + * @deprecated use {@link #configure(Version)} instead. + */ + @Deprecated + public void enableWorkarounds(Version firmwareVersion) { + try { + configure(firmwareVersion); + } catch (IOException e) { + throw new RuntimeException(e); } } @@ -99,16 +122,20 @@ public void enableWorkarounds(Version firmwareVersion) { * on such devices to trigger sending a dummy command which mitigates the issue. * * @param enableTouchWorkaround true to enable the workaround, false to disable it - * @deprecated use {@link #enableWorkarounds} instead. + * @deprecated use {@link #configure(Version)} instead. */ @Deprecated public void setEnableTouchWorkaround(boolean enableTouchWorkaround) { - if (enableTouchWorkaround) { - apduFormat = ApduFormat.EXTENDED; - maxApduSize = MaxApduSize.YK4; - processor = new TouchWorkaroundProcessor(connection, insSendRemaining); - } else { - processor = resetProcessor(); + try { + if (enableTouchWorkaround) { + extendedApdus = true; + maxApduSize = MaxApduSize.YK4; + resetProcessor(new TouchWorkaroundProcessor(connection, insSendRemaining)); + } else { + resetProcessor(null); + } + } catch (IOException e) { + throw new RuntimeException(e); } } @@ -116,16 +143,27 @@ public void setEnableTouchWorkaround(boolean enableTouchWorkaround) { * YubiKey NEO doesn't support extended APDU's for most applications. * * @param apduFormat the APDU encoding to use when sending commands + * @deprecated use {@link #configure(Version)} instead. */ + @Deprecated public void setApduFormat(ApduFormat apduFormat) { - if (this.apduFormat == apduFormat) { - return; - } - if (apduFormat != ApduFormat.EXTENDED) { - throw new UnsupportedOperationException("Cannot change from EXTENDED to SHORT APDU format"); + switch (apduFormat) { + case SHORT: + if (extendedApdus) { + throw new UnsupportedOperationException("Cannot change from EXTENDED to SHORT APDU format"); + } + break; + case EXTENDED: + if (!extendedApdus) { + extendedApdus = true; + try { + resetProcessor(null); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + break; } - this.apduFormat = apduFormat; - processor = resetProcessor(); } /** @@ -144,7 +182,7 @@ public SmartCardConnection getConnection() { * @throws ApplicationNotAvailableException in case the AID doesn't match an available application */ public byte[] select(byte[] aid) throws IOException, ApplicationNotAvailableException { - processor = resetProcessor(); + resetProcessor(null); try { return sendAndReceive(new Apdu(0, INS_SELECT, P1_SELECT, P2_SELECT, aid)); } catch (ApduException e) { @@ -186,7 +224,10 @@ public void initScp(ScpKeyParams keyParams) throws IOException, ApduException, B } else { throw new IllegalArgumentException("Unsupported ScpKeyParams"); } - apduFormat = ApduFormat.EXTENDED; + if (!connection.isExtendedLengthApduSupported()) { + throw new IllegalStateException("SCP requires extended APDU support"); + } + extendedApdus = true; maxApduSize = MaxApduSize.YK4_3; } catch (ApduException e) { if (e.getSw() == SW.CLASS_NOT_SUPPORTED) { @@ -206,11 +247,11 @@ private void initScp03(Scp03KeyParams keyParams) throws IOException, ApduExcepti if (resp.getSw() != SW.OK) { throw new ApduException(resp.getSw()); } - this.processor = processor; + resetProcessor(processor); } private void initScp11(Scp11KeyParams keyParams) throws IOException, ApduException, BadResponseException { ScpState scp = ScpState.scp11Init(processor, keyParams); - processor = new ScpProcessor(connection, scp, MaxApduSize.YK4_3, insSendRemaining); + resetProcessor(new ScpProcessor(connection, scp, MaxApduSize.YK4_3, insSendRemaining)); } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java index c8d5bc42..af73c490 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java @@ -56,12 +56,13 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; /** * Internal SCP state class for managing SCP state, handling encryption/decryption and MAC. */ -public class ScpState { +public class ScpState implements Destroyable { private final SessionKeys keys; private byte[] macChain; private int encCounter = 1; @@ -74,9 +75,8 @@ public ScpState(SessionKeys keys, byte[] macChain) { public byte[] encrypt(byte[] data) { // Pad the data int padLen = 16 - (data.length % 16); - byte[] msg = new byte[data.length + padLen]; - System.arraycopy(data, 0, msg, 0, data.length); - msg[data.length] = (byte) 0x80; + byte[] padded = Arrays.copyOf(data, data.length + padLen); + padded[data.length] = (byte) 0x80; // Encrypt try { @@ -87,17 +87,20 @@ public byte[] encrypt(byte[] data) { cipher = Cipher.getInstance("AES/CBC/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, keys.senc, new IvParameterSpec(iv)); - return cipher.doFinal(msg); + return cipher.doFinal(padded); } catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) { //This should never happen throw new RuntimeException(e); + } finally { + Arrays.fill(padded, (byte) 0); } } public byte[] decrypt(byte[] encrypted) throws BadResponseException { // Decrypt + byte[] decrypted = null; try { @SuppressWarnings("GetInstance") Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, keys.senc); @@ -106,7 +109,7 @@ public byte[] decrypt(byte[] encrypted) throws BadResponseException { cipher = Cipher.getInstance("AES/CBC/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, keys.senc, new IvParameterSpec(iv)); - byte[] decrypted = cipher.doFinal(encrypted); + decrypted = cipher.doFinal(encrypted); for (int i = decrypted.length - 1; i > 0; i--) { if (decrypted[i] == (byte) 0x80) { return Arrays.copyOf(decrypted, i); @@ -120,6 +123,10 @@ public byte[] decrypt(byte[] encrypted) throws BadResponseException { InvalidAlgorithmParameterException e) { //This should never happen throw new RuntimeException(e); + } finally { + if (decrypted != null) { + Arrays.fill(decrypted, (byte) 0); + } } } @@ -336,4 +343,14 @@ public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyPara throw new RuntimeException(e); } } + + @Override + public void destroy() throws DestroyFailedException { + keys.destroy(); + } + + @Override + public boolean isDestroyed() { + return keys.isDestroyed(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java index 21072a72..b5ce83f6 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java @@ -86,7 +86,7 @@ public SecurityDomainSession(SmartCardConnection connection) throws IOException, protocol = new SmartCardProtocol(connection); protocol.select(AppId.SECURITYDOMAIN); // We don't know the version, but we know it's at least 5.3.0 - protocol.enableWorkarounds(new Version(5, 3, 0)); + protocol.configure(new Version(5, 3, 0)); Logger.debug(logger, "Security Domain session initialized"); } diff --git a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java index 90188197..c2053e91 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java @@ -154,7 +154,7 @@ public OathSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKey salt = selectResponse.salt; challenge = selectResponse.challenge; isAccessKeySet = challenge != null && challenge.length != 0; - protocol.enableWorkarounds(version); + protocol.configure(version); if (scpKeyParams != null) { require(FEATURE_SCP); try { diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java index d4609f5d..64f9bca8 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java @@ -198,12 +198,7 @@ public OpenPgpSession(SmartCardConnection connection, @Nullable ScpKeyParams scp versionBytes[i] = decodeBcd(versionBcd[i]); } version = Version.fromBytes(versionBytes); - protocol.enableWorkarounds(version); - - // use extended length APDUs on compatible connections and devices - if (connection.isExtendedLengthApduSupported() && version.isAtLeast(4, 0, 0)) { - protocol.setApduFormat(ApduFormat.EXTENDED); - } + protocol.configure(version); // Note: This value is cached! // Do not rely on contained information that can change! diff --git a/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java b/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java index ab7c5e6b..c4d3699f 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java @@ -245,12 +245,7 @@ public PivSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyP } version = Version.fromBytes(protocol.sendAndReceive(new Apdu(0, INS_GET_VERSION, 0, 0, null))); - protocol.enableWorkarounds(version); - - // use extended length APDUs on compatible connections and devices - if (connection.isExtendedLengthApduSupported() && version.isAtLeast(4, 0, 0)) { - protocol.setApduFormat(ApduFormat.EXTENDED); - } + protocol.configure(version); try { managementKeyType = getManagementKeyMetadata().getKeyType(); diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java index c67f4b10..79e9a850 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java @@ -21,6 +21,7 @@ import com.yubico.yubikit.core.Version; import com.yubico.yubikit.core.YubiKeyConnection; import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.core.smartcard.AppId; import com.yubico.yubikit.core.application.ApplicationNotAvailableException; import com.yubico.yubikit.core.application.ApplicationSession; @@ -34,6 +35,7 @@ import com.yubico.yubikit.core.smartcard.Apdu; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.core.smartcard.SmartCardProtocol; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Result; @@ -146,6 +148,19 @@ public static void create(YubiKeyDevice device, Callback(protocol, version, parseConfigState(version, statusBytes)) { // 5.0.0-5.2.5 have an issue with status over NFC From 671420c30151ed19e2812ddafffe7c74a4df530b Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 5 Jul 2024 16:25:07 +0200 Subject: [PATCH 06/46] Implement Security Domain PUT KEY --- .../core/smartcard/SmartCardProtocol.java | 15 ++- .../core/smartcard/scp/DataEncryptor.java | 24 +++++ .../yubikit/core/smartcard/scp/ScpState.java | 19 ++++ .../smartcard/scp/SecurityDomainSession.java | 94 ++++++++++++++++--- 4 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 core/src/main/java/com/yubico/yubikit/core/smartcard/scp/DataEncryptor.java diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java index 37a35548..89822961 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java @@ -20,6 +20,7 @@ import com.yubico.yubikit.core.Version; import com.yubico.yubikit.core.application.ApplicationNotAvailableException; import com.yubico.yubikit.core.application.BadResponseException; +import com.yubico.yubikit.core.smartcard.scp.DataEncryptor; import com.yubico.yubikit.core.smartcard.scp.Scp03KeyParams; import com.yubico.yubikit.core.smartcard.scp.Scp11KeyParams; import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; @@ -215,12 +216,13 @@ public byte[] sendAndReceive(Apdu command) throws IOException, ApduException { } } - public void initScp(ScpKeyParams keyParams) throws IOException, ApduException, BadResponseException { + public @Nullable DataEncryptor initScp(ScpKeyParams keyParams) throws IOException, ApduException, BadResponseException { try { + ScpState state; if (keyParams instanceof Scp03KeyParams) { - initScp03((Scp03KeyParams) keyParams); + state = initScp03((Scp03KeyParams) keyParams); } else if (keyParams instanceof Scp11KeyParams) { - initScp11((Scp11KeyParams) keyParams); + state = initScp11((Scp11KeyParams) keyParams); } else { throw new IllegalArgumentException("Unsupported ScpKeyParams"); } @@ -229,6 +231,7 @@ public void initScp(ScpKeyParams keyParams) throws IOException, ApduException, B } extendedApdus = true; maxApduSize = MaxApduSize.YK4_3; + return state.getDataEncryptor(); } catch (ApduException e) { if (e.getSw() == SW.CLASS_NOT_SUPPORTED) { throw new UnsupportedOperationException("This YubiKey does not support secure messaging"); @@ -237,7 +240,7 @@ public void initScp(ScpKeyParams keyParams) throws IOException, ApduException, B } } - private void initScp03(Scp03KeyParams keyParams) throws IOException, ApduException, BadResponseException { + private ScpState initScp03(Scp03KeyParams keyParams) throws IOException, ApduException, BadResponseException { Pair pair = ScpState.scp03Init(processor, keyParams, null); ScpProcessor processor = new ScpProcessor(connection, pair.first, MaxApduSize.YK4_3, insSendRemaining); @@ -248,10 +251,12 @@ private void initScp03(Scp03KeyParams keyParams) throws IOException, ApduExcepti throw new ApduException(resp.getSw()); } resetProcessor(processor); + return pair.first; } - private void initScp11(Scp11KeyParams keyParams) throws IOException, ApduException, BadResponseException { + private ScpState initScp11(Scp11KeyParams keyParams) throws IOException, ApduException, BadResponseException { ScpState scp = ScpState.scp11Init(processor, keyParams); resetProcessor(new ScpProcessor(connection, scp, MaxApduSize.YK4_3, insSendRemaining)); + return scp; } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/DataEncryptor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/DataEncryptor.java new file mode 100644 index 00000000..33e1aa29 --- /dev/null +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/DataEncryptor.java @@ -0,0 +1,24 @@ +/* + * 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.core.smartcard.scp; + +/** + * Encrypts data using the DEK (data encryption key) of a current SCP session. + */ +public interface DataEncryptor { + byte[] encrypt(byte[] data); +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java index af73c490..d74f5c5b 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java @@ -72,6 +72,13 @@ public ScpState(SessionKeys keys, byte[] macChain) { this.macChain = macChain; } + public @Nullable DataEncryptor getDataEncryptor() { + if (keys.dek == null) { + return null; + } + return data -> cbcEncrypt(keys.dek, data); + } + public byte[] encrypt(byte[] data) { // Pad the data int padLen = 16 - (data.length % 16); @@ -353,4 +360,16 @@ public void destroy() throws DestroyFailedException { public boolean isDestroyed() { return keys.isDestroyed(); } + + static byte[] cbcEncrypt(SecretKey key, byte[] data) { + try { + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(new byte[16])); + return cipher.doFinal(data); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | + InvalidAlgorithmParameterException | IllegalBlockSizeException | + BadPaddingException e) { + throw new RuntimeException(e); + } + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java index b5ce83f6..3f29bfc5 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java @@ -42,6 +42,7 @@ import java.io.InputStream; import java.math.BigInteger; import java.nio.ByteBuffer; +import java.security.MessageDigest; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -54,8 +55,11 @@ import java.util.Map; import javax.annotation.Nullable; +import javax.crypto.SecretKey; public class SecurityDomainSession extends ApplicationSession { + private static final byte[] DEFAULT_KCV_IV = new byte[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + private static final byte INS_GET_DATA = (byte) 0xCA; private static final byte INS_PUT_KEY = (byte) 0xD8; private static final byte INS_STORE_DATA = (byte) 0xE2; @@ -79,6 +83,8 @@ public class SecurityDomainSession extends ApplicationSession Date: Fri, 5 Jul 2024 17:47:33 +0200 Subject: [PATCH 07/46] initial integration tests of FIPS approved PIV --- .../com/yubico/yubikit/piv/PivSession.java | 2 +- .../testing/MultiProtocolResetTests.java | 4 +- .../yubikit/testing/PinComplexityTests.java | 4 +- .../yubikit/testing/piv/PivFipsTests.java} | 19 +++-- .../yubico/yubikit/testing/piv/PivTests.java | 10 +++ .../framework/PivInstrumentedTests.java | 3 +- .../framework/YKInstrumentedTests.java | 8 ++ .../com/yubico/yubikit/testing/TestState.java | 71 ++++++++++++++++ .../testing/piv/PivCertificateTests.java | 2 +- .../yubikit/testing/piv/PivDeviceTests.java | 83 +++++++++++++++++-- .../yubikit/testing/piv/PivTestState.java | 23 +++++ 11 files changed, 208 insertions(+), 21 deletions(-) rename testing-android/src/{main/java/com/yubico/yubikit/testing/framework/DeviceInstrumentedTests.java => androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java} (62%) mode change 100755 => 100644 create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/TestState.java create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestState.java diff --git a/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java b/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java index c4d3699f..336b61b1 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java @@ -1458,7 +1458,7 @@ private int getRetriesFromCode(int statusCode) { if (statusCode == SW.AUTH_METHOD_BLOCKED) { return 0; } - if (version.isLessThan(1, 0, 4)) { + if (version.isAtLeast(1, 0, 0) && version.isLessThan(1, 0, 4)) { if (statusCode >= 0x6300 && statusCode <= 0x63ff) { return statusCode & 0xff; } 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 index d8a12310..da1ec151 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/MultiProtocolResetTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/MultiProtocolResetTests.java @@ -16,12 +16,12 @@ package com.yubico.yubikit.testing; -import com.yubico.yubikit.testing.framework.DeviceInstrumentedTests; +import com.yubico.yubikit.testing.framework.YKInstrumentedTests; import org.junit.Before; import org.junit.Test; -public class MultiProtocolResetTests extends DeviceInstrumentedTests { +public class MultiProtocolResetTests extends YKInstrumentedTests { @Before public void setupDevice() throws Throwable { diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/PinComplexityTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/PinComplexityTests.java index ea3176bb..bcc0dbbd 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/PinComplexityTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/PinComplexityTests.java @@ -16,11 +16,11 @@ package com.yubico.yubikit.testing; -import com.yubico.yubikit.testing.framework.DeviceInstrumentedTests; +import com.yubico.yubikit.testing.framework.YKInstrumentedTests; import org.junit.Test; -public class PinComplexityTests extends DeviceInstrumentedTests { +public class PinComplexityTests extends YKInstrumentedTests { @Test public void testPinComplexity() throws Throwable { diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/DeviceInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java old mode 100755 new mode 100644 similarity index 62% rename from testing-android/src/main/java/com/yubico/yubikit/testing/framework/DeviceInstrumentedTests.java rename to testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java index daeaf873..2a39aa7f --- a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/DeviceInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java @@ -14,17 +14,18 @@ * limitations under the License. */ -package com.yubico.yubikit.testing.framework; +package com.yubico.yubikit.testing.piv; -import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.smartcard.scp.ScpKid; -public class DeviceInstrumentedTests extends YKInstrumentedTests { +import org.junit.Before; - public interface Callback { - void invoke(YubiKeyDevice value) throws Throwable; - } - protected void withDevice(Callback callback) throws Throwable { - callback.invoke(device); +public class PivFipsTests extends PivTests { + + @Before + public void setup() throws Throwable { + kid = ScpKid.SCP11b; + super.setup(); } -} \ No newline at end of file +} 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 b197b480..f9b7d63c 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 @@ -18,10 +18,20 @@ import com.yubico.yubikit.testing.framework.PivInstrumentedTests; +import org.junit.Before; import org.junit.Test; +import javax.annotation.Nullable; + public class PivTests extends PivInstrumentedTests { + @Nullable protected Byte kid; + + @Before + public void setup() throws Throwable { + withDevice(device -> PivDeviceTests.verifyAndSetup(device, kid)); + } + @Test public void testPin() throws Throwable { withPivSession(PivDeviceTests::testPin); 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 3f221413..50dba918 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 @@ -18,6 +18,7 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.piv.PivSession; +import com.yubico.yubikit.testing.TestState; public class PivInstrumentedTests extends YKInstrumentedTests { @@ -28,7 +29,7 @@ public interface Callback { protected void withPivSession(Callback callback) throws Throwable { try (SmartCardConnection c = device.openConnection(SmartCardConnection.class)) { - PivSession pivSession = new PivSession(c); + PivSession pivSession = new PivSession(c, TestState.keyParams); callback.invoke(pivSession); } } 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 58778e76..89c1f15e 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 @@ -49,5 +49,13 @@ public void releaseYubiKey() throws InterruptedException { device = null; activity = null; } + + public interface Callback { + void invoke(YubiKeyDevice value) throws Throwable; + } + + protected void withDevice(Callback callback) throws Throwable { + callback.invoke(device); + } } 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..3c5c31d5 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -0,0 +1,71 @@ +/* + * 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.YubiKeyDevice; +import com.yubico.yubikit.core.application.BadResponseException; +import com.yubico.yubikit.core.smartcard.ApduException; +import com.yubico.yubikit.core.smartcard.SmartCardConnection; +import com.yubico.yubikit.core.smartcard.scp.KeyRef; +import com.yubico.yubikit.core.smartcard.scp.Scp11KeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpKid; +import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +public class TestState { + public static ScpKeyParams keyParams = null; + + public static ScpKeyParams readScpKeyParams(YubiKeyDevice device, @Nullable Byte kid) throws Throwable { + if (kid == null) { + return null; + } + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + SecurityDomainSession scp = new SecurityDomainSession(connection); + KeyRef keyRef = getKeyRef(scp, kid); + List certs = scp.getCertificateBundle(keyRef); + + return certs.isEmpty() + ? null + : kid == ScpKid.SCP03 + ? null // TODO implement SCP03 support + : new Scp11KeyParams(keyRef, certs.get(certs.size() - 1).getPublicKey()); + } + } + + private static KeyRef getKeyRef(SecurityDomainSession scp, byte kid) throws ApduException, IOException, BadResponseException { + Map> keyInformation = scp.getKeyInformation(); + KeyRef keyRef = null; + for (KeyRef info : keyInformation.keySet()) { + if (info.getKid() == kid) { + keyRef = info; + break; + } + } + + if (keyRef == null) { + throw new IllegalStateException("Failed to find required key"); + } + return keyRef; + } +} 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 77696046..c44763c1 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,7 @@ package com.yubico.yubikit.testing.piv; import static com.yubico.yubikit.piv.PivSession.FEATURE_RSA3072_RSA4096; -import static com.yubico.yubikit.testing.piv.PivTestConstants.DEFAULT_MANAGEMENT_KEY; +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; 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 f897f213..bdd1ea25 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,16 +16,26 @@ package com.yubico.yubikit.testing.piv; import static com.yubico.yubikit.piv.PivSession.FEATURE_AES_KEY; -import static com.yubico.yubikit.testing.piv.PivTestConstants.DEFAULT_MANAGEMENT_KEY; -import static com.yubico.yubikit.testing.piv.PivTestConstants.DEFAULT_PIN; -import static com.yubico.yubikit.testing.piv.PivTestConstants.DEFAULT_PUK; - +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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.core.smartcard.SW; +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.piv.InvalidPinException; import com.yubico.yubikit.piv.ManagementKeyType; import com.yubico.yubikit.piv.PivSession; +import com.yubico.yubikit.testing.TestState; import org.bouncycastle.util.encoders.Hex; import org.hamcrest.CoreMatchers; @@ -37,10 +47,20 @@ import java.io.IOException; +import javax.annotation.Nullable; + public class PivDeviceTests { private static final Logger logger = LoggerFactory.getLogger(PivDeviceTests.class); + 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 static void testManagementKey(PivSession piv) throws BadResponseException, IOException, ApduException { byte[] key2 = Hex.decode("010203040102030401020304010203040102030401020304"); @@ -99,7 +119,7 @@ public static void testPin(PivSession piv) throws ApduException, InvalidPinExcep piv.authenticate(DEFAULT_MANAGEMENT_KEY); logger.debug("Verify PIN"); - char[] pin2 = "123123".toCharArray(); + char[] pin2 = "11231123".toCharArray(); piv.verifyPin(DEFAULT_PIN); MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(3)); @@ -186,4 +206,57 @@ public static void testPuk(PivSession piv) throws ApduException, InvalidPinExcep // Change PUK piv.changePuk(puk2, DEFAULT_PUK); } + + // + public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) throws Throwable { + + PivTestState.DEFAULT_PIN = PivTestConstants.DEFAULT_PIN; + PivTestState.DEFAULT_PUK = PivTestConstants.DEFAULT_PUK; + PivTestState.DEFAULT_MANAGEMENT_KEY = PivTestConstants.DEFAULT_MANAGEMENT_KEY; + + boolean isPivFipsCapable; + + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + ManagementSession managementSession = new ManagementSession(connection); + DeviceInfo deviceInfo = managementSession.getDeviceInfo(); + assertNotNull(deviceInfo); + + isPivFipsCapable = (deviceInfo.getFipsCapable() & Capability.PIV.bit) == Capability.PIV.bit; + + if (kid != null) { + assumeTrue("Device is not PIV FIPS capable", isPivFipsCapable); + } else { + assumeFalse("Device is PIV FIPS capable but no SCP version provided", isPivFipsCapable); + } + } + + // don't read SCP params on non capable devices + TestState.keyParams = isPivFipsCapable ? TestState.readScpKeyParams(device, kid) : null; + + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + PivSession pivSession = new PivSession(connection, TestState.keyParams); + pivSession.reset(); + if (kid == null) { + // as this is not a FIPS capable device, don't change any pins and + // don't require FIPS approved + return; + } + + 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; + + ManagementSession managementSession = new ManagementSession(connection); + DeviceInfo deviceInfo = managementSession.getDeviceInfo(); + + assertNotNull(deviceInfo); + assertEquals("Device not PIV FIPS approved", (deviceInfo.getFipsApproved() & Capability.PIV.bit), Capability.PIV.bit); + + } + } } 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 new file mode 100644 index 00000000..e70f6eb1 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestState.java @@ -0,0 +1,23 @@ +/* + * 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; + +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; +} From 234edbf373230a5a59ac4eb55d43184656434af2 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 8 Jul 2024 09:10:32 +0200 Subject: [PATCH 08/46] improve PIV device tests based on HW type --- .../yubikit/testing/piv/PivDeviceTests.java | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) 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 bdd1ea25..ff8b63a0 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 @@ -19,11 +19,11 @@ 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 org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assume.assumeFalse; +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.BadResponseException; import com.yubico.yubikit.core.smartcard.ApduException; @@ -207,7 +207,6 @@ public static void testPuk(PivSession piv) throws ApduException, InvalidPinExcep piv.changePuk(puk2, DEFAULT_PUK); } - // public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) throws Throwable { PivTestState.DEFAULT_PIN = PivTestConstants.DEFAULT_PIN; @@ -215,6 +214,7 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro PivTestState.DEFAULT_MANAGEMENT_KEY = PivTestConstants.DEFAULT_MANAGEMENT_KEY; boolean isPivFipsCapable; + boolean hasPinComplexity; try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { ManagementSession managementSession = new ManagementSession(connection); @@ -222,41 +222,53 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro assertNotNull(deviceInfo); isPivFipsCapable = (deviceInfo.getFipsCapable() & Capability.PIV.bit) == Capability.PIV.bit; + hasPinComplexity = deviceInfo.getPinComplexity(); + } - if (kid != null) { - assumeTrue("Device is not PIV FIPS capable", isPivFipsCapable); - } else { - assumeFalse("Device is PIV FIPS capable but no SCP version provided", isPivFipsCapable); - } + if (kid != null) { + assumeTrue("Device is not PIV FIPS capable", isPivFipsCapable); + } else if (isPivFipsCapable) { + Assume.assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", + device.getTransport() != Transport.NFC); } // don't read SCP params on non capable devices - TestState.keyParams = isPivFipsCapable ? TestState.readScpKeyParams(device, kid) : null; + TestState.keyParams = (isPivFipsCapable && kid != null) + ? TestState.readScpKeyParams(device, kid) + : null; try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { PivSession pivSession = new PivSession(connection, TestState.keyParams); pivSession.reset(); - if (kid == null) { - // as this is not a FIPS capable device, don't change any pins and - // don't require FIPS approved - return; - } - 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); + 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; + PivTestState.DEFAULT_PIN = COMPLEX_PIN; + PivTestState.DEFAULT_PUK = COMPLEX_PUK; + PivTestState.DEFAULT_MANAGEMENT_KEY = COMPLEX_MANAGEMENT_KEY; + } + + if (kid == null && device.getTransport() == Transport.USB) { + // this might be a FIPS capable (and now approved) device, + // but the test client did not provide any kid, so the session is not + // in SCP mode and we don't require this test to run in SCP for USB transports. + // FIPS devices have to use SCP over NFC transport + return; + } ManagementSession managementSession = new ManagementSession(connection); DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - assertNotNull(deviceInfo); - assertEquals("Device not PIV FIPS approved", (deviceInfo.getFipsApproved() & Capability.PIV.bit), Capability.PIV.bit); + final boolean fipsApproved = (deviceInfo.getFipsApproved() & Capability.PIV.bit) == Capability.PIV.bit; + assertNotNull(deviceInfo); + assertTrue("Device not PIV FIPS approved", fipsApproved); } } } From 53c1374e79b0ba911040dc7956ca1b97723283c0 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 8 Jul 2024 13:05:10 +0200 Subject: [PATCH 09/46] add FIPS PIV JCA provider tests --- .../testing/piv/PivFipsJcaProviderTests.java | 29 +++++++++++++++++++ .../yubikit/testing/piv/PivFipsTests.java | 1 - .../testing/piv/PivJcaProviderTests.java | 10 +++++++ .../yubikit/testing/piv/PivDeviceTests.java | 19 +++++------- .../testing/piv/PivJcaDecryptTests.java | 8 +++-- .../testing/piv/PivJcaDeviceTests.java | 21 ++++++++++++-- .../testing/piv/PivJcaSigningTests.java | 8 +++-- .../yubikit/testing/piv/PivMoveKeyTests.java | 8 +++-- .../yubikit/testing/piv/PivTestState.java | 7 +++++ 9 files changed, 91 insertions(+), 20 deletions(-) create mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsJcaProviderTests.java diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsJcaProviderTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsJcaProviderTests.java new file mode 100644 index 00000000..711b6cd1 --- /dev/null +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsJcaProviderTests.java @@ -0,0 +1,29 @@ +/* + * 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 com.yubico.yubikit.core.smartcard.scp.ScpKid; + +import org.junit.Before; + +public class PivFipsJcaProviderTests extends PivJcaProviderTests { + @Before + public void setup() throws Throwable { + kid = ScpKid.SCP11b; + super.setup(); + } +} diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java index 2a39aa7f..eb51b9b7 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java @@ -20,7 +20,6 @@ import org.junit.Before; - public class PivFipsTests extends PivTests { @Before diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java index f12b1233..e27bd467 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java @@ -20,12 +20,22 @@ import com.yubico.yubikit.testing.framework.PivInstrumentedTests; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import javax.annotation.Nullable; + @RunWith(AndroidJUnit4.class) public class PivJcaProviderTests extends PivInstrumentedTests { + @Nullable protected Byte kid; + + @Before + public void setup() throws Throwable { + withDevice(device -> PivDeviceTests.verifyAndSetup(device, kid)); + } + @Test public void testGenerateKeys() throws Throwable { withPivSession(PivJcaDeviceTests::testGenerateKeys); 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 ff8b63a0..1c746182 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 @@ -19,6 +19,7 @@ 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; @@ -254,21 +255,17 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro PivTestState.DEFAULT_MANAGEMENT_KEY = COMPLEX_MANAGEMENT_KEY; } - if (kid == null && device.getTransport() == Transport.USB) { - // this might be a FIPS capable (and now approved) device, - // but the test client did not provide any kid, so the session is not - // in SCP mode and we don't require this test to run in SCP for USB transports. - // FIPS devices have to use SCP over NFC transport - return; - } - ManagementSession managementSession = new ManagementSession(connection); DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - final boolean fipsApproved = (deviceInfo.getFipsApproved() & Capability.PIV.bit) == Capability.PIV.bit; + FIPS_APPROVED = (deviceInfo.getFipsApproved() & Capability.PIV.bit) == Capability.PIV.bit; - assertNotNull(deviceInfo); - assertTrue("Device not PIV FIPS approved", fipsApproved); + // 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); + } } } } 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 4fc32b90..d10d3563 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,8 @@ 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.PivTestConstants.DEFAULT_MANAGEMENT_KEY; -import static com.yubico.yubikit.testing.piv.PivTestConstants.DEFAULT_PIN; +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; @@ -73,6 +73,10 @@ public static void testDecrypt(PivSession piv, KeyType keyType) throws BadRespon throw new IllegalArgumentException("Unsupported"); } + if (PivTestState.isInvalidKeyType(keyType)) { + return; + } + piv.authenticate(DEFAULT_MANAGEMENT_KEY); logger.debug("Generate key: {}", keyType); KeyPairGenerator kpg = KeyPairGenerator.getInstance("YKPivRSA"); 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 af232adb..75160052 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,8 @@ 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.PivTestConstants.DEFAULT_MANAGEMENT_KEY; -import static com.yubico.yubikit.testing.piv.PivTestConstants.DEFAULT_PIN; +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; @@ -57,6 +57,10 @@ public static void testImportKeys(PivSession piv) throws Exception { for (KeyType keyType : Arrays.asList(KeyType.RSA1024, KeyType.RSA2048, KeyType.RSA3072, KeyType.RSA4096)) { + if (PivTestState.isInvalidKeyType(keyType)) { + continue; + } + if (!piv.supports(FEATURE_RSA3072_RSA4096) && (keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096)) { continue; } @@ -95,6 +99,11 @@ 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)) { + continue; + } + String alias = Slot.SIGNATURE.getStringAlias(); KeyPair keyPair = PivTestUtils.loadKey(keyType); @@ -145,6 +154,10 @@ private static void generateKeys(PivSession piv) throws Exception { KeyPairGenerator ecGen = KeyPairGenerator.getInstance("YKPivEC"); for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384, KeyType.ED25519, KeyType.X25519)) { + if (PivTestState.isInvalidKeyType(keyType)) { + continue; + } + if (!piv.supports(FEATURE_CV25519) && (keyType == KeyType.ED25519 || keyType == KeyType.X25519)) { continue; } @@ -172,6 +185,10 @@ 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)) { + continue; + } + if (!piv.supports(FEATURE_RSA3072_RSA4096) && (keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096)) { continue; } 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 9ed9ec9b..d09c4270 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,8 @@ 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.PivTestConstants.DEFAULT_MANAGEMENT_KEY; -import static com.yubico.yubikit.testing.piv.PivTestConstants.DEFAULT_PIN; +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; @@ -76,6 +76,10 @@ public static void testSign(PivSession piv, KeyType keyType) throws NoSuchAlgori return; } + if (PivTestState.isInvalidKeyType(keyType)) { + return; + } + if (keyType == KeyType.X25519) { logger.debug("Ignoring keyType: {}", keyType); return; 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 3b84117c..763a2e2b 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,8 @@ 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.PivTestConstants.DEFAULT_MANAGEMENT_KEY; -import static com.yubico.yubikit.testing.piv.PivTestConstants.DEFAULT_PIN; +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; @@ -65,6 +65,10 @@ static void moveKey(PivSession piv) for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384, KeyType.RSA1024, KeyType.RSA2048, KeyType.ED25519, KeyType.X25519)) { + if (PivTestState.isInvalidKeyType(keyType)) { + continue; + } + if (!piv.supports(FEATURE_CV25519) && (keyType == KeyType.ED25519 || keyType == KeyType.X25519)) { continue; } 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 e70f6eb1..7517670b 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,8 +16,15 @@ package com.yubico.yubikit.testing.piv; +import com.yubico.yubikit.piv.KeyType; + 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; + + static boolean isInvalidKeyType(KeyType keyType) { + return FIPS_APPROVED && (keyType == KeyType.RSA1024 || keyType == KeyType.X25519); + } } From 40a2f5ea7169e2fa70e173561525c975c7033b70 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 8 Jul 2024 15:28:39 +0200 Subject: [PATCH 10/46] refactor FIPS PIV integration tests --- .../testing/piv/PivFipsJcaProviderTests.java | 29 ------ .../yubikit/testing/piv/PivFipsTests.java | 30 ------ .../testing/piv/PivJcaProviderTests.java | 66 ++++++------- .../yubico/yubikit/testing/piv/PivTests.java | 73 ++++++++------- .../framework/PivInstrumentedTests.java | 4 + .../framework/YKInstrumentedTests.java | 7 ++ .../com/yubico/yubikit/testing/TestState.java | 10 +- .../yubikit/testing/piv/PivDeviceTests.java | 71 -------------- .../yubikit/testing/piv/PivTestUtils.java | 93 +++++++++++++++++++ 9 files changed, 185 insertions(+), 198 deletions(-) delete mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsJcaProviderTests.java delete mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsJcaProviderTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsJcaProviderTests.java deleted file mode 100644 index 711b6cd1..00000000 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsJcaProviderTests.java +++ /dev/null @@ -1,29 +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.piv; - -import com.yubico.yubikit.core.smartcard.scp.ScpKid; - -import org.junit.Before; - -public class PivFipsJcaProviderTests extends PivJcaProviderTests { - @Before - public void setup() throws Throwable { - kid = ScpKid.SCP11b; - super.setup(); - } -} diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java deleted file mode 100644 index eb51b9b7..00000000 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivFipsTests.java +++ /dev/null @@ -1,30 +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.piv; - -import com.yubico.yubikit.core.smartcard.scp.ScpKid; - -import org.junit.Before; - -public class PivFipsTests extends PivTests { - - @Before - public void setup() throws Throwable { - kid = ScpKid.SCP11b; - super.setup(); - } -} diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java index e27bd467..20e75d35 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java @@ -16,53 +16,55 @@ package com.yubico.yubikit.testing.piv; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.yubico.yubikit.core.smartcard.scp.ScpKid; import com.yubico.yubikit.testing.framework.PivInstrumentedTests; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import javax.annotation.Nullable; - @RunWith(AndroidJUnit4.class) -public class PivJcaProviderTests extends PivInstrumentedTests { +public class PivJcaProviderTests { - @Nullable protected Byte kid; + public static class NoScpTests extends PivInstrumentedTests { + @Test + public void testGenerateKeys() throws Throwable { + withPivSession(PivJcaDeviceTests::testGenerateKeys); + } - @Before - public void setup() throws Throwable { - withDevice(device -> PivDeviceTests.verifyAndSetup(device, kid)); - } + @Test + public void testGenerateKeysPreferBC() throws Throwable { + withPivSession(PivJcaDeviceTests::testGenerateKeysPreferBC); + } - @Test - public void testGenerateKeys() throws Throwable { - withPivSession(PivJcaDeviceTests::testGenerateKeys); - } + @Test + public void testImportKeys() throws Throwable { + withPivSession(PivJcaDeviceTests::testImportKeys); + } - @Test - public void testGenerateKeysPreferBC() throws Throwable { - withPivSession(PivJcaDeviceTests::testGenerateKeysPreferBC); - } + @Test + public void testSigning() throws Throwable { + withPivSession(PivJcaSigningTests::testSign); + } - @Test - public void testImportKeys() throws Throwable { - withPivSession(PivJcaDeviceTests::testImportKeys); - } - - @Test - public void testSigning() throws Throwable { - withPivSession(PivJcaSigningTests::testSign); - } + @Test + public void testDecrypt() throws Throwable { + withPivSession(PivJcaDecryptTests::testDecrypt); + } - @Test - public void testDecrypt() throws Throwable { - withPivSession(PivJcaDecryptTests::testDecrypt); + @Test + public void testMoveKey() throws Throwable { + withPivSession(PivMoveKeyTests::moveKey); + } } - @Test - public void testMoveKey() throws Throwable { - withPivSession(PivMoveKeyTests::moveKey); + 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/piv/PivTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivTests.java index f9b7d63c..f7c821ec 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 @@ -16,49 +16,60 @@ package com.yubico.yubikit.testing.piv; +import com.yubico.yubikit.core.smartcard.scp.ScpKid; import com.yubico.yubikit.testing.framework.PivInstrumentedTests; -import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; import javax.annotation.Nullable; -public class PivTests extends PivInstrumentedTests { +@RunWith(Suite.class) +@Suite.SuiteClasses({ + PivTests.NoScpTests.class, + PivTests.Scp11bTests.class, + PivJcaProviderTests.NoScpTests.class, + PivJcaProviderTests.Scp11bTests.class +}) +public class PivTests { + public static class NoScpTests extends PivInstrumentedTests { + @Test + public void testPin() throws Throwable { + withPivSession(PivDeviceTests::testPin); + } - @Nullable protected Byte kid; + @Test + public void testPuk() throws Throwable { + withPivSession(PivDeviceTests::testPuk); + } - @Before - public void setup() throws Throwable { - withDevice(device -> PivDeviceTests.verifyAndSetup(device, kid)); - } - - @Test - public void testPin() throws Throwable { - withPivSession(PivDeviceTests::testPin); - } + @Test + public void testManagementKey() throws Throwable { + withPivSession(PivDeviceTests::testManagementKey); + } - @Test - public void testPuk() throws Throwable { - withPivSession(PivDeviceTests::testPuk); - } + @Test + public void testManagementKeyType() throws Throwable { + withPivSession(PivDeviceTests::testManagementKeyType); + } - @Test - public void testManagementKey() throws Throwable { - withPivSession(PivDeviceTests::testManagementKey); - } - - @Test - public void testManagementKeyType() throws Throwable { - withPivSession(PivDeviceTests::testManagementKeyType); - } + @Test + public void testPutUncompressedCertificate() throws Throwable { + withPivSession(PivCertificateTests::putUncompressedCertificate); + } - @Test - public void testPutUncompressedCertificate() throws Throwable { - withPivSession(PivCertificateTests::putUncompressedCertificate); + @Test + public void testPutCompressedCertificate() throws Throwable { + withPivSession(PivCertificateTests::putCompressedCertificate); + } } - @Test - public void testPutCompressedCertificate() throws Throwable { - withPivSession(PivCertificateTests::putCompressedCertificate); + public static class Scp11bTests extends NoScpTests { + @Override + @Nullable + protected Byte getScpKid() { + return ScpKid.SCP11b; + } } } 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 50dba918..a68a5579 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 @@ -19,6 +19,7 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.piv.PivSession; import com.yubico.yubikit.testing.TestState; +import com.yubico.yubikit.testing.piv.PivTestUtils; public class PivInstrumentedTests extends YKInstrumentedTests { @@ -28,6 +29,9 @@ public interface Callback { } protected void withPivSession(Callback callback) throws Throwable { + + PivTestUtils.verifyAndSetup(device, getScpKid()); + try (SmartCardConnection c = device.openConnection(SmartCardConnection.class)) { PivSession pivSession = new PivSession(c, TestState.keyParams); callback.invoke(pivSession); 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 89c1f15e..6e38d00e 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 @@ -26,6 +26,8 @@ import org.junit.Rule; import org.junit.rules.TestName; +import javax.annotation.Nullable; + public class YKInstrumentedTests { private TestActivity activity; @@ -57,5 +59,10 @@ public interface Callback { protected void withDevice(Callback callback) throws Throwable { callback.invoke(device); } + + @Nullable + protected Byte getScpKid() { + return null; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java index 3c5c31d5..8835d7c1 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -43,6 +43,9 @@ public static ScpKeyParams readScpKeyParams(YubiKeyDevice device, @Nullable Byte try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { SecurityDomainSession scp = new SecurityDomainSession(connection); KeyRef keyRef = getKeyRef(scp, kid); + if (keyRef == null) { + return null; + } List certs = scp.getCertificateBundle(keyRef); return certs.isEmpty() @@ -53,7 +56,8 @@ public static ScpKeyParams readScpKeyParams(YubiKeyDevice device, @Nullable Byte } } - private static KeyRef getKeyRef(SecurityDomainSession scp, byte kid) throws ApduException, IOException, BadResponseException { + private static KeyRef getKeyRef(SecurityDomainSession scp, byte kid) + throws ApduException, IOException, BadResponseException { Map> keyInformation = scp.getKeyInformation(); KeyRef keyRef = null; for (KeyRef info : keyInformation.keySet()) { @@ -62,10 +66,6 @@ private static KeyRef getKeyRef(SecurityDomainSession scp, byte kid) throws Apdu break; } } - - if (keyRef == null) { - throw new IllegalStateException("Failed to find required key"); - } return keyRef; } } 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 1c746182..4eda84dd 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 @@ -48,20 +48,10 @@ import java.io.IOException; -import javax.annotation.Nullable; - public class PivDeviceTests { private static final Logger logger = LoggerFactory.getLogger(PivDeviceTests.class); - 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 static void testManagementKey(PivSession piv) throws BadResponseException, IOException, ApduException { byte[] key2 = Hex.decode("010203040102030401020304010203040102030401020304"); @@ -207,65 +197,4 @@ public static void testPuk(PivSession piv) throws ApduException, InvalidPinExcep // Change PUK piv.changePuk(puk2, DEFAULT_PUK); } - - public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) throws Throwable { - - PivTestState.DEFAULT_PIN = PivTestConstants.DEFAULT_PIN; - PivTestState.DEFAULT_PUK = PivTestConstants.DEFAULT_PUK; - PivTestState.DEFAULT_MANAGEMENT_KEY = PivTestConstants.DEFAULT_MANAGEMENT_KEY; - - boolean isPivFipsCapable; - boolean hasPinComplexity; - - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - ManagementSession managementSession = new ManagementSession(connection); - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - assertNotNull(deviceInfo); - - isPivFipsCapable = (deviceInfo.getFipsCapable() & Capability.PIV.bit) == Capability.PIV.bit; - hasPinComplexity = deviceInfo.getPinComplexity(); - } - - if (kid != null) { - assumeTrue("Device is not PIV FIPS capable", isPivFipsCapable); - } else if (isPivFipsCapable) { - Assume.assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", - device.getTransport() != Transport.NFC); - } - - // don't read SCP params on non capable devices - TestState.keyParams = (isPivFipsCapable && kid != null) - ? TestState.readScpKeyParams(device, kid) - : null; - - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - PivSession pivSession = new PivSession(connection, TestState.keyParams); - pivSession.reset(); - - 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; - } - - ManagementSession managementSession = new ManagementSession(connection); - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - - FIPS_APPROVED = (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); - } - } - } } 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 9661eb26..bc842cd7 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,8 +15,24 @@ */ package com.yubico.yubikit.testing.piv; +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 com.yubico.yubikit.core.Transport; +import com.yubico.yubikit.core.YubiKeyDevice; 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.management.ManagementSession; import com.yubico.yubikit.piv.KeyType; +import com.yubico.yubikit.piv.ManagementKeyType; +import com.yubico.yubikit.piv.PivSession; +import com.yubico.yubikit.testing.TestState; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.x500.X500Name; @@ -27,6 +43,7 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.junit.Assert; +import org.junit.Assume; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,6 +71,7 @@ import java.security.spec.X509EncodedKeySpec; import java.util.Date; +import javax.annotation.Nullable; import javax.crypto.Cipher; import javax.crypto.KeyAgreement; @@ -62,6 +80,14 @@ public class PivTestUtils { private static final Logger logger = LoggerFactory.getLogger(PivTestUtils.class); + 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, + }; + private enum StaticKey { RSA1024( KeyType.RSA1024, "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBALWeZ0E5O2l_iH" + @@ -483,4 +509,71 @@ public static void x25519KeyAgreement(PrivateKey privateKey, PublicKey publicKey Assert.assertArrayEquals("Secret mismatch", secret, peerSecret); } + + public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) throws Throwable { + + PivTestState.DEFAULT_PIN = PivTestConstants.DEFAULT_PIN; + PivTestState.DEFAULT_PUK = PivTestConstants.DEFAULT_PUK; + PivTestState.DEFAULT_MANAGEMENT_KEY = PivTestConstants.DEFAULT_MANAGEMENT_KEY; + + boolean isPivFipsCapable; + boolean hasPinComplexity; + + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + ManagementSession managementSession = new ManagementSession(connection); + DeviceInfo deviceInfo = managementSession.getDeviceInfo(); + assertNotNull(deviceInfo); + + isPivFipsCapable = (deviceInfo.getFipsCapable() & Capability.PIV.bit) == Capability.PIV.bit; + hasPinComplexity = deviceInfo.getPinComplexity(); + } + + if (kid == null && isPivFipsCapable) { + Assume.assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", + device.getTransport() != Transport.NFC); + } + + // don't read SCP params on non capable devices + TestState.keyParams = (isPivFipsCapable && kid != null) + ? TestState.readScpKeyParams(device, kid) + : null; + + if (kid != null) { + // skip the test if the connected key does not provide matching SCP keys + Assume.assumeTrue( + "No matching key params found for required kid", + TestState.keyParams != null + ); + } + + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + PivSession pivSession = new PivSession(connection, TestState.keyParams); + pivSession.reset(); + + 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; + } + + ManagementSession managementSession = new ManagementSession(connection); + DeviceInfo deviceInfo = managementSession.getDeviceInfo(); + + FIPS_APPROVED = (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); + } + } + } } From e013959552b758011b39f7b9779d898fe35ccd5e Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 10 Jul 2024 12:24:01 +0200 Subject: [PATCH 11/46] Remove Destroyable interface It's not supported by the JVM in general, so no use implementing it now. --- .../yubikit/core/smartcard/ScpProcessor.java | 10 ---- .../core/smartcard/scp/Scp03KeyParams.java | 12 ----- .../core/smartcard/scp/Scp11KeyParams.java | 16 ------ .../core/smartcard/scp/ScpKeyParams.java | 4 +- .../yubikit/core/smartcard/scp/ScpState.java | 49 +++++++------------ .../core/smartcard/scp/SessionKeys.java | 23 +-------- .../core/smartcard/scp/StaticKeys.java | 25 +--------- .../com/yubico/yubikit/oath/OathSession.java | 14 ++---- 8 files changed, 25 insertions(+), 128 deletions(-) diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java index d554a2e7..25d21e0f 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java @@ -65,14 +65,4 @@ public ApduResponse sendApdu(Apdu apdu, boolean encrypt) throws IOException, Bad return new ApduResponse(ByteBuffer.allocate(respData.length + 2).put(respData).putShort(resp.getSw()).array()); } - - @Override - public void close() throws IOException { - try { - state.destroy(); - } catch (DestroyFailedException e) { - throw new IOException(e); - } - super.close(); - } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java index 5c1271e8..aafaa092 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java @@ -16,8 +16,6 @@ package com.yubico.yubikit.core.smartcard.scp; -import javax.security.auth.DestroyFailedException; - /** * SCP key parameters for performing an SCP03 authentication. * SCP03 uses a set of three keys, each with their own KID, but a shared KVN. @@ -42,14 +40,4 @@ public Scp03KeyParams(KeyRef keyRef, StaticKeys keys) { public KeyRef getKeyRef() { return keyRef; } - - @Override - public void destroy() throws DestroyFailedException { - keys.destroy(); - } - - @Override - public boolean isDestroyed() { - return keys.isDestroyed(); - } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java index ffe7ef88..d79dcee2 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java @@ -24,7 +24,6 @@ import java.util.List; import javax.annotation.Nullable; -import javax.security.auth.DestroyFailedException; /** * SCP key parameters for performing SCP11 authentication. @@ -71,19 +70,4 @@ public Scp11KeyParams(KeyRef keyRef, PublicKey pkSdEcka) { public KeyRef getKeyRef() { return keyRef; } - - @Override - public void destroy() throws DestroyFailedException { - if (skOceEcka != null) { - skOceEcka.destroy(); - } - } - - @Override - public boolean isDestroyed() { - if (skOceEcka != null) { - return skOceEcka.isDestroyed(); - } - return false; - } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java index d29f3186..16d13b82 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java @@ -16,12 +16,10 @@ package com.yubico.yubikit.core.smartcard.scp; -import javax.security.auth.Destroyable; - /** * SCP key parameters for performing an SCP authentication with a YubiKey. */ -public interface ScpKeyParams extends Destroyable { +public interface ScpKeyParams { /** * @return the identifier of the SCP key to target on the YubiKey. */ diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java index d74f5c5b..0bdf242d 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java @@ -17,6 +17,7 @@ package com.yubico.yubikit.core.smartcard.scp; import com.yubico.yubikit.core.application.BadResponseException; +import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.keys.PublicKeyValues; import com.yubico.yubikit.core.smartcard.Apdu; import com.yubico.yubikit.core.smartcard.ApduException; @@ -28,6 +29,8 @@ import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.nio.ByteBuffer; import java.security.InvalidAlgorithmParameterException; @@ -56,13 +59,14 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.security.auth.DestroyFailedException; -import javax.security.auth.Destroyable; /** * Internal SCP state class for managing SCP state, handling encryption/decryption and MAC. */ -public class ScpState implements Destroyable { +public class ScpState { + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ScpState.class); + private final SessionKeys keys; private byte[] macChain; private int encCounter = 1; @@ -324,43 +328,24 @@ public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyPara // 6 keys were derived. one for verification of receipt, 4 keys to use, and 1 which is discarded SecretKey key = keys.get(0); - try { - Mac mac = Mac.getInstance("AESCMAC"); - mac.init(key); - byte[] genReceipt = mac.doFinal(keyAgreementData); - if (!MessageDigest.isEqual(receipt, genReceipt)) { - throw new BadResponseException("Receipt does not match"); - } - return new ScpState(new SessionKeys( - keys.get(1), - keys.get(2), - keys.get(3), - keys.get(4) - ), receipt); - } finally { - try { - key.destroy(); - keys.get(5).destroy(); - } catch (DestroyFailedException e) { - // TODO: Log error - } + Mac mac = Mac.getInstance("AESCMAC"); + mac.init(key); + byte[] genReceipt = mac.doFinal(keyAgreementData); + if (!MessageDigest.isEqual(receipt, genReceipt)) { + throw new BadResponseException("Receipt does not match"); } + return new ScpState(new SessionKeys( + keys.get(1), + keys.get(2), + keys.get(3), + keys.get(4) + ), receipt); } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidAlgorithmParameterException | InvalidKeyException e) { throw new RuntimeException(e); } } - @Override - public void destroy() throws DestroyFailedException { - keys.destroy(); - } - - @Override - public boolean isDestroyed() { - return keys.isDestroyed(); - } - static byte[] cbcEncrypt(SecretKey key, byte[] data) { try { Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java index fc7f2838..2a10ad6b 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java @@ -18,42 +18,21 @@ import javax.annotation.Nullable; import javax.crypto.SecretKey; -import javax.security.auth.DestroyFailedException; -import javax.security.auth.Destroyable; /** * Session keys for SCP. DEK only needs to be provided if you need to call {@link SecurityDomainSession#putKey}. */ -public class SessionKeys implements Destroyable { +public class SessionKeys { final SecretKey senc; final SecretKey smac; final SecretKey srmac; @Nullable final SecretKey dek; - private boolean destroyed = false; - public SessionKeys(SecretKey senc, SecretKey smac, SecretKey srmac, @Nullable SecretKey dek) { this.senc = senc; this.smac = smac; this.srmac = srmac; this.dek = dek; } - - @Override - public void destroy() throws DestroyFailedException { - senc.destroy(); - smac.destroy(); - ; - srmac.destroy(); - if (dek != null) { - dek.destroy(); - } - destroyed = true; - } - - @Override - public boolean isDestroyed() { - return destroyed; - } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java index 89754d26..1bdb23ea 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java @@ -25,10 +25,8 @@ import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; -import javax.security.auth.DestroyFailedException; -import javax.security.auth.Destroyable; -public class StaticKeys implements Destroyable { +public class StaticKeys { private static final byte[] DEFAULT_KEY = new byte[]{0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f}; final SecretKey enc; @@ -36,34 +34,13 @@ public class StaticKeys implements Destroyable { @Nullable final SecretKey dek; - private boolean destroyed = false; - public StaticKeys(byte[] enc, byte[] mac, @Nullable byte[] dek) { this.enc = new SecretKeySpec(enc, "AES"); this.mac = new SecretKeySpec(mac, "AES"); this.dek = dek != null ? new SecretKeySpec(dek, "AES") : null; } - @Override - public void destroy() throws DestroyFailedException { - enc.destroy(); - mac.destroy(); - if (dek != null) { - dek.destroy(); - } - destroyed = true; - } - - @Override - public boolean isDestroyed() { - return destroyed; - } - public SessionKeys derive(byte[] context) { - if (destroyed) { - throw new SecurityException("StaticKeys has been destroyed"); - } - // TODO: Implement return new SessionKeys( deriveKey(enc, (byte) 0x4, context, (short) 0x80), deriveKey(mac, (byte) 0x6, context, (short) 0x80), diff --git a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java index c2053e91..29fdd8af 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java @@ -140,7 +140,7 @@ public OathSession(SmartCardConnection connection) throws IOException, Applicati /** * Establishes a new session with a YubiKeys OATH application. * - * @param connection to the YubiKey + * @param connection to the YubiKey * @param scpKeyParams SCP key parameters to establish a secure connection * @throws IOException in case of connection error * @throws ApplicationNotAvailableException if the application is missing or disabled @@ -201,14 +201,10 @@ public void reset() throws IOException, ApduException { challenge = null; isAccessKeySet = false; if (scpKeyParams != null) { - if (!scpKeyParams.isDestroyed()) { - try { - protocol.initScp(scpKeyParams); - } catch (BadResponseException e) { - throw new IOException("Failed setting up SCP session", e); - } - } else { - Logger.warn(logger, "SCP session cannot be re-initialized, ScpKeyParams have been destroyed"); + try { + protocol.initScp(scpKeyParams); + } catch (BadResponseException e) { + throw new IOException("Failed setting up SCP session", e); } } Logger.info(logger, "OATH application data reset performed"); From c7a782b196c3860a8409120d7c018db0e73f830f Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 10 Jul 2024 17:31:26 +0200 Subject: [PATCH 12/46] Don't include Le in MAC calculation --- .../java/com/yubico/yubikit/core/smartcard/ScpProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java index 25d21e0f..5337f7dd 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java @@ -48,7 +48,7 @@ public ApduResponse sendApdu(Apdu apdu, boolean encrypt) throws IOException, Bad // Calculate and add MAC to data byte[] macedData = new byte[data.length + 8]; System.arraycopy(data, 0, macedData, 0, data.length); - byte[] apduData = processor.formatApdu(cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, 0, macedData.length, apdu.getLe()); + byte[] apduData = processor.formatApdu(cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, 0, macedData.length, 0); byte[] mac = state.mac(Arrays.copyOf(apduData, apduData.length - 8)); System.arraycopy(mac, 0, macedData, macedData.length - 8, 8); From ac73b3d36b604c11fd83883ac429819e206abb53 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 9 Jul 2024 15:10:26 +0200 Subject: [PATCH 13/46] add OpenPgp FIPS integration tests --- .../yubico/yubikit/management/DeviceInfo.java | 2 +- .../yubikit/openpgp/OpenPgpSession.java | 5 +- .../yubikit/testing/openpgp/OpenPgpTests.java | 222 ++++++++++-------- .../framework/OpenPgpInstrumentedTests.java | 11 +- .../testing/openpgp/OpenPgpDeviceTests.java | 78 ++++-- .../testing/openpgp/OpenPgpTestState.java | 25 ++ .../testing/openpgp/OpenPgpTestUtils.java | 104 ++++++++ 7 files changed, 314 insertions(+), 133 deletions(-) create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestState.java create mode 100755 testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java diff --git a/management/src/main/java/com/yubico/yubikit/management/DeviceInfo.java b/management/src/main/java/com/yubico/yubikit/management/DeviceInfo.java index 3563f6fa..2fef83a6 100755 --- a/management/src/main/java/com/yubico/yubikit/management/DeviceInfo.java +++ b/management/src/main/java/com/yubico/yubikit/management/DeviceInfo.java @@ -269,7 +269,7 @@ static DeviceInfo parseTlvs(Map data, Version defaultVersion) { ? Version.fromBytes(data.get(TAG_FIRMWARE_VERSION)) : defaultVersion; - final Version versionZero = new Version(0,0,0); + final Version versionZero = new Version(0, 0, 0); Version fpsVersion = null; if (data.containsKey(TAG_FPS_VERSION)) { diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java index 64f9bca8..210a5145 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java @@ -30,7 +30,6 @@ import com.yubico.yubikit.core.keys.PublicKeyValues; import com.yubico.yubikit.core.smartcard.Apdu; import com.yubico.yubikit.core.smartcard.ApduException; -import com.yubico.yubikit.core.smartcard.ApduFormat; import com.yubico.yubikit.core.smartcard.AppId; import com.yubico.yubikit.core.smartcard.SW; import com.yubico.yubikit.core.smartcard.SmartCardConnection; @@ -198,6 +197,7 @@ public OpenPgpSession(SmartCardConnection connection, @Nullable ScpKeyParams scp versionBytes[i] = decodeBcd(versionBcd[i]); } version = Version.fromBytes(versionBytes); + // version = new Version (5,7, 2); protocol.configure(version); // Note: This value is cached! @@ -824,7 +824,8 @@ public void setAlgorithmAttributes(KeyRef keyRef, AlgorithmAttributes attributes if (!supported.containsKey(keyRef)) { throw new UnsupportedOperationException("Key slot not supported"); } - if (!supported.get(keyRef).contains(attributes)) { + List supportedAttributes = supported.get(keyRef); + if (!supportedAttributes.contains(attributes)) { throw new UnsupportedOperationException("Algorithm attributes not supported: " + attributes); } 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 49525052..4e2b748c 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Yubico. + * Copyright (C) 2023-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,108 +16,128 @@ package com.yubico.yubikit.testing.openpgp; +import androidx.annotation.Nullable; + +import com.yubico.yubikit.core.smartcard.scp.ScpKid; import com.yubico.yubikit.testing.framework.OpenPgpInstrumentedTests; import org.junit.Test; - -public class OpenPgpTests extends OpenPgpInstrumentedTests { - @Test - public void testImportRsaKeys() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testImportRsaKeys); - } - - @Test - public void testImportEcDsaKeys() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testImportEcDsaKeys); - } - - @Test - public void testImportEd25519Keys() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testImportEd25519); - } - - @Test - public void testImportX25519Keys() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testImportX25519); - } - - @Test - public void testGenerateRequiresAdmin() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testGenerateRequiresAdmin); - } - - @Test - public void testChangePin() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testChangePin); - } - - @Test - public void testResetPin() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testResetPin); - } - - @Test - public void testSetPinAttempts() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testSetPinAttempts); - } - - @Test - public void testGenerateRsaKeys() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testGenerateRsaKeys); - } - - @Test - public void testGenerateEcKeys() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testGenerateEcKeys); - } - - @Test - public void testGenerateEd25519() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testGenerateEd25519); - } - - @Test - public void testGenerateX25519() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testGenerateX25519); - } - - @Test - public void testAttestation() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testAttestation); - } - - @Test - public void testSigPinPolicy() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testSigPinPolicy); - } - - @Test - public void testKdf() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testKdf); - } - - @Test - public void testUnverifyPin() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testUnverifyPin); - } - - @Test - public void testDeleteKey() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testDeleteKey); - } - - @Test - public void testCertificateManagement() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testCertificateManagement); - } - - @Test - public void testGetChallenge() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testGetChallenge); - } - - @Test - public void testSetUif() throws Throwable { - withOpenPgpSession(OpenPgpDeviceTests::testSetUif); +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + OpenPgpTests.NoScpTests.class, + OpenPgpTests.Scp11bTests.class, +}) +public class OpenPgpTests { + public static class NoScpTests extends OpenPgpInstrumentedTests { + @Test + public void testImportRsaKeys() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testImportRsaKeys); + } + + @Test + public void testImportEcDsaKeys() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testImportEcDsaKeys); + } + + @Test + public void testImportEd25519Keys() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testImportEd25519); + } + + @Test + public void testImportX25519Keys() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testImportX25519); + } + + @Test + public void testGenerateRequiresAdmin() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testGenerateRequiresAdmin); + } + + @Test + public void testChangePin() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testChangePin); + } + + @Test + public void testResetPin() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testResetPin); + } + + @Test + public void testSetPinAttempts() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testSetPinAttempts); + } + + @Test + public void testGenerateRsaKeys() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testGenerateRsaKeys); + } + + @Test + public void testGenerateEcKeys() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testGenerateEcKeys); + } + + @Test + public void testGenerateEd25519() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testGenerateEd25519); + } + + @Test + public void testGenerateX25519() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testGenerateX25519); + } + + @Test + public void testAttestation() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testAttestation); + } + + @Test + public void testSigPinPolicy() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testSigPinPolicy); + } + + @Test + public void testKdf() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testKdf); + } + + @Test + public void testUnverifyPin() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testUnverifyPin); + } + + @Test + public void testDeleteKey() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testDeleteKey); + } + + @Test + public void testCertificateManagement() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testCertificateManagement); + } + + @Test + public void testGetChallenge() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testGetChallenge); + } + + @Test + public void testSetUif() throws Throwable { + withOpenPgpSession(OpenPgpDeviceTests::testSetUif); + } + } + + public static class Scp11bTests extends NoScpTests { + @Nullable + @Override + protected Byte getScpKid() { + return ScpKid.SCP11b; + } } } 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 608fd5a5..5298dccf 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 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,6 +18,8 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.openpgp.OpenPgpSession; +import com.yubico.yubikit.testing.TestState; +import com.yubico.yubikit.testing.openpgp.OpenPgpTestUtils; public class OpenPgpInstrumentedTests extends YKInstrumentedTests { @@ -26,8 +28,11 @@ public interface Callback { } protected void withOpenPgpSession(Callback callback) throws Throwable { - try(SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - callback.invoke(new OpenPgpSession(connection)); + + OpenPgpTestUtils.verifyAndSetup(device, getScpKid()); + + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + callback.invoke(new OpenPgpSession(connection, TestState.keyParams)); } } } \ No newline at end of file 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 677e3564..1c70cad4 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,6 +16,9 @@ package com.yubico.yubikit.testing.openpgp; +import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.DEFAULT_ADMIN; +import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.DEFAULT_PIN; + import com.yubico.yubikit.core.application.InvalidPinException; import com.yubico.yubikit.core.keys.PrivateKeyValues; import com.yubico.yubikit.core.keys.PublicKeyValues; @@ -67,8 +70,6 @@ import javax.crypto.KeyAgreement; public class OpenPgpDeviceTests { - private static final char[] DEFAULT_PIN = Pw.DEFAULT_USER_PIN; - private static final char[] DEFAULT_ADMIN = Pw.DEFAULT_ADMIN_PIN; private static final char[] CHANGED_PIN = "12341234".toCharArray(); private static final char[] RESET_CODE = "43214321".toCharArray(); private static final Logger logger = LoggerFactory.getLogger(OpenPgpDeviceTests.class); @@ -82,12 +83,22 @@ private static int[] getSupportedRsaKeySizes(OpenPgpSession openpgp) { } public static void testGenerateRequiresAdmin(OpenPgpSession openpgp) throws Exception { + try { - openpgp.generateRsaKey(KeyRef.DEC, 2048); + openpgp.generateEcKey(KeyRef.DEC, OpenPgpCurve.BrainpoolP256R1); Assert.fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } + + if (!OpenPgpTestState.FIPS_APPROVED) { + try { + openpgp.generateRsaKey(KeyRef.DEC, 2048); + Assert.fail(); + } catch (ApduException e) { + Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); + } + } } public static void testChangePin(OpenPgpSession openpgp) throws Exception { @@ -176,14 +187,16 @@ public static void testGenerateRsaKeys(OpenPgpSession openpgp) throws Exception verifier.update(message); assert verifier.verify(signature); - 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); + if (!OpenPgpTestState.FIPS_APPROVED) { + 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(DEFAULT_PIN, true); - byte[] decrypted = openpgp.decrypt(cipherText); - Assert.assertArrayEquals(message, decrypted); + openpgp.verifyUserPin(DEFAULT_PIN, true); + byte[] decrypted = openpgp.decrypt(cipherText); + Assert.assertArrayEquals(message, decrypted); + } } } @@ -196,6 +209,11 @@ public static void testGenerateEcKeys(OpenPgpSession openpgp) throws Exception { byte[] message = "hello".getBytes(StandardCharsets.UTF_8); for (OpenPgpCurve curve : ecdsaCurves) { + if (OpenPgpTestState.FIPS_APPROVED) { + if (curve == OpenPgpCurve.SECP256K1) { + continue; + } + } logger.info("Curve: {}", curve); PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, curve).toPublicKey(); openpgp.verifyUserPin(DEFAULT_PIN, false); @@ -242,6 +260,8 @@ public static void testGenerateEd25519(OpenPgpSession openpgp) throws Exception public static void testGenerateX25519(OpenPgpSession openpgp) throws Exception { Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", OpenPgpTestState.FIPS_APPROVED); + Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); openpgp.verifyAdminPin(DEFAULT_ADMIN); @@ -282,14 +302,17 @@ public static void testImportRsaKeys(OpenPgpSession openpgp) throws Exception { verifier.update(message); assert verifier.verify(signature); - 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); + if (!OpenPgpTestState.FIPS_APPROVED) { + 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(DEFAULT_PIN, true); - byte[] decrypted = openpgp.decrypt(cipherText); - Assert.assertArrayEquals(message, decrypted); + + openpgp.verifyUserPin(DEFAULT_PIN, true); + byte[] decrypted = openpgp.decrypt(cipherText); + Assert.assertArrayEquals(message, decrypted); + } } } @@ -306,6 +329,9 @@ public static void testImportEcDsaKeys(OpenPgpSession openpgp) throws Exception curves.remove(OpenPgpCurve.Ed25519); curves.remove(OpenPgpCurve.X25519); + if (OpenPgpTestState.FIPS_APPROVED) { + curves.remove(OpenPgpCurve.SECP256K1); + } byte[] message = "hello".getBytes(StandardCharsets.UTF_8); for (OpenPgpCurve curve : curves) { @@ -365,6 +391,7 @@ public static void testImportEd25519(OpenPgpSession openpgp) throws Exception { public static void testImportX25519(OpenPgpSession openpgp) throws Exception { Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", OpenPgpTestState.FIPS_APPROVED); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); @@ -449,9 +476,6 @@ public static void testKdf(OpenPgpSession openpgp) throws Exception { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - // KDF can only be set in a mostly clean state - openpgp.reset(); - // Set a non-default PINs to ensure that they reset openpgp.changeUserPin(DEFAULT_PIN, CHANGED_PIN); openpgp.changeAdminPin(DEFAULT_ADMIN, CHANGED_PIN); @@ -460,17 +484,19 @@ public static void testKdf(OpenPgpSession openpgp) throws Exception { openpgp.setKdf( Kdf.IterSaltedS2k.create(Kdf.IterSaltedS2k.HashAlgorithm.SHA256, 0x780000) ); - openpgp.verifyUserPin(DEFAULT_PIN, false); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + // this reset the device to defaults - it is not FIPS approved anymore and + // default PINs have to be used + openpgp.verifyUserPin(Pw.DEFAULT_USER_PIN, false); + openpgp.verifyAdminPin(Pw.DEFAULT_ADMIN_PIN); - openpgp.changeUserPin(DEFAULT_PIN, CHANGED_PIN); + openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, CHANGED_PIN); openpgp.verifyUserPin(CHANGED_PIN, false); - openpgp.changeAdminPin(DEFAULT_ADMIN, CHANGED_PIN); + openpgp.changeAdminPin(Pw.DEFAULT_ADMIN_PIN, CHANGED_PIN); openpgp.verifyAdminPin(CHANGED_PIN); openpgp.setKdf(new Kdf.None()); - openpgp.verifyAdminPin(DEFAULT_ADMIN); - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.verifyAdminPin(Pw.DEFAULT_ADMIN_PIN); + openpgp.verifyUserPin(Pw.DEFAULT_USER_PIN, false); } public static void testUnverifyPin(OpenPgpSession openpgp) throws Exception { 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 new file mode 100644 index 00000000..a14a74fa --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestState.java @@ -0,0 +1,25 @@ +/* + * 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 com.yubico.yubikit.openpgp.Pw; + +class OpenPgpTestState { + static char[] DEFAULT_PIN = Pw.DEFAULT_USER_PIN; + static char[] DEFAULT_ADMIN = Pw.DEFAULT_ADMIN_PIN; + static boolean FIPS_APPROVED = false; +} 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 new file mode 100755 index 00000000..b7d0238b --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java @@ -0,0 +1,104 @@ +/* + * 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.openpgp.OpenPgpTestState.DEFAULT_ADMIN; +import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.DEFAULT_PIN; +import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.FIPS_APPROVED; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.yubico.yubikit.core.Transport; +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.openpgp.OpenPgpSession; +import com.yubico.yubikit.openpgp.Pw; +import com.yubico.yubikit.testing.TestState; + +import org.junit.Assume; + +import javax.annotation.Nullable; + +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, @Nullable Byte kid) throws Throwable { + + OpenPgpTestState.DEFAULT_PIN = Pw.DEFAULT_USER_PIN; + OpenPgpTestState.DEFAULT_ADMIN = Pw.DEFAULT_ADMIN_PIN; + + boolean isOpenPgpFipsCapable; + boolean hasPinComplexity; + + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + ManagementSession managementSession = new ManagementSession(connection); + DeviceInfo deviceInfo = managementSession.getDeviceInfo(); + assertNotNull(deviceInfo); + + isOpenPgpFipsCapable = + (deviceInfo.getFipsCapable() & Capability.OPENPGP.bit) == Capability.OPENPGP.bit; + hasPinComplexity = deviceInfo.getPinComplexity(); + } + + if (kid == null && isOpenPgpFipsCapable) { + Assume.assumeTrue("Trying to use OpenPgp FIPS capable device over NFC without SCP", + device.getTransport() != Transport.NFC); + } + + // don't read SCP params on non capable devices + TestState.keyParams = (isOpenPgpFipsCapable && kid != null) + ? TestState.readScpKeyParams(device, kid) + : null; + + if (kid != null) { + // skip the test if the connected key does not provide matching SCP keys + Assume.assumeTrue( + "No matching key params found for required kid", + TestState.keyParams != null + ); + } + + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + OpenPgpSession openPgp = new OpenPgpSession(connection, TestState.keyParams); + openPgp.reset(); + + if (hasPinComplexity) { + // only use complex pins if pin complexity is required + openPgp.changeUserPin(DEFAULT_PIN, COMPLEX_USER_PIN); + openPgp.changeAdminPin(DEFAULT_ADMIN, COMPLEX_ADMIN_PIN); + OpenPgpTestState.DEFAULT_PIN = COMPLEX_USER_PIN; + OpenPgpTestState.DEFAULT_ADMIN = COMPLEX_ADMIN_PIN; + } + + ManagementSession managementSession = new ManagementSession(connection); + DeviceInfo deviceInfo = managementSession.getDeviceInfo(); + + FIPS_APPROVED = (deviceInfo.getFipsApproved() & Capability.OPENPGP.bit) == Capability.OPENPGP.bit; + + // 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); + } + } + } +} From 94af86c8e8625e62be5d0e2467c329aaf73a02c3 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 10 Jul 2024 17:31:51 +0200 Subject: [PATCH 14/46] Fix NFC timeout for tests, add test suites --- .../yubico/yubikit/testing/DeviceTests.java | 34 +++++++++++++++++++ .../yubikit/testing/FastDeviceTests.java | 32 +++++++++++++++++ .../yubikit/testing/openpgp/OpenPgpTests.java | 3 ++ .../testing/piv/PivJcaProviderTests.java | 6 ++++ .../com/yubico/yubikit/testing/SlowTest.java | 20 +++++++++++ .../yubico/yubikit/testing/TestActivity.java | 16 +++++++-- 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/DeviceTests.java create mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java create mode 100644 testing-android/src/main/java/com/yubico/yubikit/testing/SlowTest.java 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 new file mode 100644 index 00000000..32b5aec4 --- /dev/null +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/DeviceTests.java @@ -0,0 +1,34 @@ +/* + * 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.openpgp.OpenPgpTests; +import com.yubico.yubikit.testing.piv.PivTests; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +/** + * All integration tests. + */ +@RunWith(Suite.class) +@Suite.SuiteClasses({ + PivTests.class, + OpenPgpTests.class, +}) +public class DeviceTests { +} \ No newline at end of file diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java new file mode 100644 index 00000000..ca8f9d2a --- /dev/null +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java @@ -0,0 +1,32 @@ +/* + * 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 org.junit.experimental.categories.Categories; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +/** + * All device tests excluding tests considered too slow. + */ +@RunWith(Categories.class) +@Categories.ExcludeCategory(SlowTest.class) +@Suite.SuiteClasses({ + DeviceTests.class +}) +public class FastDeviceTests { +} 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 4e2b748c..34bd4399 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 @@ -19,9 +19,11 @@ import androidx.annotation.Nullable; import com.yubico.yubikit.core.smartcard.scp.ScpKid; +import com.yubico.yubikit.testing.SlowTest; import com.yubico.yubikit.testing.framework.OpenPgpInstrumentedTests; import org.junit.Test; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -73,6 +75,7 @@ public void testSetPinAttempts() throws Throwable { } @Test + @Category(SlowTest.class) public void testGenerateRsaKeys() throws Throwable { withOpenPgpSession(OpenPgpDeviceTests::testGenerateRsaKeys); } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java index 20e75d35..57b73440 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java @@ -20,9 +20,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.yubico.yubikit.core.smartcard.scp.ScpKid; +import com.yubico.yubikit.testing.SlowTest; import com.yubico.yubikit.testing.framework.PivInstrumentedTests; import org.junit.Test; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @@ -30,11 +32,13 @@ public class PivJcaProviderTests { public static class NoScpTests extends PivInstrumentedTests { @Test + @Category(SlowTest.class) public void testGenerateKeys() throws Throwable { withPivSession(PivJcaDeviceTests::testGenerateKeys); } @Test + @Category(SlowTest.class) public void testGenerateKeysPreferBC() throws Throwable { withPivSession(PivJcaDeviceTests::testGenerateKeysPreferBC); } @@ -45,11 +49,13 @@ public void testImportKeys() throws Throwable { } @Test + @Category(SlowTest.class) public void testSigning() throws Throwable { withPivSession(PivJcaSigningTests::testSign); } @Test + @Category(SlowTest.class) public void testDecrypt() throws Throwable { withPivSession(PivJcaDecryptTests::testDecrypt); } diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/SlowTest.java b/testing-android/src/main/java/com/yubico/yubikit/testing/SlowTest.java new file mode 100644 index 00000000..2ce9ac4d --- /dev/null +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/SlowTest.java @@ -0,0 +1,20 @@ +/* + * 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; + +public interface SlowTest { +} diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.java b/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.java index 21ecb4f8..3d776502 100644 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.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. @@ -80,7 +80,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onResume() { super.onResume(); try { - yubiKitManager.startNfcDiscovery(new NfcConfiguration().timeout(15000), this, sessionQueue::add); + yubiKitManager.startNfcDiscovery(new NfcConfiguration().timeout(1000 * 60 * 5), this, sessionQueue::add); } catch (NfcNotAvailable e) { if (e.isDisabled()) { logger.error("NFC is disabled", e); @@ -139,7 +139,17 @@ public synchronized void returnSession(YubiKeyDevice device) throws InterruptedE setBusy(false); }); if (device instanceof NfcYubiKeyDevice) { - ((NfcYubiKeyDevice) device).remove(lock::release); + // TODO make waitForRemoval configurable from a test case + final boolean waitForRemoval = false; + + //noinspection ConstantValue + if (waitForRemoval) { + // this causes the app to wait for removal of NFC key from the NFC sensor + ((NfcYubiKeyDevice) device).remove(lock::release); + } else { + lock.release(); + } + lock.acquire(); } } From abaa35d6be93690ed236dc2fdc59a6eac8c8aa0c Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 10 Jul 2024 17:57:14 +0200 Subject: [PATCH 15/46] change state variable names --- .../testing/openpgp/OpenPgpDeviceTests.java | 92 +++++++++---------- .../testing/openpgp/OpenPgpTestState.java | 4 +- .../testing/openpgp/OpenPgpTestUtils.java | 16 ++-- 3 files changed, 56 insertions(+), 56 deletions(-) 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 1c70cad4..c4339819 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,8 +16,8 @@ package com.yubico.yubikit.testing.openpgp; -import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.DEFAULT_ADMIN; -import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.DEFAULT_PIN; +import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.ADMIN_PIN; +import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.USER_PIN; import com.yubico.yubikit.core.application.InvalidPinException; import com.yubico.yubikit.core.keys.PrivateKeyValues; @@ -102,14 +102,14 @@ public static void testGenerateRequiresAdmin(OpenPgpSession openpgp) throws Exce } public static void testChangePin(OpenPgpSession openpgp) throws Exception { - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); Assert.assertThrows(InvalidPinException.class, () -> openpgp.verifyUserPin(CHANGED_PIN, false)); - Assert.assertThrows(InvalidPinException.class, () -> openpgp.changeUserPin(CHANGED_PIN, DEFAULT_PIN)); + Assert.assertThrows(InvalidPinException.class, () -> openpgp.changeUserPin(CHANGED_PIN, USER_PIN)); - openpgp.changeUserPin(DEFAULT_PIN, CHANGED_PIN); + openpgp.changeUserPin(USER_PIN, CHANGED_PIN); openpgp.verifyUserPin(CHANGED_PIN, false); - openpgp.changeUserPin(CHANGED_PIN, DEFAULT_PIN); - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.changeUserPin(CHANGED_PIN, USER_PIN); + openpgp.verifyUserPin(USER_PIN, false); } public static void testResetPin(OpenPgpSession openpgp) throws Exception { @@ -125,15 +125,15 @@ public static void testResetPin(OpenPgpSession openpgp) throws Exception { assert openpgp.getPinStatus().getAttemptsUser() == 0; try { - openpgp.resetPin(DEFAULT_PIN, null); + openpgp.resetPin(USER_PIN, null); Assert.fail(); } catch (ApduException e) { Assert.assertEquals(e.getSw(), SW.SECURITY_CONDITION_NOT_SATISFIED); } // Reset PIN using Admin PIN - openpgp.verifyAdminPin(DEFAULT_ADMIN); - openpgp.resetPin(DEFAULT_PIN, null); + openpgp.verifyAdminPin(ADMIN_PIN); + openpgp.resetPin(USER_PIN, null); remaining = openpgp.getPinStatus().getAttemptsUser(); assert remaining > 0; for (int i = remaining; i > 0; i--) { @@ -148,15 +148,15 @@ public static void testResetPin(OpenPgpSession openpgp) throws Exception { // Reset PIN using Reset Code openpgp.setResetCode(RESET_CODE); - Assert.assertThrows(InvalidPinException.class, () -> openpgp.resetPin(DEFAULT_PIN, CHANGED_PIN)); - openpgp.resetPin(DEFAULT_PIN, RESET_CODE); + Assert.assertThrows(InvalidPinException.class, () -> openpgp.resetPin(USER_PIN, CHANGED_PIN)); + openpgp.resetPin(USER_PIN, RESET_CODE); assert openpgp.getPinStatus().getAttemptsUser() > 0; } public static void testSetPinAttempts(OpenPgpSession openpgp) throws Exception { Assume.assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_PIN_ATTEMPTS)); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); openpgp.setPinAttempts(6, 3, 3); assert openpgp.getPinStatus().getAttemptsUser() == 6; @@ -174,13 +174,13 @@ public static void testSetPinAttempts(OpenPgpSession openpgp) throws Exception { public static void testGenerateRsaKeys(OpenPgpSession openpgp) throws Exception { Assume.assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_RSA_GENERATION)); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); 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(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("NONEwithRSA"); verifier.initVerify(publicKey); @@ -193,7 +193,7 @@ public static void testGenerateRsaKeys(OpenPgpSession openpgp) throws Exception cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] cipherText = cipher.doFinal(message); - openpgp.verifyUserPin(DEFAULT_PIN, true); + openpgp.verifyUserPin(USER_PIN, true); byte[] decrypted = openpgp.decrypt(cipherText); Assert.assertArrayEquals(message, decrypted); } @@ -205,7 +205,7 @@ public static void testGenerateEcKeys(OpenPgpSession openpgp) throws Exception { Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); for (OpenPgpCurve curve : ecdsaCurves) { @@ -216,7 +216,7 @@ public static void testGenerateEcKeys(OpenPgpSession openpgp) throws Exception { } logger.info("Curve: {}", curve); PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, curve).toPublicKey(); - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("NONEwithECDSA"); @@ -229,7 +229,7 @@ public static void testGenerateEcKeys(OpenPgpSession openpgp) throws Exception { kpg.initialize(new ECGenParameterSpec(curve.name())); KeyPair pair = kpg.generateKeyPair(); - openpgp.verifyUserPin(DEFAULT_PIN, true); + openpgp.verifyUserPin(USER_PIN, true); byte[] actual = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair.getPublic())); KeyAgreement ka = KeyAgreement.getInstance("ECDH"); ka.init(pair.getPrivate()); @@ -244,11 +244,11 @@ public static void testGenerateEd25519(OpenPgpSession openpgp) throws Exception Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, OpenPgpCurve.Ed25519).toPublicKey(); - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("Ed25519"); @@ -264,13 +264,13 @@ public static void testGenerateX25519(OpenPgpSession openpgp) throws Exception { Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); PublicKey publicKey = openpgp.generateEcKey(KeyRef.DEC, OpenPgpCurve.X25519).toPublicKey(); KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519"); KeyPair pair = kpg.generateKeyPair(); - openpgp.verifyUserPin(DEFAULT_PIN, true); + openpgp.verifyUserPin(USER_PIN, true); byte[] actual = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair.getPublic())); KeyAgreement ka = KeyAgreement.getInstance("XDH"); ka.init(pair.getPrivate()); @@ -280,7 +280,7 @@ public static void testGenerateX25519(OpenPgpSession openpgp) throws Exception { } public static void testImportRsaKeys(OpenPgpSession openpgp) throws Exception { - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); @@ -294,7 +294,7 @@ public static void testImportRsaKeys(OpenPgpSession openpgp) throws Exception { Assert.assertArrayEquals(pair.getPublic().getEncoded(), encoded); PublicKey publicKey = openpgp.getPublicKey(KeyRef.SIG).toPublicKey(); - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("NONEwithRSA"); @@ -309,7 +309,7 @@ public static void testImportRsaKeys(OpenPgpSession openpgp) throws Exception { byte[] cipherText = cipher.doFinal(message); - openpgp.verifyUserPin(DEFAULT_PIN, true); + openpgp.verifyUserPin(USER_PIN, true); byte[] decrypted = openpgp.decrypt(cipherText); Assert.assertArrayEquals(message, decrypted); } @@ -322,7 +322,7 @@ public static void testImportEcDsaKeys(OpenPgpSession openpgp) throws Exception Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDSA"); List curves = new ArrayList<>(Arrays.asList(OpenPgpCurve.values())); @@ -342,7 +342,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(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("NONEwithECDSA"); @@ -357,7 +357,7 @@ public static void testImportEcDsaKeys(OpenPgpSession openpgp) throws Exception ka.doPhase(openpgp.getPublicKey(KeyRef.DEC).toPublicKey(), true); byte[] expected = ka.generateSecret(); - openpgp.verifyUserPin(DEFAULT_PIN, true); + openpgp.verifyUserPin(USER_PIN, true); byte[] agreement = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair2.getPublic())); Assert.assertArrayEquals(expected, agreement); @@ -370,7 +370,7 @@ public static void testImportEd25519(OpenPgpSession openpgp) throws Exception { Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519"); KeyPair pair = kpg.generateKeyPair(); @@ -378,7 +378,7 @@ public static void testImportEd25519(OpenPgpSession openpgp) throws Exception { byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); byte[] signature = openpgp.sign(message); Signature verifier = Signature.getInstance("Ed25519"); @@ -396,7 +396,7 @@ public static void testImportX25519(OpenPgpSession openpgp) throws Exception { Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519"); KeyPair pair = kpg.generateKeyPair(); @@ -409,7 +409,7 @@ public static void testImportX25519(OpenPgpSession openpgp) throws Exception { ka.doPhase(openpgp.getPublicKey(KeyRef.DEC).toPublicKey(), true); byte[] expected = ka.generateSecret(); - openpgp.verifyUserPin(DEFAULT_PIN, true); + openpgp.verifyUserPin(USER_PIN, true); byte[] agreement = openpgp.decrypt(PublicKeyValues.Ec.fromPublicKey(pair2.getPublic())); Assert.assertArrayEquals(expected, agreement); @@ -418,18 +418,18 @@ public static void testImportX25519(OpenPgpSession openpgp) throws Exception { public static void testAttestation(OpenPgpSession openpgp) throws Exception { Assume.assumeTrue("Attestation support", openpgp.supports(OpenPgpSession.FEATURE_ATTESTATION)); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP256R1).toPublicKey(); - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); X509Certificate cert = openpgp.attestKey(KeyRef.SIG); Assert.assertEquals(publicKey, cert.getPublicKey()); } public static void testSigPinPolicy(OpenPgpSession openpgp) throws Exception { - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); @@ -446,7 +446,7 @@ public static void testSigPinPolicy(OpenPgpSession openpgp) throws Exception { } openpgp.setSignaturePinPolicy(PinPolicy.ALWAYS); - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); openpgp.sign(message); Assert.assertEquals(1, openpgp.getSignatureCounter()); try { @@ -458,7 +458,7 @@ public static void testSigPinPolicy(OpenPgpSession openpgp) throws Exception { Assert.assertEquals(1, openpgp.getSignatureCounter()); openpgp.setSignaturePinPolicy(PinPolicy.ONCE); - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); openpgp.sign(message); Assert.assertEquals(2, openpgp.getSignatureCounter()); openpgp.sign(message); @@ -477,8 +477,8 @@ public static void testKdf(OpenPgpSession openpgp) throws Exception { } // Set a non-default PINs to ensure that they reset - openpgp.changeUserPin(DEFAULT_PIN, CHANGED_PIN); - openpgp.changeAdminPin(DEFAULT_ADMIN, CHANGED_PIN); + openpgp.changeUserPin(USER_PIN, CHANGED_PIN); + openpgp.changeAdminPin(ADMIN_PIN, CHANGED_PIN); openpgp.verifyAdminPin(CHANGED_PIN); openpgp.setKdf( @@ -506,7 +506,7 @@ public static void testUnverifyPin(OpenPgpSession openpgp) throws Exception { kpg.initialize(2048); KeyPair pair = kpg.generateKeyPair(); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); openpgp.setSignaturePinPolicy(PinPolicy.ONCE); @@ -519,7 +519,7 @@ public static void testUnverifyPin(OpenPgpSession openpgp) throws Exception { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); openpgp.sign(message); @@ -538,10 +538,10 @@ public static void testDeleteKey(OpenPgpSession openpgp) throws Exception { kpg.initialize(2048); KeyPair pair = kpg.generateKeyPair(); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - openpgp.verifyUserPin(DEFAULT_PIN, false); + openpgp.verifyUserPin(USER_PIN, false); byte[] message = "hello".getBytes(StandardCharsets.UTF_8); openpgp.sign(message); @@ -575,7 +575,7 @@ public static void testCertificateManagement(OpenPgpSession openpgp) throws Exce CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(stream); - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); openpgp.putCertificate(KeyRef.SIG, cert); X509Certificate actual = openpgp.getCertificate(KeyRef.SIG); @@ -613,7 +613,7 @@ public static void testSetUif(OpenPgpSession openpgp) throws Exception { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - openpgp.verifyAdminPin(DEFAULT_ADMIN); + openpgp.verifyAdminPin(ADMIN_PIN); openpgp.setUif(KeyRef.SIG, Uif.ON); Assert.assertEquals(Uif.ON, openpgp.getUif(KeyRef.SIG)); 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 a14a74fa..d45bccad 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 @@ -19,7 +19,7 @@ import com.yubico.yubikit.openpgp.Pw; class OpenPgpTestState { - static char[] DEFAULT_PIN = Pw.DEFAULT_USER_PIN; - static char[] DEFAULT_ADMIN = Pw.DEFAULT_ADMIN_PIN; + static char[] USER_PIN = Pw.DEFAULT_USER_PIN; + static char[] ADMIN_PIN = Pw.DEFAULT_ADMIN_PIN; static boolean FIPS_APPROVED = false; } 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 index b7d0238b..94a6ecdc 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java @@ -15,8 +15,8 @@ */ package com.yubico.yubikit.testing.openpgp; -import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.DEFAULT_ADMIN; -import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.DEFAULT_PIN; +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.testing.openpgp.OpenPgpTestState.FIPS_APPROVED; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -42,8 +42,8 @@ public class OpenPgpTestUtils { public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) throws Throwable { - OpenPgpTestState.DEFAULT_PIN = Pw.DEFAULT_USER_PIN; - OpenPgpTestState.DEFAULT_ADMIN = Pw.DEFAULT_ADMIN_PIN; + OpenPgpTestState.USER_PIN = Pw.DEFAULT_USER_PIN; + OpenPgpTestState.ADMIN_PIN = Pw.DEFAULT_ADMIN_PIN; boolean isOpenPgpFipsCapable; boolean hasPinComplexity; @@ -82,10 +82,10 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro if (hasPinComplexity) { // only use complex pins if pin complexity is required - openPgp.changeUserPin(DEFAULT_PIN, COMPLEX_USER_PIN); - openPgp.changeAdminPin(DEFAULT_ADMIN, COMPLEX_ADMIN_PIN); - OpenPgpTestState.DEFAULT_PIN = COMPLEX_USER_PIN; - OpenPgpTestState.DEFAULT_ADMIN = COMPLEX_ADMIN_PIN; + 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; } ManagementSession managementSession = new ManagementSession(connection); From 94fbcd6dee0f6a7f24d8a6b00fa7849afde1eff7 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 11 Jul 2024 17:18:05 +0200 Subject: [PATCH 16/46] add FIPS OATH device tests --- .../yubico/yubikit/testing/DeviceTests.java | 2 + .../yubikit/testing/oath/OathTests.java | 59 ++++++++++ .../framework/OathInstrumentedTests.java | 56 ++++++++++ .../yubikit/testing/oath/OathDeviceTests.java | 103 ++++++++++++++++++ .../yubikit/testing/oath/OathTestUtils.java | 96 ++++++++++++++++ 5 files changed, 316 insertions(+) create mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/oath/OathTests.java create mode 100644 testing-android/src/main/java/com/yubico/yubikit/testing/framework/OathInstrumentedTests.java create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/oath/OathDeviceTests.java create mode 100755 testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java 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 32b5aec4..f26e782f 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.oath.OathTests; import com.yubico.yubikit.testing.openpgp.OpenPgpTests; import com.yubico.yubikit.testing.piv.PivTests; @@ -29,6 +30,7 @@ @Suite.SuiteClasses({ PivTests.class, OpenPgpTests.class, + OathTests.class }) public class DeviceTests { } \ No newline at end of file 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 new file mode 100644 index 00000000..35791b3d --- /dev/null +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/oath/OathTests.java @@ -0,0 +1,59 @@ +/* + * 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.oath; + +import androidx.annotation.Nullable; + +import com.yubico.yubikit.core.smartcard.scp.ScpKid; +import com.yubico.yubikit.testing.framework.OathInstrumentedTests; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + OathTests.NoScpTests.class, + OathTests.Scp11bTests.class, +}) +public class OathTests { + public static class NoScpTests extends OathInstrumentedTests { + @Test + public void testChangePassword() throws Throwable { + withOathSession(OathDeviceTests::testChangePassword); + withOathSession(OathDeviceTests::testChangePasswordAfterReconnect); + } + + @Test + public void testResetPassword() throws Throwable { + withOathSession(OathDeviceTests::testRemovePassword); + } + + @Test + public void testAccountManagement() throws Throwable { + withOathSession(OathDeviceTests::testAccountManagement); + } + } + + public static class Scp11bTests extends NoScpTests { + @Nullable + @Override + protected Byte getScpKid() { + return ScpKid.SCP11b; + } + } +} 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 new file mode 100644 index 00000000..64a6dde6 --- /dev/null +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/OathInstrumentedTests.java @@ -0,0 +1,56 @@ +/* + * 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.framework; + +import com.yubico.yubikit.core.smartcard.SmartCardConnection; +import com.yubico.yubikit.oath.OathSession; +import com.yubico.yubikit.testing.TestState; +import com.yubico.yubikit.testing.oath.OathTestUtils; + +import org.junit.Before; + +public class OathInstrumentedTests extends YKInstrumentedTests { + + public interface Callback { + void invoke(OathSession value) throws Throwable; + } + + private boolean shouldVerifyAndSetupSession = true; + + @Before + public void initializeDeviceTest() { + shouldVerifyAndSetupSession = true; + } + + /** 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, getScpKid()); + shouldVerifyAndSetupSession = false; + } else { + OathTestUtils.updateFipsApprovedValue(device); + } + + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + callback.invoke(new OathSession(connection, TestState.keyParams)); + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..bad78b58 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathDeviceTests.java @@ -0,0 +1,103 @@ +/* + * 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.oath; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.yubico.yubikit.core.smartcard.ApduException; +import com.yubico.yubikit.core.smartcard.SW; +import com.yubico.yubikit.oath.Credential; +import com.yubico.yubikit.oath.CredentialData; +import com.yubico.yubikit.oath.OathSession; + +import java.net.URI; +import java.util.List; + +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 testChangePasswordAfterReconnect(OathSession oath) throws Exception { + assertTrue(oath.isAccessKeySet()); + assertTrue(oath.isLocked()); + assertTrue(oath.unlock(CHANGED_PASSWORD)); + } + + public static void testRemovePassword(OathSession oath) throws Exception { + assertTrue(oath.isAccessKeySet()); + assertTrue(oath.isLocked()); + assertTrue(oath.unlock(OATH_PASSWORD)); + + if (FIPS_APPROVED) { + // 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()); + // the key is still password protected + assertTrue(oath.isAccessKeySet()); + } else { + oath.deleteAccessKey(); + assertFalse(oath.isAccessKeySet()); + assertFalse(oath.isLocked()); + } + } + + public static void testAccountManagement(OathSession oath) throws Exception { + assertTrue(oath.unlock(OATH_PASSWORD)); + List credentials = oath.getCredentials(); + assertEquals(0, credentials.size()); + final String uri = "otpauth://totp/foobar:bob@example.com?secret=abba"; + CredentialData credentialData = CredentialData.parseUri(new URI(uri)); + oath.putCredential(credentialData, false); + + credentials = oath.getCredentials(); + assertEquals(1, credentials.size()); + Credential credential = credentials.get(0); + assertEquals("bob@example.com", credential.getAccountName()); + assertEquals("foobar", credential.getIssuer()); + + credential = oath.renameCredential(credential, "ann@example.com", null); + assertEquals("ann@example.com", credential.getAccountName()); + assertNull(credential.getIssuer()); + + credentials = oath.getCredentials(); + assertEquals(1, credentials.size()); + credential = credentials.get(0); + assertEquals("ann@example.com", credential.getAccountName()); + assertNull(credential.getIssuer()); + + oath.deleteCredential(credential.getId()); + credentials = oath.getCredentials(); + assertEquals(0, credentials.size()); + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java new file mode 100755 index 00000000..0379744a --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java @@ -0,0 +1,96 @@ +/* + * 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.oath; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.yubico.yubikit.core.Transport; +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.oath.OathSession; +import com.yubico.yubikit.testing.TestState; + +import org.junit.Assume; + +import javax.annotation.Nullable; + +public class OathTestUtils { + + public static void updateFipsApprovedValue(YubiKeyDevice device) throws Throwable { + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + ManagementSession managementSession = new ManagementSession(connection); + DeviceInfo deviceInfo = managementSession.getDeviceInfo(); + assertNotNull(deviceInfo); + OathDeviceTests.FIPS_APPROVED = + (deviceInfo.getFipsApproved() & Capability.OATH.bit) == Capability.OATH.bit; + } + } + + public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) throws Throwable { + + OathDeviceTests.OATH_PASSWORD = "".toCharArray(); + + boolean isOathFipsCapable; + + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + ManagementSession managementSession = new ManagementSession(connection); + DeviceInfo deviceInfo = managementSession.getDeviceInfo(); + assertNotNull(deviceInfo); + + isOathFipsCapable = + (deviceInfo.getFipsCapable() & Capability.OATH.bit) == Capability.OATH.bit; + } + + if (kid == null && isOathFipsCapable) { + Assume.assumeTrue("Trying to use OATH FIPS capable device over NFC without SCP", + device.getTransport() != Transport.NFC); + } + + // don't read SCP params on non capable devices + TestState.keyParams = (isOathFipsCapable && kid != null) + ? TestState.readScpKeyParams(device, kid) + : null; + + if (kid != null) { + // skip the test if the connected key does not provide matching SCP keys + Assume.assumeTrue( + "No matching key params found for required kid", + TestState.keyParams != null + ); + } + + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + OathSession oath = new OathSession(connection, TestState.keyParams); + + oath.reset(); + + final char[] oathPassword = "112345678".toCharArray(); + oath.setPassword(oathPassword); + OathDeviceTests.OATH_PASSWORD = oathPassword; + } + + updateFipsApprovedValue(device); + + // 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); + } + } +} From 167e0b9503bf720c724fe69e1ab2389aa8a55739 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 16 Jul 2024 10:34:22 +0200 Subject: [PATCH 17/46] refactor test framework --- .../yubikit/testing/FastDeviceTests.java | 9 ++++--- .../yubikit/testing/oath/OathTests.java | 2 +- .../yubikit/testing/openpgp/OpenPgpTests.java | 2 +- .../testing/piv/PivJcaProviderTests.java | 3 ++- .../yubico/yubikit/testing/TestActivity.java | 25 +++++++++++++------ .../yubikit/testing/oath/OathTestUtils.java | 14 ++++++++--- .../testing/openpgp/OpenPgpTestUtils.java | 12 ++++++++- .../yubikit/testing/piv/PivTestUtils.java | 16 +++++++++--- 8 files changed, 62 insertions(+), 21 deletions(-) diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java index ca8f9d2a..e9baade8 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java @@ -21,12 +21,13 @@ import org.junit.runners.Suite; /** - * All device tests excluding tests considered too slow. + * These tests are here to make testing a bit faster and exclude following: + *

    + *
  • LargeTests
  • + *
*/ @RunWith(Categories.class) +@Suite.SuiteClasses(DeviceTests.class) @Categories.ExcludeCategory(SlowTest.class) -@Suite.SuiteClasses({ - DeviceTests.class -}) public class FastDeviceTests { } 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 35791b3d..14589556 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 @@ -16,7 +16,7 @@ package com.yubico.yubikit.testing.oath; -import androidx.annotation.Nullable; +import javax.annotation.Nullable; import com.yubico.yubikit.core.smartcard.scp.ScpKid; import com.yubico.yubikit.testing.framework.OathInstrumentedTests; 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 34bd4399..9144e505 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,7 +16,7 @@ package com.yubico.yubikit.testing.openpgp; -import androidx.annotation.Nullable; +import javax.annotation.Nullable; import com.yubico.yubikit.core.smartcard.scp.ScpKid; import com.yubico.yubikit.testing.SlowTest; diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java index 57b73440..ab78bc10 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java @@ -16,7 +16,8 @@ package com.yubico.yubikit.testing.piv; -import androidx.annotation.Nullable; +import javax.annotation.Nullable; + import androidx.test.ext.junit.runners.AndroidJUnit4; import com.yubico.yubikit.core.smartcard.scp.ScpKid; diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.java b/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.java index 3d776502..f2190cd6 100644 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.java @@ -22,7 +22,8 @@ import android.widget.ProgressBar; import android.widget.TextView; -import androidx.annotation.Nullable; +import javax.annotation.Nullable; + import androidx.appcompat.app.AppCompatActivity; import com.yubico.yubikit.android.YubiKitManager; @@ -79,6 +80,20 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { @Override protected void onResume() { super.onResume(); + startNfcDiscovery(); + } + + @Override + protected void onPause() { + stopNfcDiscovery(); + super.onPause(); + } + + private void stopNfcDiscovery() { + yubiKitManager.stopNfcDiscovery(this); + } + + private void startNfcDiscovery() { try { yubiKitManager.startNfcDiscovery(new NfcConfiguration().timeout(1000 * 60 * 5), this, sessionQueue::add); } catch (NfcNotAvailable e) { @@ -90,12 +105,6 @@ protected void onResume() { } } - @Override - protected void onPause() { - yubiKitManager.stopNfcDiscovery(this); - super.onPause(); - } - private void setBusy(boolean busy) { runOnUiThread(() -> { if (busy) { @@ -148,6 +157,8 @@ public synchronized void returnSession(YubiKeyDevice device) throws InterruptedE ((NfcYubiKeyDevice) device).remove(lock::release); } else { lock.release(); + stopNfcDiscovery(); + startNfcDiscovery(); } lock.acquire(); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java index 0379744a..c6bf68bd 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java @@ -17,9 +17,11 @@ 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; @@ -59,7 +61,7 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro } if (kid == null && isOathFipsCapable) { - Assume.assumeTrue("Trying to use OATH FIPS capable device over NFC without SCP", + assumeTrue("Trying to use OATH FIPS capable device over NFC without SCP", device.getTransport() != Transport.NFC); } @@ -70,15 +72,21 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro if (kid != null) { // skip the test if the connected key does not provide matching SCP keys - Assume.assumeTrue( + assumeTrue( "No matching key params found for required kid", TestState.keyParams != null ); } try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - OathSession oath = new OathSession(connection, TestState.keyParams); + OathSession oath = null; + try { + oath = new OathSession(connection, TestState.keyParams); + } catch (ApplicationNotAvailableException ignored) { + } + + assumeTrue("OATH not available", oath != null); oath.reset(); final char[] oathPassword = "112345678".toCharArray(); 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 index 94a6ecdc..519d9a0f 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java @@ -20,13 +20,16 @@ import static com.yubico.yubikit.testing.openpgp.OpenPgpTestState.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.smartcard.SmartCardConnection; 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.openpgp.Pw; import com.yubico.yubikit.testing.TestState; @@ -77,7 +80,14 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro } try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - OpenPgpSession openPgp = new OpenPgpSession(connection, TestState.keyParams); + OpenPgpSession openPgp = null; + try { + openPgp = new OpenPgpSession(connection, TestState.keyParams); + } catch (ApplicationNotAvailableException ignored) { + + } + + assumeTrue("OpenPGP not available", openPgp != null); openPgp.reset(); if (hasPinComplexity) { 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 bc842cd7..61966170 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 @@ -21,9 +21,11 @@ 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; @@ -519,6 +521,8 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro boolean isPivFipsCapable; boolean hasPinComplexity; + assumeTrue("No SmartCard support", device.supportsConnection(SmartCardConnection.class)); + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { ManagementSession managementSession = new ManagementSession(connection); DeviceInfo deviceInfo = managementSession.getDeviceInfo(); @@ -529,7 +533,7 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro } if (kid == null && isPivFipsCapable) { - Assume.assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", + assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", device.getTransport() != Transport.NFC); } @@ -540,14 +544,20 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro if (kid != null) { // skip the test if the connected key does not provide matching SCP keys - Assume.assumeTrue( + assumeTrue( "No matching key params found for required kid", TestState.keyParams != null ); } try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - PivSession pivSession = new PivSession(connection, TestState.keyParams); + PivSession pivSession = null; + try { + pivSession = new PivSession(connection, TestState.keyParams); + } catch (ApplicationNotAvailableException ignored) { + + } + assumeTrue("PIV not available", pivSession != null); pivSession.reset(); if (hasPinComplexity) { From c6cbf16e5277d895f9ef393f7030a36f720ed5f6 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 16 Jul 2024 14:33:51 +0200 Subject: [PATCH 18/46] refactor device test SCP parameters --- .../yubikit/openpgp/OpenPgpSession.java | 1 - .../yubikit/testing/FastDeviceTests.java | 2 +- .../framework/OathInstrumentedTests.java | 5 +-- .../framework/OpenPgpInstrumentedTests.java | 5 +-- .../framework/PivInstrumentedTests.java | 5 +-- .../framework/YKInstrumentedTests.java | 3 ++ .../testing/PinComplexityDeviceTests.java | 2 +- .../{TestState.java => ScpParameters.java} | 30 +++++++++++-- .../yubikit/testing/oath/OathTestUtils.java | 21 +++------- .../testing/openpgp/OpenPgpTestUtils.java | 20 +++------ .../yubikit/testing/piv/PivDeviceTests.java | 11 ----- .../yubikit/testing/piv/PivTestUtils.java | 42 ++++++++++--------- 12 files changed, 71 insertions(+), 76 deletions(-) rename testing/src/main/java/com/yubico/yubikit/testing/{TestState.java => ScpParameters.java} (78%) diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java index 210a5145..33e195aa 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java @@ -197,7 +197,6 @@ public OpenPgpSession(SmartCardConnection connection, @Nullable ScpKeyParams scp versionBytes[i] = decodeBcd(versionBcd[i]); } version = Version.fromBytes(versionBytes); - // version = new Version (5,7, 2); protocol.configure(version); // Note: This value is cached! diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java index e9baade8..3499bc93 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java @@ -23,7 +23,7 @@ /** * These tests are here to make testing a bit faster and exclude following: *
    - *
  • LargeTests
  • + *
  • {@link SlowTest}
  • *
*/ @RunWith(Categories.class) 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 64a6dde6..f9ba5ba0 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 @@ -18,7 +18,6 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.oath.OathSession; -import com.yubico.yubikit.testing.TestState; import com.yubico.yubikit.testing.oath.OathTestUtils; import org.junit.Before; @@ -43,14 +42,14 @@ public void initializeDeviceTest() { */ protected void withOathSession(Callback callback) throws Throwable { if (shouldVerifyAndSetupSession) { - OathTestUtils.verifyAndSetup(device, getScpKid()); + OathTestUtils.verifyAndSetup(device, scpParameters); shouldVerifyAndSetupSession = false; } else { OathTestUtils.updateFipsApprovedValue(device); } try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - callback.invoke(new OathSession(connection, TestState.keyParams)); + callback.invoke(new OathSession(connection, scpParameters.getKeyParams())); } } } \ 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 5298dccf..2abef10f 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 @@ -18,7 +18,6 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.openpgp.OpenPgpSession; -import com.yubico.yubikit.testing.TestState; import com.yubico.yubikit.testing.openpgp.OpenPgpTestUtils; public class OpenPgpInstrumentedTests extends YKInstrumentedTests { @@ -29,10 +28,10 @@ public interface Callback { protected void withOpenPgpSession(Callback callback) throws Throwable { - OpenPgpTestUtils.verifyAndSetup(device, getScpKid()); + OpenPgpTestUtils.verifyAndSetup(device, scpParameters); try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - callback.invoke(new OpenPgpSession(connection, TestState.keyParams)); + callback.invoke(new OpenPgpSession(connection, scpParameters.getKeyParams())); } } } \ 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 a68a5579..f5b2483c 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 @@ -18,7 +18,6 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.piv.PivSession; -import com.yubico.yubikit.testing.TestState; import com.yubico.yubikit.testing.piv.PivTestUtils; @@ -30,10 +29,10 @@ public interface Callback { protected void withPivSession(Callback callback) throws Throwable { - PivTestUtils.verifyAndSetup(device, getScpKid()); + PivTestUtils.verifyAndSetup(device, scpParameters); try (SmartCardConnection c = device.openConnection(SmartCardConnection.class)) { - PivSession pivSession = new PivSession(c, TestState.keyParams); + PivSession pivSession = new PivSession(c, scpParameters.getKeyParams()); callback.invoke(pivSession); } } 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 6e38d00e..2211834d 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 @@ -19,6 +19,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule; import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.testing.ScpParameters; import com.yubico.yubikit.testing.TestActivity; import org.junit.After; @@ -32,6 +33,7 @@ public class YKInstrumentedTests { private TestActivity activity; protected YubiKeyDevice device = null; + protected ScpParameters scpParameters; @Rule public final TestName name = new TestName(); @@ -43,6 +45,7 @@ public class YKInstrumentedTests { public void getYubiKey() throws InterruptedException { scenarioRule.getScenario().onActivity((TestActivity activity) -> this.activity = activity); device = activity.awaitSession(getClass().getSimpleName(), name.getMethodName()); + scpParameters = new ScpParameters(device, getScpKid()); } @After diff --git a/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java index d70b23e8..f5005014 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java @@ -115,7 +115,7 @@ private static void testPivPinComplexity(PivSession piv) throws IOException, Apd } - private static void testOpenPgpPinComplexity(OpenPgpSession openpgp) throws IOException, ApduException, InvalidPinException, BadResponseException { + private static void testOpenPgpPinComplexity(OpenPgpSession openpgp) throws IOException, ApduException, InvalidPinException { char[] defaultPin = "123456".toCharArray(); char[] complexDefaultPin = "11234567".toCharArray(); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java b/testing/src/main/java/com/yubico/yubikit/testing/ScpParameters.java similarity index 78% rename from testing/src/main/java/com/yubico/yubikit/testing/TestState.java rename to testing/src/main/java/com/yubico/yubikit/testing/ScpParameters.java index 8835d7c1..9ac2f829 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/ScpParameters.java @@ -33,10 +33,32 @@ import javax.annotation.Nullable; -public class TestState { - public static ScpKeyParams keyParams = null; +public class ScpParameters { + @Nullable + private final Byte kid; + @Nullable + private ScpKeyParams keyParams = null; - public static ScpKeyParams readScpKeyParams(YubiKeyDevice device, @Nullable Byte kid) throws Throwable { + public ScpParameters(YubiKeyDevice device, @Nullable Byte kid) { + this.kid = kid; + try { + keyParams = readScpKeyParams(device); + } catch (Throwable e) { + keyParams = null; + } + } + + @Nullable + public Byte getKid() { + return kid; + } + + @Nullable + public ScpKeyParams getKeyParams() { + return keyParams; + } + + private ScpKeyParams readScpKeyParams(YubiKeyDevice device) throws Throwable { if (kid == null) { return null; } @@ -56,7 +78,7 @@ public static ScpKeyParams readScpKeyParams(YubiKeyDevice device, @Nullable Byte } } - private static KeyRef getKeyRef(SecurityDomainSession scp, byte kid) + private KeyRef getKeyRef(SecurityDomainSession scp, byte kid) throws ApduException, IOException, BadResponseException { Map> keyInformation = scp.getKeyInformation(); KeyRef keyRef = null; diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java index c6bf68bd..60af64d4 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java @@ -27,11 +27,7 @@ import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.management.ManagementSession; import com.yubico.yubikit.oath.OathSession; -import com.yubico.yubikit.testing.TestState; - -import org.junit.Assume; - -import javax.annotation.Nullable; +import com.yubico.yubikit.testing.ScpParameters; public class OathTestUtils { @@ -45,7 +41,7 @@ public static void updateFipsApprovedValue(YubiKeyDevice device) throws Throwabl } } - public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) throws Throwable { + public static void verifyAndSetup(YubiKeyDevice device, ScpParameters scpParameters) throws Throwable { OathDeviceTests.OATH_PASSWORD = "".toCharArray(); @@ -60,28 +56,23 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro (deviceInfo.getFipsCapable() & Capability.OATH.bit) == Capability.OATH.bit; } - if (kid == null && isOathFipsCapable) { + if (scpParameters.getKid() == null && isOathFipsCapable) { assumeTrue("Trying to use OATH FIPS capable device over NFC without SCP", device.getTransport() != Transport.NFC); } - // don't read SCP params on non capable devices - TestState.keyParams = (isOathFipsCapable && kid != null) - ? TestState.readScpKeyParams(device, kid) - : null; - - if (kid != null) { + 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", - TestState.keyParams != null + scpParameters.getKeyParams() != null ); } try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { OathSession oath = null; try { - oath = new OathSession(connection, TestState.keyParams); + oath = new OathSession(connection, scpParameters.getKeyParams()); } catch (ApplicationNotAvailableException ignored) { } 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 index 519d9a0f..d327cb6c 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java @@ -29,21 +29,18 @@ 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.openpgp.Pw; -import com.yubico.yubikit.testing.TestState; +import com.yubico.yubikit.testing.ScpParameters; import org.junit.Assume; -import javax.annotation.Nullable; - 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, @Nullable Byte kid) throws Throwable { + public static void verifyAndSetup(YubiKeyDevice device, ScpParameters scpParameters) throws Throwable { OpenPgpTestState.USER_PIN = Pw.DEFAULT_USER_PIN; OpenPgpTestState.ADMIN_PIN = Pw.DEFAULT_ADMIN_PIN; @@ -61,28 +58,23 @@ public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) thro hasPinComplexity = deviceInfo.getPinComplexity(); } - if (kid == null && isOpenPgpFipsCapable) { + if (scpParameters.getKid() == null && isOpenPgpFipsCapable) { Assume.assumeTrue("Trying to use OpenPgp FIPS capable device over NFC without SCP", device.getTransport() != Transport.NFC); } - // don't read SCP params on non capable devices - TestState.keyParams = (isOpenPgpFipsCapable && kid != null) - ? TestState.readScpKeyParams(device, kid) - : null; - - if (kid != null) { + 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", - TestState.keyParams != null + scpParameters.getKeyParams() != null ); } try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { OpenPgpSession openPgp = null; try { - openPgp = new OpenPgpSession(connection, TestState.keyParams); + openPgp = new OpenPgpSession(connection, scpParameters.getKeyParams()); } catch (ApplicationNotAvailableException ignored) { } 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 4eda84dd..13faef74 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 @@ -19,24 +19,13 @@ 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.BadResponseException; import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.core.smartcard.SW; -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.piv.InvalidPinException; import com.yubico.yubikit.piv.ManagementKeyType; import com.yubico.yubikit.piv.PivSession; -import com.yubico.yubikit.testing.TestState; import org.bouncycastle.util.encoders.Hex; import org.hamcrest.CoreMatchers; 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 61966170..c9d97e0c 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 @@ -34,7 +34,7 @@ import com.yubico.yubikit.piv.KeyType; import com.yubico.yubikit.piv.ManagementKeyType; import com.yubico.yubikit.piv.PivSession; -import com.yubico.yubikit.testing.TestState; +import com.yubico.yubikit.testing.ScpParameters; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.x500.X500Name; @@ -45,7 +45,6 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.junit.Assert; -import org.junit.Assume; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,7 +72,6 @@ import java.security.spec.X509EncodedKeySpec; import java.util.Date; -import javax.annotation.Nullable; import javax.crypto.Cipher; import javax.crypto.KeyAgreement; @@ -512,53 +510,57 @@ public static void x25519KeyAgreement(PrivateKey privateKey, PublicKey publicKey Assert.assertArrayEquals("Secret mismatch", secret, peerSecret); } - public static void verifyAndSetup(YubiKeyDevice device, @Nullable Byte kid) throws Throwable { + public static void verifyAndSetup(YubiKeyDevice device, ScpParameters scpParameters) + throws Throwable { PivTestState.DEFAULT_PIN = PivTestConstants.DEFAULT_PIN; PivTestState.DEFAULT_PUK = PivTestConstants.DEFAULT_PUK; PivTestState.DEFAULT_MANAGEMENT_KEY = PivTestConstants.DEFAULT_MANAGEMENT_KEY; - boolean isPivFipsCapable; - boolean hasPinComplexity; + boolean isPivFipsCapable = false; + boolean hasPinComplexity = false; assumeTrue("No SmartCard support", device.supportsConnection(SmartCardConnection.class)); try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { ManagementSession managementSession = new ManagementSession(connection); - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - assertNotNull(deviceInfo); + try { + DeviceInfo deviceInfo = managementSession.getDeviceInfo(); + assertNotNull(deviceInfo); - isPivFipsCapable = (deviceInfo.getFipsCapable() & Capability.PIV.bit) == Capability.PIV.bit; - hasPinComplexity = deviceInfo.getPinComplexity(); + isPivFipsCapable = (deviceInfo.getFipsCapable() & Capability.PIV.bit) == Capability.PIV.bit; + hasPinComplexity = deviceInfo.getPinComplexity(); + } catch (UnsupportedOperationException ignored) { + + } } - if (kid == null && isPivFipsCapable) { + if (scpParameters.getKid() == null && isPivFipsCapable) { assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", device.getTransport() != Transport.NFC); } - // don't read SCP params on non capable devices - TestState.keyParams = (isPivFipsCapable && kid != null) - ? TestState.readScpKeyParams(device, kid) - : null; - - if (kid != null) { + 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", - TestState.keyParams != null + scpParameters.getKeyParams() != null ); } try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { PivSession pivSession = null; try { - pivSession = new PivSession(connection, TestState.keyParams); + pivSession = new PivSession(connection, scpParameters.getKeyParams()); } catch (ApplicationNotAvailableException ignored) { } assumeTrue("PIV not available", pivSession != null); - pivSession.reset(); + try { + pivSession.reset(); + } catch (Exception e) { + + } if (hasPinComplexity) { // only use complex pins if pin complexity is required From f92b73017eb0d6872317931cc0a5b6d63bbc131b Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 22 Jul 2024 09:47:04 +0200 Subject: [PATCH 19/46] refactor device tests --- .../yubikit/testing/SmokeDeviceTests.java | 32 +++++ .../fido/Ctap2ClientPinInstrumentedTests.java | 7 ++ .../yubikit/testing/oath/OathTests.java | 4 + .../yubikit/testing/openpgp/OpenPgpTests.java | 9 ++ .../testing/piv/PivJcaProviderTests.java | 2 + .../yubico/yubikit/testing/piv/PivTests.java | 10 ++ .../com/yubico/yubikit/testing/SmokeTest.java | 20 ++++ .../yubico/yubikit/testing/TestActivity.java | 20 +--- .../framework/OathInstrumentedTests.java | 8 +- .../framework/OpenPgpInstrumentedTests.java | 5 +- .../framework/PivInstrumentedTests.java | 5 +- .../framework/YKInstrumentedTests.java | 11 +- .../testing/PinComplexityDeviceTests.java | 109 ++++++++---------- .../yubikit/testing/StaticTestState.java | 13 +-- .../com/yubico/yubikit/testing/TestUtils.java | 93 +++++++++++++++ .../yubikit/testing/oath/OathTestUtils.java | 27 +---- .../testing/openpgp/OpenPgpTestUtils.java | 41 +++---- .../yubikit/testing/piv/PivTestUtils.java | 46 +++----- 18 files changed, 291 insertions(+), 171 deletions(-) create mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java create mode 100644 testing-android/src/main/java/com/yubico/yubikit/testing/SmokeTest.java rename testing-android/src/androidTest/java/com/yubico/yubikit/testing/PinComplexityTests.java => testing/src/main/java/com/yubico/yubikit/testing/StaticTestState.java (68%) create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/TestUtils.java diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java new file mode 100644 index 00000000..f13335bc --- /dev/null +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java @@ -0,0 +1,32 @@ +/* + * 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 org.junit.experimental.categories.Categories; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +/** + * Quick, randomly selected tests for different applications. + */ +@RunWith(Categories.class) +@Categories.IncludeCategory({ + SmokeTest.class, +}) +@Suite.SuiteClasses(DeviceTests.class) +public class SmokeDeviceTests { +} \ No newline at end of file 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 ddda00ef..44c7d692 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,6 +22,7 @@ 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; @@ -53,6 +54,12 @@ 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/oath/OathTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/oath/OathTests.java index 14589556..03b3ea28 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 @@ -19,9 +19,11 @@ import javax.annotation.Nullable; import com.yubico.yubikit.core.smartcard.scp.ScpKid; +import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.OathInstrumentedTests; import org.junit.Test; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -33,6 +35,7 @@ public class OathTests { public static class NoScpTests extends OathInstrumentedTests { @Test + @Category(SmokeTest.class) public void testChangePassword() throws Throwable { withOathSession(OathDeviceTests::testChangePassword); withOathSession(OathDeviceTests::testChangePasswordAfterReconnect); @@ -44,6 +47,7 @@ public void testResetPassword() throws Throwable { } @Test + @Category(SmokeTest.class) public void testAccountManagement() throws Throwable { withOathSession(OathDeviceTests::testAccountManagement); } 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 9144e505..9cd59860 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 @@ -19,7 +19,9 @@ 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; import org.junit.Test; @@ -101,6 +103,7 @@ public void testAttestation() throws Throwable { } @Test + @Category(SmokeTest.class) public void testSigPinPolicy() throws Throwable { withOpenPgpSession(OpenPgpDeviceTests::testSigPinPolicy); } @@ -126,6 +129,7 @@ public void testCertificateManagement() throws Throwable { } @Test + @Category(SmokeTest.class) public void testGetChallenge() throws Throwable { withOpenPgpSession(OpenPgpDeviceTests::testGetChallenge); } @@ -134,6 +138,11 @@ public void testGetChallenge() throws Throwable { public void testSetUif() throws Throwable { withOpenPgpSession(OpenPgpDeviceTests::testSetUif); } + + @Test + public void testPinComplexity() throws Throwable { + withOpenPgpSession(PinComplexityDeviceTests::testOpenPgpPinComplexity); + } } public static class Scp11bTests extends NoScpTests { diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java index ab78bc10..430ca3e7 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java @@ -22,6 +22,7 @@ import com.yubico.yubikit.core.smartcard.scp.ScpKid; import com.yubico.yubikit.testing.SlowTest; +import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.PivInstrumentedTests; import org.junit.Test; @@ -45,6 +46,7 @@ public void testGenerateKeysPreferBC() throws Throwable { } @Test + @Category(SmokeTest.class) public void testImportKeys() throws Throwable { withPivSession(PivJcaDeviceTests::testImportKeys); } 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 f7c821ec..505bc626 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,9 +17,12 @@ 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; import org.junit.Test; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -35,6 +38,7 @@ public class PivTests { public static class NoScpTests extends PivInstrumentedTests { @Test + @Category(SmokeTest.class) public void testPin() throws Throwable { withPivSession(PivDeviceTests::testPin); } @@ -60,9 +64,15 @@ public void testPutUncompressedCertificate() throws Throwable { } @Test + @Category(SmokeTest.class) public void testPutCompressedCertificate() throws Throwable { withPivSession(PivCertificateTests::putCompressedCertificate); } + + @Test + public void testPinComplexity() throws Throwable { + withPivSession(PinComplexityDeviceTests::testPivPinComplexity); + } } public static class Scp11bTests extends NoScpTests { diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/SmokeTest.java b/testing-android/src/main/java/com/yubico/yubikit/testing/SmokeTest.java new file mode 100644 index 00000000..9cc98909 --- /dev/null +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/SmokeTest.java @@ -0,0 +1,20 @@ +/* + * 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; + +public interface SmokeTest { +} diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.java b/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.java index f2190cd6..b763da6e 100644 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/TestActivity.java @@ -22,8 +22,6 @@ import android.widget.ProgressBar; import android.widget.TextView; -import javax.annotation.Nullable; - import androidx.appcompat.app.AppCompatActivity; import com.yubico.yubikit.android.YubiKitManager; @@ -41,6 +39,8 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.Semaphore; +import javax.annotation.Nullable; + public class TestActivity extends AppCompatActivity { private TextView testClassNameText; @@ -148,19 +148,9 @@ public synchronized void returnSession(YubiKeyDevice device) throws InterruptedE setBusy(false); }); if (device instanceof NfcYubiKeyDevice) { - // TODO make waitForRemoval configurable from a test case - final boolean waitForRemoval = false; - - //noinspection ConstantValue - if (waitForRemoval) { - // this causes the app to wait for removal of NFC key from the NFC sensor - ((NfcYubiKeyDevice) device).remove(lock::release); - } else { - lock.release(); - stopNfcDiscovery(); - startNfcDiscovery(); - } - + lock.release(); + stopNfcDiscovery(); + startNfcDiscovery(); lock.acquire(); } } 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 f9ba5ba0..6631f85d 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 @@ -18,6 +18,7 @@ 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; @@ -35,21 +36,22 @@ public void initializeDeviceTest() { shouldVerifyAndSetupSession = true; } - /** This method can be called several times during one test. + /** + * 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, scpParameters); + OathTestUtils.verifyAndSetup(device); shouldVerifyAndSetupSession = false; } else { OathTestUtils.updateFipsApprovedValue(device); } try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - callback.invoke(new OathSession(connection, scpParameters.getKeyParams())); + callback.invoke(new OathSession(connection, StaticTestState.scpParameters.getKeyParams())); } } } \ 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 2abef10f..c7344500 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 @@ -18,6 +18,7 @@ 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; public class OpenPgpInstrumentedTests extends YKInstrumentedTests { @@ -28,10 +29,10 @@ public interface Callback { protected void withOpenPgpSession(Callback callback) throws Throwable { - OpenPgpTestUtils.verifyAndSetup(device, scpParameters); + OpenPgpTestUtils.verifyAndSetup(device); try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - callback.invoke(new OpenPgpSession(connection, scpParameters.getKeyParams())); + callback.invoke(new OpenPgpSession(connection, StaticTestState.scpParameters.getKeyParams())); } } } \ 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 f5b2483c..6aec3a77 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 @@ -18,6 +18,7 @@ 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; @@ -29,10 +30,10 @@ public interface Callback { protected void withPivSession(Callback callback) throws Throwable { - PivTestUtils.verifyAndSetup(device, scpParameters); + PivTestUtils.verifyAndSetup(device); try (SmartCardConnection c = device.openConnection(SmartCardConnection.class)) { - PivSession pivSession = new PivSession(c, scpParameters.getKeyParams()); + PivSession pivSession = new PivSession(c, StaticTestState.scpParameters.getKeyParams()); callback.invoke(pivSession); } } 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 2211834d..46bca52f 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 @@ -20,6 +20,7 @@ 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; @@ -33,7 +34,6 @@ public class YKInstrumentedTests { private TestActivity activity; protected YubiKeyDevice device = null; - protected ScpParameters scpParameters; @Rule public final TestName name = new TestName(); @@ -45,14 +45,21 @@ public class YKInstrumentedTests { public void getYubiKey() throws InterruptedException { scenarioRule.getScenario().onActivity((TestActivity activity) -> this.activity = activity); device = activity.awaitSession(getClass().getSimpleName(), name.getMethodName()); - scpParameters = new ScpParameters(device, getScpKid()); + StaticTestState.scpParameters = new ScpParameters(device, getScpKid()); + StaticTestState.currentDevice = device; } @After + public void after() throws InterruptedException { + releaseYubiKey(); + } + public void releaseYubiKey() throws InterruptedException { activity.returnSession(device); device = null; activity = null; + StaticTestState.currentDevice = null; + StaticTestState.scpParameters = null; } public interface Callback { diff --git a/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java index f5005014..bb5f1456 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java @@ -20,21 +20,16 @@ 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.YubiKeyDevice; -import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.application.CommandException; -import com.yubico.yubikit.core.application.InvalidPinException; import com.yubico.yubikit.core.fido.CtapException; -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.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.management.ManagementSession; import com.yubico.yubikit.openpgp.OpenPgpSession; import com.yubico.yubikit.piv.PivSession; @@ -42,68 +37,42 @@ import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.Assert; -import org.junit.Assume; 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 using "weak" PINs on PIV, OpenPGP and Fido2 sessions produces - * expected exceptions. - *

- * Best used over USB transport. + * The test will verify that trying to set a weak PIN for PIV produces expected exceptions. * * @see DeviceInfo#getPinComplexity() */ - public static void testPinComplexity(YubiKeyDevice device) throws IOException, CommandException { - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - - ManagementSession managementSession = new ManagementSession(connection); - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - - Assume.assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); + public static void testPivPinComplexity(PivSession piv) throws Throwable { - PivSession piv = new PivSession(connection); - testPivPinComplexity(piv); - - OpenPgpSession openPgp = new OpenPgpSession(connection); - testOpenPgpPinComplexity(openPgp); - - } - - try (FidoConnection connection = device.openConnection(FidoConnection.class)) { - Ctap2Session ctap2 = new Ctap2Session(connection); - testFidoPinComplexity(ctap2); - } - } - - private static void testPivPinComplexity(PivSession piv) throws IOException, ApduException, InvalidPinException, BadResponseException { + verifyDevice(); + piv.reset(); piv.authenticate(Hex.decode("010203040506070801020304050607080102030405060708")); char[] defaultPin = "123456".toCharArray(); - char[] complexDefaultPin = "11234567".toCharArray(); - char[] currentPin = defaultPin; - // figure out what is the default pin - // on devices with PIN Complexity on, we cannot reset to default 123456 - // that is why we use 1123456. For easier testing we figure out the current pin here. - try { - piv.verifyPin(currentPin); - } catch (Exception ignored) { - currentPin = complexDefaultPin; - piv.verifyPin(currentPin); - } + 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(currentPin, weakPin); + piv.changePin(defaultPin, weakPin); Assert.fail("Set weak PIN"); } catch (ApduException apduException) { if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { @@ -113,24 +82,33 @@ private static void testPivPinComplexity(PivSession piv) throws IOException, Apd 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); } - private static void testOpenPgpPinComplexity(OpenPgpSession openpgp) throws IOException, ApduException, InvalidPinException { - char[] defaultPin = "123456".toCharArray(); - char[] complexDefaultPin = "11234567".toCharArray(); - char[] currentPin = defaultPin; + /** + * 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 { - // figure out what is the default pin - // on devices with PIN Complexity on, we cannot reset to default 123456 - // that is why we use 1123456. For easier testing we figure out the current pin here. - try { - openpgp.verifyUserPin(currentPin, false); - } catch (Exception ignored) { - currentPin = complexDefaultPin; - openpgp.verifyUserPin(currentPin, false); - } + verifyDevice(); + char[] currentPin = "123456".toCharArray(); + openpgp.reset(); openpgp.verifyUserPin(currentPin, false); char[] weakPin = "33333333".toCharArray(); @@ -138,18 +116,27 @@ private static void testOpenPgpPinComplexity(OpenPgpSession openpgp) throws IOEx openpgp.changeUserPin(currentPin, weakPin); } catch (ApduException apduException) { if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { - Assert.fail("Unexpected exception"); + 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"); } } - private static void testFidoPinComplexity(Ctap2Session ctap2) throws IOException, CommandException { + public static void testFidoPinComplexity(Ctap2Session ctap2, Object... ignoredArgs) + throws IOException, CommandException { PinUvAuthProtocol pinUvAuthProtocol = new PinUvAuthProtocolV2(); - char[] defaultPin = "112345678".toCharArray(); + char[] defaultPin = "11234567".toCharArray(); Ctap2Session.InfoData info = ctap2.getCachedInfo(); ClientPin pin = new ClientPin(ctap2, pinUvAuthProtocol); diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/PinComplexityTests.java b/testing/src/main/java/com/yubico/yubikit/testing/StaticTestState.java similarity index 68% rename from testing-android/src/androidTest/java/com/yubico/yubikit/testing/PinComplexityTests.java rename to testing/src/main/java/com/yubico/yubikit/testing/StaticTestState.java index bcc0dbbd..e7b29d00 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/PinComplexityTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/StaticTestState.java @@ -16,14 +16,9 @@ package com.yubico.yubikit.testing; -import com.yubico.yubikit.testing.framework.YKInstrumentedTests; +import com.yubico.yubikit.core.YubiKeyDevice; -import org.junit.Test; - -public class PinComplexityTests extends YKInstrumentedTests { - - @Test - public void testPinComplexity() throws Throwable { - withDevice(PinComplexityDeviceTests::testPinComplexity); - } +public class StaticTestState { + public static YubiKeyDevice currentDevice; + public static ScpParameters scpParameters; } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/TestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/TestUtils.java new file mode 100644 index 00000000..cf9e4254 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestUtils.java @@ -0,0 +1,93 @@ +/* + * 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/oath/OathTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java index 60af64d4..1059d43f 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java @@ -15,7 +15,7 @@ */ package com.yubico.yubikit.testing.oath; -import static org.junit.Assert.assertNotNull; +import static com.yubico.yubikit.testing.StaticTestState.scpParameters; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; @@ -24,37 +24,20 @@ 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.management.ManagementSession; import com.yubico.yubikit.oath.OathSession; -import com.yubico.yubikit.testing.ScpParameters; +import com.yubico.yubikit.testing.TestUtils; public class OathTestUtils { public static void updateFipsApprovedValue(YubiKeyDevice device) throws Throwable { - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - ManagementSession managementSession = new ManagementSession(connection); - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - assertNotNull(deviceInfo); - OathDeviceTests.FIPS_APPROVED = - (deviceInfo.getFipsApproved() & Capability.OATH.bit) == Capability.OATH.bit; - } + OathDeviceTests.FIPS_APPROVED = TestUtils.isFipsApproved(device, Capability.OATH); } - public static void verifyAndSetup(YubiKeyDevice device, ScpParameters scpParameters) throws Throwable { + public static void verifyAndSetup(YubiKeyDevice device) throws Throwable { OathDeviceTests.OATH_PASSWORD = "".toCharArray(); - boolean isOathFipsCapable; - - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - ManagementSession managementSession = new ManagementSession(connection); - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - assertNotNull(deviceInfo); - - isOathFipsCapable = - (deviceInfo.getFipsCapable() & Capability.OATH.bit) == Capability.OATH.bit; - } + boolean isOathFipsCapable = TestUtils.isFipsCapable(device, Capability.OATH); if (scpParameters.getKid() == null && isOathFipsCapable) { assumeTrue("Trying to use OATH FIPS capable device over NFC without SCP", 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 index d327cb6c..a7d940a8 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java @@ -15,9 +15,10 @@ */ 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.USER_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; @@ -28,10 +29,9 @@ 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.openpgp.OpenPgpSession; import com.yubico.yubikit.openpgp.Pw; -import com.yubico.yubikit.testing.ScpParameters; +import com.yubico.yubikit.testing.TestUtils; import org.junit.Assume; @@ -40,23 +40,14 @@ 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, ScpParameters scpParameters) throws Throwable { + public static void verifyAndSetup(YubiKeyDevice device) throws Throwable { OpenPgpTestState.USER_PIN = Pw.DEFAULT_USER_PIN; OpenPgpTestState.ADMIN_PIN = Pw.DEFAULT_ADMIN_PIN; - boolean isOpenPgpFipsCapable; - boolean hasPinComplexity; - - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - ManagementSession managementSession = new ManagementSession(connection); - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - assertNotNull(deviceInfo); - - isOpenPgpFipsCapable = - (deviceInfo.getFipsCapable() & Capability.OPENPGP.bit) == Capability.OPENPGP.bit; - hasPinComplexity = deviceInfo.getPinComplexity(); - } + 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", @@ -89,18 +80,16 @@ public static void verifyAndSetup(YubiKeyDevice device, ScpParameters scpParamet OpenPgpTestState.USER_PIN = COMPLEX_USER_PIN; OpenPgpTestState.ADMIN_PIN = COMPLEX_ADMIN_PIN; } + } - ManagementSession managementSession = new ManagementSession(connection); - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - - FIPS_APPROVED = (deviceInfo.getFipsApproved() & Capability.OPENPGP.bit) == Capability.OPENPGP.bit; + 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); - } + // 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/PivTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestUtils.java index c9d97e0c..e56627a3 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,6 +15,7 @@ */ 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; @@ -30,11 +31,11 @@ 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.piv.KeyType; import com.yubico.yubikit.piv.ManagementKeyType; import com.yubico.yubikit.piv.PivSession; -import com.yubico.yubikit.testing.ScpParameters; +import com.yubico.yubikit.testing.StaticTestState; +import com.yubico.yubikit.testing.TestUtils; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.x500.X500Name; @@ -510,30 +511,19 @@ public static void x25519KeyAgreement(PrivateKey privateKey, PublicKey publicKey Assert.assertArrayEquals("Secret mismatch", secret, peerSecret); } - public static void verifyAndSetup(YubiKeyDevice device, ScpParameters scpParameters) - throws Throwable { + 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; - boolean isPivFipsCapable = false; - boolean hasPinComplexity = false; - assumeTrue("No SmartCard support", device.supportsConnection(SmartCardConnection.class)); - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - ManagementSession managementSession = new ManagementSession(connection); - try { - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - assertNotNull(deviceInfo); - - isPivFipsCapable = (deviceInfo.getFipsCapable() & Capability.PIV.bit) == Capability.PIV.bit; - hasPinComplexity = deviceInfo.getPinComplexity(); - } catch (UnsupportedOperationException ignored) { - - } - } + 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", @@ -574,18 +564,16 @@ public static void verifyAndSetup(YubiKeyDevice device, ScpParameters scpParamet PivTestState.DEFAULT_PUK = COMPLEX_PUK; PivTestState.DEFAULT_MANAGEMENT_KEY = COMPLEX_MANAGEMENT_KEY; } + } - ManagementSession managementSession = new ManagementSession(connection); - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - - FIPS_APPROVED = (deviceInfo.getFipsApproved() & Capability.PIV.bit) == Capability.PIV.bit; + 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); - } + // 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); } } } From 11f7c59ff1b78f8d4fadb81dc4c61b5af180c10c Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 23 Jul 2024 08:24:10 +0200 Subject: [PATCH 20/46] add TestState to PIV, OATH, OpenPGP device tests --- .../yubikit/testing/oath/OathTests.java | 3 +- .../framework/OathInstrumentedTests.java | 41 ++-- .../framework/OpenPgpInstrumentedTests.java | 20 +- .../framework/PivInstrumentedTests.java | 23 +-- .../framework/YKInstrumentedTests.java | 15 +- .../testing/PinComplexityDeviceTests.java | 116 +++++------ .../com/yubico/yubikit/testing/TestState.java | 182 ++++++++++++++++++ .../com/yubico/yubikit/testing/TestUtils.java | 93 --------- .../yubikit/testing/oath/OathDeviceTests.java | 40 ++-- .../yubikit/testing/oath/OathTestState.java | 123 ++++++++++++ .../yubikit/testing/oath/OathTestUtils.java | 78 -------- .../testing/openpgp/OpenPgpDeviceTests.java | 145 +++++++------- .../testing/openpgp/OpenPgpTestState.java | 115 ++++++++++- .../testing/openpgp/OpenPgpTestUtils.java | 95 --------- .../piv/PivBioMultiProtocolDeviceTests.java | 2 +- .../testing/piv/PivCertificateTests.java | 13 +- .../yubikit/testing/piv/PivDeviceTests.java | 51 +++-- .../testing/piv/PivJcaDecryptTests.java | 14 +- .../testing/piv/PivJcaDeviceTests.java | 36 ++-- .../testing/piv/PivJcaSigningTests.java | 14 +- .../yubikit/testing/piv/PivMoveKeyTests.java | 10 +- .../yubikit/testing/piv/PivTestConstants.java | 25 --- .../yubikit/testing/piv/PivTestState.java | 146 +++++++++++++- .../yubikit/testing/piv/PivTestUtils.java | 89 +-------- 24 files changed, 814 insertions(+), 675 deletions(-) create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/TestState.java delete mode 100644 testing/src/main/java/com/yubico/yubikit/testing/TestUtils.java create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java delete mode 100755 testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java delete mode 100755 testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestUtils.java delete mode 100644 testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestConstants.java 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/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..21d96dc7 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,14 @@ 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..b5589710 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,6 +18,7 @@ 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; @@ -70,6 +71,18 @@ 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 protected Byte getScpKid() { return null; diff --git a/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java index bb5f1456..2ef52374 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java @@ -22,7 +22,7 @@ import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; -import com.yubico.yubikit.core.application.CommandException; +import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.core.fido.CtapException; import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.fido.ctap.ClientPin; @@ -32,19 +32,20 @@ import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.openpgp.OpenPgpSession; import com.yubico.yubikit.piv.PivSession; +import com.yubico.yubikit.testing.fido.FidoTestState; +import com.yubico.yubikit.testing.openpgp.OpenPgpTestState; +import com.yubico.yubikit.testing.piv.PivTestState; -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); + private static void verifyDevice(TestState state) { + final DeviceInfo deviceInfo = state.getDeviceInfo(); assumeTrue("Device does not support PIN complexity", deviceInfo != null); assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); } @@ -56,23 +57,21 @@ private static void verifyDevice() { * * @see DeviceInfo#getPinComplexity() */ - public static void testPivPinComplexity(PivSession piv) throws Throwable { + public static void testPivPinComplexity(PivSession piv, PivTestState state) throws Throwable { - verifyDevice(); + verifyDevice(state); piv.reset(); - piv.authenticate(Hex.decode("010203040506070801020304050607080102030405060708")); + piv.authenticate(state.defaultManagementKey); - char[] defaultPin = "123456".toCharArray(); - - piv.verifyPin(defaultPin); + piv.verifyPin(state.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); + piv.changePin(state.defaultPin, weakPin); Assert.fail("Set weak PIN"); } catch (ApduException apduException) { if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { @@ -82,17 +81,19 @@ public static void testPivPinComplexity(PivSession piv) throws Throwable { Assert.fail("Unexpected exception:" + e.getMessage()); } - piv.verifyPin(defaultPin); + piv.verifyPin(state.defaultPin); // change to complex pin - char[] complexPin = "11234567".toCharArray(); + char[] complexPin = "CMPLXPIN".toCharArray(); try { - piv.changePin(defaultPin, complexPin); + piv.changePin(state.defaultPin, complexPin); } catch (Exception e) { Assert.fail("Unexpected exception:" + e.getMessage()); } piv.verifyPin(complexPin); + + piv.changePin(complexPin, state.defaultPin); } @@ -103,17 +104,16 @@ public static void testPivPinComplexity(PivSession piv) throws Throwable { * * @see DeviceInfo#getPinComplexity() */ - public static void testOpenPgpPinComplexity(OpenPgpSession openpgp) throws Throwable { + public static void testOpenPgpPinComplexity(OpenPgpSession openpgp, OpenPgpTestState state) throws Throwable { - verifyDevice(); + verifyDevice(state); - char[] currentPin = "123456".toCharArray(); openpgp.reset(); - openpgp.verifyUserPin(currentPin, false); + openpgp.verifyUserPin(state.defaultUserPin, false); char[] weakPin = "33333333".toCharArray(); try { - openpgp.changeUserPin(currentPin, weakPin); + openpgp.changeUserPin(state.defaultUserPin, weakPin); } catch (ApduException apduException) { if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { fail("Unexpected exception"); @@ -123,49 +123,57 @@ public static void testOpenPgpPinComplexity(OpenPgpSession openpgp) throws Throw } // set complex pin - char[] complexPin = "11234567".toCharArray(); + char[] complexPin = "CMPLXPIN".toCharArray(); try { - openpgp.changeUserPin(currentPin, complexPin); + openpgp.changeUserPin(state.defaultUserPin, 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")); + openpgp.changeUserPin(complexPin, state.defaultUserPin); + } - if (!pinSet) { - pin.setPin(defaultPin); - } else { - pin.getPinToken( - defaultPin, - ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA, - "localhost"); - } + public static void testFido2PinComplexity(FidoTestState state) throws Throwable { + + verifyDevice(state); + + state.withCtap2(ctap2 -> { + 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")); + + try { + if (!pinSet) { + pin.setPin(defaultPin); + } else { + pin.getPinToken( + defaultPin, + ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA, + "localhost"); + } + } catch (ApduException e) { + fail("Failed to set or use PIN. Reset the device and try again"); + } - assertThat(pin.getPinUvAuth().getVersion(), is(pinUvAuthProtocol.getVersion())); - assertThat(pin.getPinRetries().getCount(), is(8)); + 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[] 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); + char[] strongPin = "STRONG PIN".toCharArray(); + pin.changePin(defaultPin, strongPin); + pin.changePin(strongPin, defaultPin); - assertThat(pin.getPinRetries().getCount(), is(8)); + 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..959bfaa3 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -0,0 +1,182 @@ +/* + * 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.Transport; +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 com.yubico.yubikit.testing.fido.FidoTestState; +import com.yubico.yubikit.testing.oath.OathTestState; + +import java.io.IOException; + +import javax.annotation.Nullable; + +public class TestState { + + public static class Builder> { + + final protected YubiKeyDevice device; + private @Nullable Byte scpKid = null; + private @Nullable 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); + } + } + + protected DeviceInfo getDeviceInfo() { + DeviceInfo deviceInfo = null; + try (YubiKeyConnection connection = openConnection()) { + ManagementSession managementSession = getManagementSession(connection, null); + deviceInfo = managementSession.getDeviceInfo(); + } catch (IOException | CommandException ignored) { + + } + + return deviceInfo; + } + + private 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; + } + + 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"); + } + + 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; + } +} 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/oath/OathDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathDeviceTests.java index bad78b58..553a5f91 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 @@ -22,6 +22,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.core.smartcard.SW; import com.yubico.yubikit.oath.Credential; @@ -34,32 +35,35 @@ public class OathDeviceTests { // state of current test - static char[] OATH_PASSWORD = "".toCharArray(); - static boolean FIPS_APPROVED = false; + //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 +76,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/OathTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java new file mode 100644 index 00000000..dc2714e5 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java @@ -0,0 +1,123 @@ +/* + * 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.oath; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +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.CommandException; +import com.yubico.yubikit.core.fido.FidoConnection; +import com.yubico.yubikit.core.smartcard.SmartCardConnection; +import com.yubico.yubikit.management.Capability; +import com.yubico.yubikit.oath.OathSession; +import com.yubico.yubikit.testing.ScpParameters; +import com.yubico.yubikit.testing.TestState; + +import java.io.IOException; + +import javax.annotation.Nullable; + +public class OathTestState extends TestState { + public boolean isFipsApproved; + public char[] password; + + public static class Builder extends TestState.Builder { + + public Builder(YubiKeyDevice device) { + super(device); + } + + public OathTestState build() throws Throwable { + return new OathTestState(this); + } + } + + protected OathTestState(OathTestState.Builder builder) throws Throwable { + super(builder); + + password = "".toCharArray(); + + boolean isOathFipsCapable = isFipsCapable(Capability.OATH); + + if (scpParameters.getKid() == null && isOathFipsCapable) { + assumeTrue("Trying to use OATH 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 = currentDevice.openConnection(SmartCardConnection.class)) { + OathSession oath = null; + try { + oath = new OathSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + + } + + assumeTrue("OATH not available", oath != null); + oath.reset(); + + final char[] complexPassword = "11234567".toCharArray(); + oath.setPassword(complexPassword); + password = complexPassword; + } + + 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", isFipsApproved); + } + } + + public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { + callback.invoke(this); + } + + 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()) { + callback.invoke(getOathSession(connection, scpParameters), 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; + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java deleted file mode 100755 index 1059d43f..00000000 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestUtils.java +++ /dev/null @@ -1,78 +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.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; - -public class OathTestUtils { - - public static void updateFipsApprovedValue(YubiKeyDevice device) throws Throwable { - OathDeviceTests.FIPS_APPROVED = TestUtils.isFipsApproved(device, Capability.OATH); - } - - public static void verifyAndSetup(YubiKeyDevice device) throws Throwable { - - OathDeviceTests.OATH_PASSWORD = "".toCharArray(); - - boolean isOathFipsCapable = TestUtils.isFipsCapable(device, Capability.OATH); - - if (scpParameters.getKid() == null && isOathFipsCapable) { - assumeTrue("Trying to use OATH 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)) { - OathSession oath = null; - try { - oath = new OathSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - - } - - assumeTrue("OATH not available", oath != null); - oath.reset(); - - final char[] oathPassword = "112345678".toCharArray(); - oath.setPassword(oathPassword); - OathDeviceTests.OATH_PASSWORD = oathPassword; - } - - updateFipsApprovedValue(device); - - // 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); - } - } -} 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..7fa2692c 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,9 +16,6 @@ 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 com.yubico.yubikit.core.application.InvalidPinException; import com.yubico.yubikit.core.keys.PrivateKeyValues; import com.yubico.yubikit.core.keys.PublicKeyValues; @@ -82,7 +79,7 @@ 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 +88,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 +98,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 +122,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 +145,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 +168,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 +226,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 +236,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 +254,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 +276,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 +291,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 +299,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 +339,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 +354,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 +375,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 +386,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 +406,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 +443,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 +455,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 +474,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 +496,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 +516,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 +530,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 +551,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 +572,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 +583,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 +600,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 +610,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)); 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..1820b4ce 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,117 @@ 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.YubiKeyConnection; +import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.application.ApplicationNotAvailableException; +import com.yubico.yubikit.core.application.CommandException; +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.ScpParameters; +import com.yubico.yubikit.testing.TestState; + +import org.junit.Assume; + +import java.io.IOException; + +import javax.annotation.Nullable; + +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 = currentDevice.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(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); + + // 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); + } + } + + public void withOpenPgp(StatefulSessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getOpenPgpSession(connection, scpParameters), this); + } + reconnect(); + } -class OpenPgpTestState { - static char[] USER_PIN = Pw.DEFAULT_USER_PIN; - static char[] ADMIN_PIN = Pw.DEFAULT_ADMIN_PIN; - static boolean FIPS_APPROVED = false; + @Nullable + private OpenPgpSession getOpenPgpSession(SmartCardConnection connection, ScpParameters scpParameters) + throws IOException, CommandException { + try { + return new OpenPgpSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + // no OpenPgp support + } + return null; + } } 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/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..248286b0 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,147 @@ package com.yubico.yubikit.testing.piv; +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.application.ApplicationNotAvailableException; +import com.yubico.yubikit.core.application.CommandException; +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.ScpParameters; +import com.yubico.yubikit.testing.TestState; + +import java.io.IOException; + +import javax.annotation.Nullable; + +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)); + + DeviceInfo deviceInfo = getDeviceInfo(); + 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()); + } -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; + 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 + ); + } - static boolean isInvalidKeyType(KeyType keyType) { - return FIPS_APPROVED && (keyType == KeyType.RSA1024 || keyType == KeyType.X25519); + try (SmartCardConnection connection = currentDevice.openConnection(SmartCardConnection.class)) { + 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); + } + } + + boolean isInvalidKeyType(KeyType keyType) { + return isFipsApproved && (keyType == KeyType.RSA1024 || keyType == KeyType.X25519); + } + + public void withPiv(SessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getPivSession(connection, scpParameters)); + } + reconnect(); + } + + public void withPiv(StatefulSessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getPivSession(connection, scpParameters), this); + } + reconnect(); + } + + @Nullable + private PivSession getPivSession(SmartCardConnection connection, ScpParameters scpParameters) + throws IOException, CommandException { + try { + return new PivSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + // no PIV support + } + return null; } + } 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); - } - } } From 1de48072fc38ea846e42e58ac5ef513a44a4fe2c Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 23 Jul 2024 09:57:59 +0200 Subject: [PATCH 21/46] refactor MPE and PIN complexity tests --- .../testing/MultiProtocolResetTests.java | 45 ----- .../fido/Ctap2ClientPinInstrumentedTests.java | 7 - .../testing/mpe/MultiProtocolResetTests.java | 58 ++++++ .../yubikit/testing/openpgp/OpenPgpTests.java | 7 +- .../yubico/yubikit/testing/piv/PivTests.java | 3 +- .../framework/MpeInstrumentedTests.java | 35 ++++ .../framework/YKInstrumentedTests.java | 14 -- .../MultiProtocolResetDeviceTests.java | 138 -------------- .../testing/PinComplexityDeviceTests.java | 179 ------------------ .../yubikit/testing/StaticTestState.java | 24 --- .../com/yubico/yubikit/testing/TestState.java | 19 +- .../yubikit/testing/mpe/MpeTestState.java | 126 ++++++++++++ .../mpe/MultiProtocolResetDeviceTests.java | 77 ++++++++ .../yubikit/testing/oath/OathDeviceTests.java | 6 - .../testing/openpgp/OpenPgpDeviceTests.java | 46 ++++- .../piv/PivPinComplexityDeviceTests.java | 78 ++++++++ 16 files changed, 433 insertions(+), 429 deletions(-) delete mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/MultiProtocolResetTests.java create mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetTests.java create mode 100755 testing-android/src/main/java/com/yubico/yubikit/testing/framework/MpeInstrumentedTests.java delete mode 100755 testing/src/main/java/com/yubico/yubikit/testing/MultiProtocolResetDeviceTests.java delete mode 100755 testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java delete mode 100644 testing/src/main/java/com/yubico/yubikit/testing/StaticTestState.java create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java create mode 100755 testing/src/main/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetDeviceTests.java create mode 100755 testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java 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/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/YKInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/YKInstrumentedTests.java index b5589710..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 @@ -20,8 +20,6 @@ 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; @@ -46,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 @@ -59,16 +55,6 @@ 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() { 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 2ef52374..00000000 --- a/testing/src/main/java/com/yubico/yubikit/testing/PinComplexityDeviceTests.java +++ /dev/null @@ -1,179 +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.YubiKeyDevice; -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 com.yubico.yubikit.testing.fido.FidoTestState; -import com.yubico.yubikit.testing.openpgp.OpenPgpTestState; -import com.yubico.yubikit.testing.piv.PivTestState; - -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; -import org.junit.Assert; - -import java.util.Objects; - -public class PinComplexityDeviceTests { - - private static void verifyDevice(TestState state) { - final DeviceInfo deviceInfo = state.getDeviceInfo(); - 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, PivTestState state) throws Throwable { - - verifyDevice(state); - - piv.reset(); - piv.authenticate(state.defaultManagementKey); - - piv.verifyPin(state.defaultPin); - - MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(3)); - - // try to change to pin which breaks PIN complexity - char[] weakPin = "33333333".toCharArray(); - try { - piv.changePin(state.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(state.defaultPin); - - // change to complex pin - char[] complexPin = "CMPLXPIN".toCharArray(); - try { - piv.changePin(state.defaultPin, complexPin); - } catch (Exception e) { - Assert.fail("Unexpected exception:" + e.getMessage()); - } - - piv.verifyPin(complexPin); - - piv.changePin(complexPin, state.defaultPin); - } - - - /** - * 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, OpenPgpTestState state) throws Throwable { - - verifyDevice(state); - - openpgp.reset(); - openpgp.verifyUserPin(state.defaultUserPin, false); - - char[] weakPin = "33333333".toCharArray(); - try { - openpgp.changeUserPin(state.defaultUserPin, 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(state.defaultUserPin, complexPin); - } catch (Exception e) { - Assert.fail("Unexpected exception"); - } - - openpgp.changeUserPin(complexPin, state.defaultUserPin); - } - - public static void testFido2PinComplexity(FidoTestState state) throws Throwable { - - verifyDevice(state); - - state.withCtap2(ctap2 -> { - 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")); - - try { - if (!pinSet) { - pin.setPin(defaultPin); - } else { - pin.getPinToken( - defaultPin, - ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA, - "localhost"); - } - } catch (ApduException e) { - fail("Failed to set or use PIN. Reset the device and try again"); - } - - 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/StaticTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/StaticTestState.java deleted file mode 100644 index e7b29d00..00000000 --- a/testing/src/main/java/com/yubico/yubikit/testing/StaticTestState.java +++ /dev/null @@ -1,24 +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.YubiKeyDevice; - -public class StaticTestState { - public static YubiKeyDevice currentDevice; - public static ScpParameters scpParameters; -} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java index 959bfaa3..b0522ed2 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -27,8 +27,6 @@ import com.yubico.yubikit.management.Capability; import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.management.ManagementSession; -import com.yubico.yubikit.testing.fido.FidoTestState; -import com.yubico.yubikit.testing.oath.OathTestState; import java.io.IOException; @@ -37,10 +35,11 @@ public class TestState { public static class Builder> { - final protected YubiKeyDevice device; - private @Nullable Byte scpKid = null; - private @Nullable ReconnectDeviceCallback reconnectDeviceCallback = null; + @Nullable + private Byte scpKid = null; + @Nullable + private ReconnectDeviceCallback reconnectDeviceCallback = null; public Builder(YubiKeyDevice device) { this.device = device; @@ -65,8 +64,10 @@ public TestState build() throws Throwable { protected YubiKeyDevice currentDevice; protected ScpParameters scpParameters; - @Nullable public final Byte scpKid; - @Nullable private final ReconnectDeviceCallback reconnectDeviceCallback; + @Nullable + public final Byte scpKid; + @Nullable + private final ReconnectDeviceCallback reconnectDeviceCallback; private final boolean isUsbTransport; protected TestState(Builder builder) { @@ -117,7 +118,7 @@ protected void reconnect() { } } - protected DeviceInfo getDeviceInfo() { + public DeviceInfo getDeviceInfo() { DeviceInfo deviceInfo = null; try (YubiKeyConnection connection = openConnection()) { ManagementSession managementSession = getManagementSession(connection, null); @@ -129,7 +130,7 @@ protected DeviceInfo getDeviceInfo() { return deviceInfo; } - private ManagementSession getManagementSession(YubiKeyConnection connection, ScpParameters scpParameters) + protected ManagementSession getManagementSession(YubiKeyConnection connection, ScpParameters scpParameters) throws IOException, CommandException { ScpKeyParams keyParams = scpParameters != null ? scpParameters.getKeyParams() : null; ManagementSession session = (connection instanceof FidoConnection) 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..d326cf7b --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java @@ -0,0 +1,126 @@ +/* + * 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.Assume.assumeTrue; + +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.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.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.piv.PivSession; +import com.yubico.yubikit.support.DeviceUtil; +import com.yubico.yubikit.testing.ScpParameters; +import com.yubico.yubikit.testing.TestState; + +import org.junit.Assume; + +import java.io.IOException; + +import javax.annotation.Nullable; + +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); + + 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")); + + try (SmartCardConnection connection = openSmartCardConnection()) { + final ManagementSession managementSession = getManagementSession(connection, scpParameters); + managementSession.deviceReset(); + } + + // PIV and FIDO2 should not be reset blocked + assertFalse(isPivResetBlocked()); + assertFalse(isFidoResetBlocked()); + } + + public void withPiv(StatefulSessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + final PivSession piv = getPivSession(connection, scpParameters); + assumeTrue("No PIV support", piv != null); + callback.invoke(piv, this); + } + reconnect(); + } + + public void withCtap2(StatefulSessionCallback callback) throws Throwable { + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + callback.invoke(ctap2, this); + } + reconnect(); + } + + @Nullable + private PivSession getPivSession(SmartCardConnection connection, ScpParameters scpParameters) + throws IOException { + try { + return new PivSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException | ApduException ignored) { + // no OATH support + } + return null; + } + + @Nullable + private Ctap2Session getCtap2Session(YubiKeyConnection connection) + throws IOException, CommandException { + + return (connection instanceof FidoConnection) + ? new Ctap2Session((FidoConnection) connection) + : connection instanceof SmartCardConnection + ? new Ctap2Session((SmartCardConnection) connection) + : null; + } + + 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 553a5f91..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 @@ -22,7 +22,6 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.core.smartcard.SW; import com.yubico.yubikit.oath.Credential; @@ -34,11 +33,6 @@ 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(OathTestState state) throws Throwable { 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 7fa2692c..3f2aebf0 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,11 +16,16 @@ package com.yubico.yubikit.testing.openpgp; +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; @@ -72,7 +77,8 @@ 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) { @@ -623,4 +629,42 @@ public static void testSetUif(OpenPgpSession openpgp, OpenPgpTestState state) th // 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(); + openpgp.verifyUserPin(state.defaultUserPin, false); + + char[] weakPin = "33333333".toCharArray(); + try { + openpgp.changeUserPin(state.defaultUserPin, 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(state.defaultUserPin, complexPin); + } catch (Exception e) { + Assert.fail("Unexpected exception"); + } + + openpgp.changeUserPin(complexPin, state.defaultUserPin); + } } 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..84b09419 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java @@ -0,0 +1,78 @@ +/* + * 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(); + piv.authenticate(state.defaultManagementKey); + + piv.verifyPin(state.defaultPin); + + MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(3)); + + // try to change to pin which breaks PIN complexity + char[] weakPin = "33333333".toCharArray(); + try { + piv.changePin(state.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(state.defaultPin); + + // change to complex pin + char[] complexPin = "CMPLXPIN".toCharArray(); + try { + piv.changePin(state.defaultPin, complexPin); + } catch (Exception e) { + Assert.fail("Unexpected exception:" + e.getMessage()); + } + + piv.verifyPin(complexPin); + + piv.changePin(complexPin, state.defaultPin); + } +} From 5f9450b36bde4a538bb9261fba8135951419a47c Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 23 Jul 2024 11:39:58 +0200 Subject: [PATCH 22/46] TestState provides connection/session utils --- .../yubico/yubikit/testing/DeviceTests.java | 4 +- .../framework/PivInstrumentedTests.java | 1 - .../com/yubico/yubikit/testing/MpeUtils.java | 28 +++ .../com/yubico/yubikit/testing/TestState.java | 165 +++++++++++++++--- .../yubikit/testing/mpe/MpeTestState.java | 62 +------ .../yubikit/testing/oath/OathTestState.java | 39 +---- .../testing/openpgp/OpenPgpTestState.java | 35 +--- .../piv/PivPinComplexityDeviceTests.java | 13 +- .../yubikit/testing/piv/PivTestState.java | 42 +---- 9 files changed, 189 insertions(+), 200 deletions(-) create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/MpeUtils.java 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/main/java/com/yubico/yubikit/testing/framework/PivInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/PivInstrumentedTests.java index 21d96dc7..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 @@ -20,7 +20,6 @@ import com.yubico.yubikit.testing.TestState; import com.yubico.yubikit.testing.piv.PivTestState; - public class PivInstrumentedTests extends YKInstrumentedTests { protected void withPivSession(TestState.StatefulSessionCallback callback) throws Throwable { final PivTestState state = new PivTestState.Builder(device).scpKid(getScpKid()).build(); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/MpeUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/MpeUtils.java new file mode 100644 index 00000000..469be307 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/MpeUtils.java @@ -0,0 +1,28 @@ +/* + * 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.management.DeviceInfo; +import com.yubico.yubikit.support.DeviceUtil; + +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/TestState.java b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java index b0522ed2..2b06152f 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -16,17 +16,25 @@ 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; @@ -118,34 +126,126 @@ protected void reconnect() { } } - public DeviceInfo getDeviceInfo() { - DeviceInfo deviceInfo = null; - try (YubiKeyConnection connection = openConnection()) { - ManagementSession managementSession = getManagementSession(connection, null); - deviceInfo = managementSession.getDeviceInfo(); - } catch (IOException | CommandException ignored) { + 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; + } - return deviceInfo; + // 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(); } - protected ManagementSession getManagementSession(YubiKeyConnection connection, ScpParameters scpParameters) + @Nullable + protected Ctap2Session getCtap2Session(YubiKeyConnection connection) throws IOException, CommandException { - ScpKeyParams keyParams = scpParameters != null ? scpParameters.getKeyParams() : null; - ManagementSession session = (connection instanceof FidoConnection) - ? new ManagementSession((FidoConnection) connection) + return (connection instanceof FidoConnection) + ? new Ctap2Session((FidoConnection) connection) : connection instanceof SmartCardConnection - ? new ManagementSession((SmartCardConnection) connection, keyParams) + ? new Ctap2Session((SmartCardConnection) connection) : null; + } - if (session == null) { - throw new IllegalArgumentException("Connection does not support ManagementSession"); + // OATH helpers + public void withOath(SessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getOathSession(connection, scpParameters)); } + reconnect(); + } - return session; + 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); @@ -163,21 +263,32 @@ protected YubiKeyConnection openConnection() throws IOException { throw new IllegalArgumentException("Device does not support FIDO or SmartCard connection"); } - public boolean isFipsCapable(DeviceInfo deviceInfo, Capability capability) { - return deviceInfo != null && - (deviceInfo.getFipsCapable() & capability.bit) == capability.bit; - } + // common utils + public DeviceInfo getDeviceInfo() { + DeviceInfo deviceInfo = null; + try (YubiKeyConnection connection = openConnection()) { + ManagementSession managementSession = getManagementSession(connection, null); + deviceInfo = managementSession.getDeviceInfo(); + } catch (IOException | CommandException ignored) { - public boolean isFipsCapable(Capability capability) { - return isFipsCapable(getDeviceInfo(), capability); - } + } - public boolean isFipsApproved(Capability capability) { - return isFipsApproved(getDeviceInfo(), capability); + return deviceInfo; } - public boolean isFipsApproved(DeviceInfo deviceInfo, Capability capability) { - return deviceInfo != null && - (deviceInfo.getFipsApproved() & capability.bit) == capability.bit; + 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/mpe/MpeTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java index d326cf7b..79af5b8e 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java @@ -16,31 +16,17 @@ 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.YubiKeyConnection; import com.yubico.yubikit.core.YubiKeyDevice; -import com.yubico.yubikit.core.application.ApplicationNotAvailableException; -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.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.piv.PivSession; -import com.yubico.yubikit.support.DeviceUtil; -import com.yubico.yubikit.testing.ScpParameters; import com.yubico.yubikit.testing.TestState; -import org.junit.Assume; - -import java.io.IOException; - -import javax.annotation.Nullable; - public class MpeTestState extends TestState { public static class Builder extends TestState.Builder { @@ -58,11 +44,7 @@ protected MpeTestState(MpeTestState.Builder builder) throws Throwable { DeviceInfo deviceInfo = getDeviceInfo(); assumeTrue("Cannot get device information", deviceInfo != null); - - 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")); + assumeTrue("Not a MPE device", isMpe(deviceInfo)); try (SmartCardConnection connection = openSmartCardConnection()) { final ManagementSession managementSession = getManagementSession(connection, scpParameters); @@ -74,46 +56,6 @@ protected MpeTestState(MpeTestState.Builder builder) throws Throwable { assertFalse(isFidoResetBlocked()); } - public void withPiv(StatefulSessionCallback callback) throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - final PivSession piv = getPivSession(connection, scpParameters); - assumeTrue("No PIV support", piv != null); - callback.invoke(piv, this); - } - reconnect(); - } - - public void withCtap2(StatefulSessionCallback callback) throws Throwable { - try (YubiKeyConnection connection = openConnection()) { - final Ctap2Session ctap2 = getCtap2Session(connection); - assumeTrue("No CTAP2 support", ctap2 != null); - callback.invoke(ctap2, this); - } - reconnect(); - } - - @Nullable - private PivSession getPivSession(SmartCardConnection connection, ScpParameters scpParameters) - throws IOException { - try { - return new PivSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException | ApduException ignored) { - // no OATH support - } - return null; - } - - @Nullable - private Ctap2Session getCtap2Session(YubiKeyConnection connection) - throws IOException, CommandException { - - return (connection instanceof FidoConnection) - ? new Ctap2Session((FidoConnection) connection) - : connection instanceof SmartCardConnection - ? new Ctap2Session((SmartCardConnection) connection) - : null; - } - boolean isPivResetBlocked() { final DeviceInfo deviceInfo = getDeviceInfo(); return (deviceInfo.getResetBlocked() & Capability.PIV.bit) == Capability.PIV.bit; diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java index dc2714e5..8e2972b9 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java @@ -19,21 +19,13 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; -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.CommandException; -import com.yubico.yubikit.core.fido.FidoConnection; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.management.Capability; import com.yubico.yubikit.oath.OathSession; -import com.yubico.yubikit.testing.ScpParameters; import com.yubico.yubikit.testing.TestState; -import java.io.IOException; - -import javax.annotation.Nullable; - public class OathTestState extends TestState { public boolean isFipsApproved; public char[] password; @@ -68,7 +60,7 @@ protected OathTestState(OathTestState.Builder builder) throws Throwable { ); } - try (SmartCardConnection connection = currentDevice.openConnection(SmartCardConnection.class)) { + try (SmartCardConnection connection = openSmartCardConnection()) { OathSession oath = null; try { oath = new OathSession(connection, scpParameters.getKeyParams()); @@ -91,33 +83,4 @@ protected OathTestState(OathTestState.Builder builder) throws Throwable { assertTrue("Device not OATH FIPS approved as expected", isFipsApproved); } } - - public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { - callback.invoke(this); - } - - 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()) { - callback.invoke(getOathSession(connection, scpParameters), 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; - } } 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 1820b4ce..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 @@ -20,24 +20,16 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; -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.CommandException; 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.ScpParameters; import com.yubico.yubikit.testing.TestState; import org.junit.Assume; -import java.io.IOException; - -import javax.annotation.Nullable; - public class OpenPgpTestState extends TestState { private static final char[] COMPLEX_USER_PIN = "112345678".toCharArray(); @@ -81,13 +73,8 @@ protected OpenPgpTestState(OpenPgpTestState.Builder builder) throws Throwable { ); } - try (SmartCardConnection connection = currentDevice.openConnection(SmartCardConnection.class)) { - OpenPgpSession openPgp = null; - try { - openPgp = new OpenPgpSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - - } + try (SmartCardConnection connection = openSmartCardConnection()) { + OpenPgpSession openPgp = getOpenPgpSession(connection, scpParameters); assumeTrue("OpenPGP not available", openPgp != null); openPgp.reset(); @@ -111,22 +98,4 @@ protected OpenPgpTestState(OpenPgpTestState.Builder builder) throws Throwable { assertTrue("Device not OpenPgp FIPS approved as expected", isFipsApproved); } } - - public void withOpenPgp(StatefulSessionCallback callback) throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - callback.invoke(getOpenPgpSession(connection, scpParameters), this); - } - reconnect(); - } - - @Nullable - private OpenPgpSession getOpenPgpSession(SmartCardConnection connection, ScpParameters scpParameters) - throws IOException, CommandException { - try { - return new OpenPgpSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - // no OpenPgp support - } - return null; - } } 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 index 84b09419..f29d5dd8 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java @@ -42,16 +42,18 @@ static void testPinComplexity(PivSession piv, PivTestState state) throws Throwab assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); piv.reset(); - piv.authenticate(state.defaultManagementKey); - piv.verifyPin(state.defaultPin); + // 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(state.defaultPin, weakPin); + piv.changePin(PivTestState.DEFAULT_PIN, weakPin); Assert.fail("Set weak PIN"); } catch (ApduException apduException) { if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { @@ -61,18 +63,19 @@ static void testPinComplexity(PivSession piv, PivTestState state) throws Throwab Assert.fail("Unexpected exception:" + e.getMessage()); } - piv.verifyPin(state.defaultPin); + piv.verifyPin(PivTestState.DEFAULT_PIN); // change to complex pin char[] complexPin = "CMPLXPIN".toCharArray(); try { - piv.changePin(state.defaultPin, complexPin); + 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/PivTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestState.java index 248286b0..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,26 +16,21 @@ 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.application.ApplicationNotAvailableException; -import com.yubico.yubikit.core.application.CommandException; 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.ScpParameters; import com.yubico.yubikit.testing.TestState; -import java.io.IOException; - -import javax.annotation.Nullable; - public class PivTestState extends TestState { static final char[] DEFAULT_PIN = "123456".toCharArray(); @@ -80,6 +75,10 @@ protected PivTestState(Builder builder) throws Throwable { assumeTrue("No SmartCard support", currentDevice.supportsConnection(SmartCardConnection.class)); DeviceInfo deviceInfo = getDeviceInfo(); + + // skip MPE devices + assumeFalse("Ignoring MPE device", isMpe(deviceInfo)); + boolean isPivFipsCapable = isFipsCapable(deviceInfo, Capability.PIV); boolean hasPinComplexity = deviceInfo != null && deviceInfo.getPinComplexity(); @@ -95,14 +94,13 @@ protected PivTestState(Builder builder) throws Throwable { ); } - try (SmartCardConnection connection = currentDevice.openConnection(SmartCardConnection.class)) { + try (SmartCardConnection connection = openSmartCardConnection()) { PivSession pivSession = getPivSession(connection, scpParameters); assumeTrue("PIV not available", pivSession != null); try { pivSession.reset(); } catch (Exception ignored) { - } if (hasPinComplexity) { @@ -133,30 +131,4 @@ protected PivTestState(Builder builder) throws Throwable { boolean isInvalidKeyType(KeyType keyType) { return isFipsApproved && (keyType == KeyType.RSA1024 || keyType == KeyType.X25519); } - - public void withPiv(SessionCallback callback) throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - callback.invoke(getPivSession(connection, scpParameters)); - } - reconnect(); - } - - public void withPiv(StatefulSessionCallback callback) throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - callback.invoke(getPivSession(connection, scpParameters), this); - } - reconnect(); - } - - @Nullable - private PivSession getPivSession(SmartCardConnection connection, ScpParameters scpParameters) - throws IOException, CommandException { - try { - return new PivSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - // no PIV support - } - return null; - } - } From 63e5e49bc70cd572190409930635826bd7a0027f Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 23 Jul 2024 12:11:18 +0200 Subject: [PATCH 23/46] fix OpenPgp PIN complexity test --- .../yubikit/testing/openpgp/OpenPgpDeviceTests.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 3f2aebf0..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 @@ -644,11 +644,14 @@ public static void testPinComplexity(OpenPgpSession openpgp, OpenPgpTestState st assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); openpgp.reset(); - openpgp.verifyUserPin(state.defaultUserPin, false); + + // 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(state.defaultUserPin, weakPin); + openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, weakPin); } catch (ApduException apduException) { if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { fail("Unexpected exception"); @@ -660,11 +663,12 @@ public static void testPinComplexity(OpenPgpSession openpgp, OpenPgpTestState st // set complex pin char[] complexPin = "CMPLXPIN".toCharArray(); try { - openpgp.changeUserPin(state.defaultUserPin, complexPin); + 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); } } From ad5e169fb46fa513ddada5edd57cf9f181c1f5f8 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 12 Jul 2024 17:59:22 +0200 Subject: [PATCH 24/46] refactor FIDO integration tests, support FIPS --- .../yubico/yubikit/testing/DeviceTests.java | 6 +- ...DeviceTests.java => SmallDeviceTests.java} | 2 + .../BasicWebAuthnClientInstrumentedTests.java | 90 +++-------- .../Ctap2BioEnrollmentInstrumentedTests.java | 45 +----- .../fido/Ctap2ClientPinInstrumentedTests.java | 65 +++----- .../fido/Ctap2ConfigInstrumentedTests.java | 28 +--- ...CredentialManagementInstrumentedTests.java | 66 +++----- .../fido/Ctap2SessionInstrumentedTests.java | 61 +++----- .../Ctap2SessionResetInstrumentedTests.java | 20 +-- ...nterpriseAttestationInstrumentedTests.java | 80 +++------- .../yubikit/testing/fido/FidoTests.java | 35 +++++ .../fido/UvDiscouragedInstrumentedTests.java | 21 +-- .../yubikit/testing/openpgp/OpenPgpTests.java | 9 +- .../testing/piv/PivJcaProviderTests.java | 12 +- ....java => PinUvAuthProtocolV1Category.java} | 2 +- .../framework/FidoInstrumentedTests.java | 61 ++------ .../fido/BasicWebAuthnClientTests.java | 59 ++++--- .../testing/fido/Ctap2BioEnrollmentTests.java | 49 ++---- .../testing/fido/Ctap2ClientPinTests.java | 33 ++-- .../testing/fido/Ctap2ConfigTests.java | 34 ++-- .../fido/Ctap2CredentialManagementTests.java | 27 ++-- .../testing/fido/Ctap2SessionTests.java | 32 ++-- .../fido/EnterpriseAttestationTests.java | 44 +++--- .../yubikit/testing/fido/FidoTestUtils.java | 145 ++++++++++++++++++ .../yubico/yubikit/testing/fido/TestData.java | 8 +- 25 files changed, 473 insertions(+), 561 deletions(-) rename testing-android/src/androidTest/java/com/yubico/yubikit/testing/{FastDeviceTests.java => SmallDeviceTests.java} (96%) create mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java rename testing-android/src/main/java/com/yubico/yubikit/testing/{SlowTest.java => PinUvAuthProtocolV1Category.java} (93%) create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestUtils.java 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 4ae28c48..b72581cc 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 @@ -17,6 +17,7 @@ package com.yubico.yubikit.testing; import com.yubico.yubikit.testing.mpe.MultiProtocolResetTests; +import com.yubico.yubikit.testing.fido.FidoTests; import com.yubico.yubikit.testing.oath.OathTests; import com.yubico.yubikit.testing.openpgp.OpenPgpTests; import com.yubico.yubikit.testing.piv.PivTests; @@ -32,7 +33,8 @@ PivTests.class, OpenPgpTests.class, OathTests.class, - MultiProtocolResetTests.class + MultiProtocolResetTests.class, + FidoTests.class }) public class DeviceTests { -} \ No newline at end of file +} diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmallDeviceTests.java similarity index 96% rename from testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java rename to testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmallDeviceTests.java index 3499bc93..0b0531d3 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmallDeviceTests.java @@ -16,6 +16,8 @@ package com.yubico.yubikit.testing; +import androidx.test.filters.LargeTest; + import org.junit.experimental.categories.Categories; import org.junit.runner.RunWith; import org.junit.runners.Suite; diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java index f92384bc..c0d960a8 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java @@ -16,109 +16,69 @@ package com.yubico.yubikit.testing.fido; -import static com.yubico.yubikit.testing.fido.Ctap2ClientPinInstrumentedTests.supportsPinUvAuthProtocol; - -import androidx.test.filters.LargeTest; - -import com.yubico.yubikit.fido.ctap.ClientPin; 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.PinUvAuthProtocolV1Category; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; -import org.junit.experimental.runners.Enclosed; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.util.Arrays; -import java.util.Collection; +import org.junit.runners.Suite; -@RunWith(Enclosed.class) +@RunWith(Suite.class) +@Suite.SuiteClasses({ + BasicWebAuthnClientInstrumentedTests.PinUvAuthV2Test.class, + BasicWebAuthnClientInstrumentedTests.PinUvAuthV1Test.class, +}) public class BasicWebAuthnClientInstrumentedTests { - @LargeTest - @RunWith(Parameterized.class) - public static class BasicWebAuthnClientParametrizedTests extends FidoInstrumentedTests { - - @Parameterized.Parameter - public PinUvAuthProtocol pinUvAuthProtocol; - - @Parameterized.Parameters - public static Collection data() { - return Arrays.asList( - new PinUvAuthProtocolV1(), - new PinUvAuthProtocolV2()); - } - + public static class PinUvAuthV2Test extends FidoInstrumentedTests { @Test public void testMakeCredentialGetAssertion() throws Throwable { - withCtap2Session( - (device, session) -> supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - BasicWebAuthnClientTests::testMakeCredentialGetAssertion, - pinUvAuthProtocol, - TestData.PIN); + withCtap2Session(BasicWebAuthnClientTests::testMakeCredentialGetAssertion); } @Test public void testMakeCredentialGetAssertionTokenUvOnly() throws Throwable { - withCtap2Session( - (device, session) -> supportsPinUvAuthProtocol(session, pinUvAuthProtocol) - && ClientPin.isTokenSupported(session.getCachedInfo()), - BasicWebAuthnClientTests::testMakeCredentialGetAssertion, - pinUvAuthProtocol, - null); + withCtap2Session(BasicWebAuthnClientTests::testMakeCredentialGetAssertionTokenUvOnly); } @Test public void testGetAssertionMultipleUsersRk() throws Throwable { - withCtap2Session( - (device, session) -> supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - BasicWebAuthnClientTests::testGetAssertionMultipleUsersRk, - pinUvAuthProtocol); + withCtap2Session(BasicWebAuthnClientTests::testGetAssertionMultipleUsersRk); } @Test public void testGetAssertionWithAllowList() throws Throwable { - withCtap2Session( - (device, session) -> supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - BasicWebAuthnClientTests::testGetAssertionWithAllowList, - pinUvAuthProtocol); + withCtap2Session(BasicWebAuthnClientTests::testGetAssertionWithAllowList); } @Test public void testMakeCredentialWithExcludeList() throws Throwable { - withCtap2Session( - (device, session) -> supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - BasicWebAuthnClientTests::testMakeCredentialWithExcludeList, - pinUvAuthProtocol); + withCtap2Session(BasicWebAuthnClientTests::testMakeCredentialWithExcludeList); } @Test public void testMakeCredentialKeyAlgorithms() throws Throwable { - withCtap2Session( - (device, session) -> supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - BasicWebAuthnClientTests::testMakeCredentialKeyAlgorithms, - pinUvAuthProtocol); + withCtap2Session(BasicWebAuthnClientTests::testMakeCredentialKeyAlgorithms); } @Test public void testClientPinManagement() throws Throwable { - withCtap2Session( - (device, session) -> supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - BasicWebAuthnClientTests::testClientPinManagement, - pinUvAuthProtocol); + withCtap2Session(BasicWebAuthnClientTests::testClientPinManagement); } @Test public void testClientCredentialManagement() throws Throwable { - withCtap2Session( - "Credential management or PIN/UV Auth protocol not supported", - (device, session) -> - Ctap2CredentialManagementInstrumentedTests - .isCredentialManagementSupported(session) && - supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - BasicWebAuthnClientTests::testClientCredentialManagement, - pinUvAuthProtocol); + withCtap2Session(BasicWebAuthnClientTests::testClientCredentialManagement); + } + } + + @Category(PinUvAuthProtocolV1Category.class) + public static class PinUvAuthV1Test extends PinUvAuthV2Test { + @Override + protected PinUvAuthProtocol getPinUvAuthProtocol() { + return new PinUvAuthProtocolV1(); } } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentInstrumentedTests.java index 29069d91..39dc5559 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentInstrumentedTests.java @@ -18,55 +18,14 @@ import androidx.test.filters.LargeTest; -import com.yubico.yubikit.fido.ctap.BioEnrollment; -import com.yubico.yubikit.fido.ctap.Ctap2Session; -import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; @LargeTest public class Ctap2BioEnrollmentInstrumentedTests extends FidoInstrumentedTests { - @Test - public void testFingerprintEnrollment() { - runTest(Ctap2BioEnrollmentTests::testFingerprintEnrollment); - } - - // helpers - private final static Logger logger = - LoggerFactory.getLogger(Ctap2BioEnrollmentInstrumentedTests.class); - - private static boolean supportsPinUvAuthProtocol( - Ctap2Session session, - int pinUvAuthProtocolVersion) { - final List pinUvAuthProtocols = - session.getCachedInfo().getPinUvAuthProtocols(); - return pinUvAuthProtocols.contains(pinUvAuthProtocolVersion); - } - - private static boolean supportsBioEnrollment(Ctap2Session session) { - return BioEnrollment.isSupported(session.getCachedInfo()); - } - - private static boolean isSupported(Ctap2Session session) { - return supportsBioEnrollment(session) && supportsPinUvAuthProtocol(session, 2); - } - - private void runTest(Callback callback) { - try { - withCtap2Session( - "Bio enrollment or pinUvProtocol Two not supported", - (device, session) -> supportsBioEnrollment(session) && supportsPinUvAuthProtocol(session, 2), - callback, - new PinUvAuthProtocolV2() - ); - } catch (Throwable throwable) { - logger.error("Caught exception: ", throwable); - } + public void testFingerprintEnrollment() throws Throwable { + withCtap2Session(Ctap2BioEnrollmentTests::testFingerprintEnrollment); } } 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 ddda00ef..63b5a8e2 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 @@ -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,53 +16,34 @@ package com.yubico.yubikit.testing.fido; -import androidx.test.filters.LargeTest; - -import com.yubico.yubikit.fido.ctap.Ctap2Session; 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.PinUvAuthProtocolV1Category; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; - -import java.util.List; - -@LargeTest -public class Ctap2ClientPinInstrumentedTests extends FidoInstrumentedTests { - - public static boolean supportsPinUvAuthProtocol( - Ctap2Session session, - PinUvAuthProtocol pinUvAuthProtocol) { - return supportsPinUvAuthProtocol(session, pinUvAuthProtocol.getVersion()); - } - - public static boolean supportsPinUvAuthProtocol( - Ctap2Session session, - int pinUvAuthProtocolVersion) { - final List pinUvAuthProtocols = - session.getCachedInfo().getPinUvAuthProtocols(); - return pinUvAuthProtocols.contains(pinUvAuthProtocolVersion); - } - - @Test - public void testSetPinProtocolV1() throws Throwable { - withCtap2Session( - Ctap2ClientPinTests::testSetPinProtocol, - new PinUvAuthProtocolV1() - ); +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + Ctap2ClientPinInstrumentedTests.PinUvAuthV2Test.class, + Ctap2ClientPinInstrumentedTests.PinUvAuthV1Test.class, +}) +public class Ctap2ClientPinInstrumentedTests { + public static class PinUvAuthV2Test extends FidoInstrumentedTests { + @Test + public void testClientPin() throws Throwable { + withCtap2Session(Ctap2ClientPinTests::testClientPin); + } } - @Test - public void testSetPinProtocolV2() throws Throwable { - final PinUvAuthProtocol pinUvAuthProtocol = new PinUvAuthProtocolV2(); - withCtap2Session( - "PIN/UV Auth Protocol not supported", - (device, session) -> supportsPinUvAuthProtocol( - session, - pinUvAuthProtocol), - Ctap2ClientPinTests::testSetPinProtocol, - pinUvAuthProtocol - ); + @Category(PinUvAuthProtocolV1Category.class) + public static class PinUvAuthV1Test extends PinUvAuthV2Test { + @Override + protected PinUvAuthProtocol getPinUvAuthProtocol() { + return new PinUvAuthProtocolV1(); + } } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java index 68b6590d..fece1025 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java @@ -16,9 +16,6 @@ package com.yubico.yubikit.testing.fido; -import androidx.test.filters.LargeTest; - -import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -27,27 +24,16 @@ * NOTE: Run the testcases in this suite manually one by one. See test case documentation * and reset the FIDO application where needed. */ -@LargeTest public class Ctap2ConfigInstrumentedTests extends FidoInstrumentedTests { @Test public void testReadWriteEnterpriseAttestation() throws Throwable { - withCtap2Session( - "Device has no support for EnterpriseAttestation", - (ignoredDevice, session) -> session.getInfo().getOptions().containsKey("ep"), - Ctap2ConfigTests::testReadWriteEnterpriseAttestation, - new PinUvAuthProtocolV2() - ); + withCtap2Session(Ctap2ConfigTests::testReadWriteEnterpriseAttestation); } @Test public void testToggleAlwaysUv() throws Throwable { - withCtap2Session( - "Device has no support for alwaysUV", - (ignoredDevice, session) -> session.getInfo().getOptions().containsKey("alwaysUv"), - Ctap2ConfigTests::testToggleAlwaysUv, - new PinUvAuthProtocolV2() - ); + withCtap2Session(Ctap2ConfigTests::testToggleAlwaysUv); } /** @@ -57,10 +43,7 @@ public void testToggleAlwaysUv() throws Throwable { */ @Test public void testSetForcePinChange() throws Throwable { - withCtap2Session( - Ctap2ConfigTests::testSetForcePinChange, - new PinUvAuthProtocolV2() - ); + withCtap2Session(Ctap2ConfigTests::testSetForcePinChange); } /** @@ -70,9 +53,6 @@ public void testSetForcePinChange() throws Throwable { */ @Test public void testSetMinPinLength() throws Throwable { - withCtap2Session( - Ctap2ConfigTests::testSetMinPinLength, - new PinUvAuthProtocolV2() - ); + withCtap2Session(Ctap2ConfigTests::testSetMinPinLength); } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java index 89540717..f92ea0c3 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.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,65 +16,39 @@ package com.yubico.yubikit.testing.fido; -import static com.yubico.yubikit.testing.fido.Ctap2ClientPinInstrumentedTests.supportsPinUvAuthProtocol; - -import androidx.test.filters.LargeTest; - -import com.yubico.yubikit.fido.ctap.CredentialManagement; -import com.yubico.yubikit.fido.ctap.Ctap2Session; 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.PinUvAuthProtocolV1Category; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; -import org.junit.experimental.runners.Enclosed; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import org.junit.runners.Suite; -import java.util.Arrays; -import java.util.Collection; - -@RunWith(Enclosed.class) +@RunWith(Suite.class) +@Suite.SuiteClasses({ + Ctap2CredentialManagementInstrumentedTests.PinUvAuthV2Test.class, + Ctap2CredentialManagementInstrumentedTests.PinUvAuthV1Test.class, +}) public class Ctap2CredentialManagementInstrumentedTests { - - static boolean isCredentialManagementSupported(Ctap2Session session) { - final Ctap2Session.InfoData info = session.getCachedInfo(); - return CredentialManagement.isSupported(info); - } - - @LargeTest - @RunWith(Parameterized.class) - public static class Ctap2CredentialManagementParametrizedTests extends FidoInstrumentedTests { - - @Parameterized.Parameter - public PinUvAuthProtocol pinUvAuthProtocol; - - @Parameterized.Parameters - public static Collection data() { - return Arrays.asList( - new PinUvAuthProtocolV1(), - new PinUvAuthProtocolV2()); - } - + public static class PinUvAuthV2Test extends FidoInstrumentedTests { @Test public void testReadMetadata() throws Throwable { - withCtap2Session( - "Credential management or PIN/UV Auth protocol not supported", - (device, session) -> isCredentialManagementSupported(session) && - supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - Ctap2CredentialManagementTests::testReadMetadata, - pinUvAuthProtocol); + withCtap2Session(Ctap2CredentialManagementTests::testReadMetadata); } @Test public void testManagement() throws Throwable { - withCtap2Session( - "Credential management or PIN/UV Auth protocol not supported", - (device, session) -> isCredentialManagementSupported(session) && - supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - Ctap2CredentialManagementTests::testManagement, - pinUvAuthProtocol); + withCtap2Session(Ctap2CredentialManagementTests::testManagement); + } + } + + @Category(PinUvAuthProtocolV1Category.class) + public static class PinUvAuthV1Test extends PinUvAuthV2Test { + @Override + protected PinUvAuthProtocol getPinUvAuthProtocol() { + return new PinUvAuthProtocolV1(); } } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.java index d52682ff..ce287225 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.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,63 +16,44 @@ package com.yubico.yubikit.testing.fido; -import static com.yubico.yubikit.testing.fido.Ctap2ClientPinInstrumentedTests.supportsPinUvAuthProtocol; - -import androidx.test.filters.LargeTest; - -import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice; 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.PinUvAuthProtocolV1Category; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; -import org.junit.experimental.runners.Enclosed; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.util.Arrays; -import java.util.Collection; +import org.junit.runners.Suite; -@RunWith(Enclosed.class) +@RunWith(Suite.class) +@Suite.SuiteClasses({ + Ctap2SessionInstrumentedTests.PinUvAuthV2Test.class, + Ctap2SessionInstrumentedTests.PinUvAuthV1Test.class, +}) public class Ctap2SessionInstrumentedTests { - @LargeTest - @RunWith(Parameterized.class) - public static class Ctap2SessionParametrizedTests extends FidoInstrumentedTests { - @Parameterized.Parameter - public PinUvAuthProtocol pinUvAuthProtocol; - - @Parameterized.Parameters - public static Collection data() { - return Arrays.asList( - new PinUvAuthProtocolV1(), - new PinUvAuthProtocolV2()); - } - + public static class PinUvAuthV2Test extends FidoInstrumentedTests { @Test public void testCtap2GetInfo() throws Throwable { - withCtap2Session( - (device, session) -> supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - Ctap2SessionTests::testCtap2GetInfo, - pinUvAuthProtocol); + withCtap2Session(Ctap2SessionTests::testCtap2GetInfo); } @Test public void testCancelCborCommandImmediate() throws Throwable { - withCtap2Session( - (device, session) -> device instanceof UsbYubiKeyDevice && - supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - Ctap2SessionTests::testCancelCborCommandImmediate, - pinUvAuthProtocol); + withCtap2Session(Ctap2SessionTests::testCancelCborCommandImmediate); } @Test public void testCancelCborCommandAfterDelay() throws Throwable { - withCtap2Session( - (device, session) -> device instanceof UsbYubiKeyDevice && - supportsPinUvAuthProtocol(session, pinUvAuthProtocol), - Ctap2SessionTests::testCancelCborCommandAfterDelay, - pinUvAuthProtocol); + withCtap2Session(Ctap2SessionTests::testCancelCborCommandAfterDelay); + } + } + + @Category(PinUvAuthProtocolV1Category.class) + public static class PinUvAuthV1Test extends PinUvAuthV2Test { + @Override + protected PinUvAuthProtocol getPinUvAuthProtocol() { + return new PinUvAuthProtocolV1(); } } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java index 8888f53f..51217e27 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Yubico. + * Copyright (C) 2023-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,13 +16,10 @@ package com.yubico.yubikit.testing.fido; -import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; -import java.util.Map; - /** * Tests FIDO Reset. *

@@ -34,21 +31,8 @@ * */ public class Ctap2SessionResetInstrumentedTests extends FidoInstrumentedTests { - - /** - * @noinspection BooleanMethodIsAlwaysInverted - */ - private static boolean supportsBioEnroll(Ctap2Session session) { - final Map options = session.getCachedInfo().getOptions(); - return options.containsKey("bioEnroll"); - } - @Test public void testReset() throws Throwable { - withCtap2Session( - "Skipping reset test - authenticator supports bio enrollment", - (device, session) -> !supportsBioEnroll(session), - Ctap2SessionTests::testReset - ); + withCtap2Session(Ctap2SessionTests::testReset); } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java index 834a703c..c856e6f2 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Yubico. + * Copyright (C) 2023-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,85 +16,49 @@ package com.yubico.yubikit.testing.fido; -import static com.yubico.yubikit.testing.fido.Ctap2ClientPinInstrumentedTests.supportsPinUvAuthProtocol; - -import androidx.test.filters.LargeTest; - -import com.yubico.yubikit.fido.ctap.Ctap2Session; 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.PinUvAuthProtocolV1Category; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; -import org.junit.experimental.runners.Enclosed; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import org.junit.runners.Suite; -import java.util.Arrays; -import java.util.Collection; - -@RunWith(Enclosed.class) +@RunWith(Suite.class) +@Suite.SuiteClasses({ + EnterpriseAttestationInstrumentedTests.PinUvAuthV2Test.class, + EnterpriseAttestationInstrumentedTests.PinUvAuthV1Test.class, +}) public class EnterpriseAttestationInstrumentedTests { - @LargeTest - @RunWith(Parameterized.class) - public static class EnterpriseAttestationParametrizedTests extends FidoInstrumentedTests { - - @Parameterized.Parameter - public PinUvAuthProtocol pinUvAuthProtocol; - - @Parameterized.Parameters - public static Collection data() { - return Arrays.asList( - new PinUvAuthProtocolV1(), - new PinUvAuthProtocolV2()); - } - - static boolean isEnterpriseAttestationsSupported(Ctap2Session session) { - final Ctap2Session.InfoData info = session.getCachedInfo(); - return info.getOptions().containsKey("ep"); - } - - static boolean isSupported(Ctap2Session session, - PinUvAuthProtocol pinUvAuthProtocol) { - return isEnterpriseAttestationsSupported(session) && - supportsPinUvAuthProtocol(session, pinUvAuthProtocol); - } - + public static class PinUvAuthV2Test extends FidoInstrumentedTests { @Test public void testSupportedPlatformManagedEA() throws Throwable { - withCtap2Session( - "Enterprise attestation is not supported/enabled", - (device, session) -> isSupported(session, pinUvAuthProtocol), - EnterpriseAttestationTests::testSupportedPlatformManagedEA, - pinUvAuthProtocol); + withCtap2Session(EnterpriseAttestationTests::testSupportedPlatformManagedEA); } @Test public void testUnsupportedPlatformManagedEA() throws Throwable { - withCtap2Session( - "Enterprise attestation is not supported/enabled", - (device, session) -> isSupported(session, pinUvAuthProtocol), - EnterpriseAttestationTests::testUnsupportedPlatformManagedEA, - pinUvAuthProtocol); + withCtap2Session(EnterpriseAttestationTests::testUnsupportedPlatformManagedEA); } @Test public void testCreateOptionsAttestationPreference() throws Throwable { - withCtap2Session( - "Enterprise attestation is not supported/enabled", - (device, session) -> isSupported(session, pinUvAuthProtocol), - EnterpriseAttestationTests::testCreateOptionsAttestationPreference, - pinUvAuthProtocol); + withCtap2Session(EnterpriseAttestationTests::testCreateOptionsAttestationPreference); } @Test public void testVendorFacilitatedEA() throws Throwable { - withCtap2Session( - "Enterprise attestation is not supported/enabled", - (device, session) -> isSupported(session, pinUvAuthProtocol), - EnterpriseAttestationTests::testVendorFacilitatedEA, - pinUvAuthProtocol); + withCtap2Session(EnterpriseAttestationTests::testVendorFacilitatedEA); + } + } + + @Category(PinUvAuthProtocolV1Category.class) + public static class PinUvAuthV1Test extends PinUvAuthV2Test { + @Override + protected PinUvAuthProtocol getPinUvAuthProtocol() { + return new PinUvAuthProtocolV1(); } } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java new file mode 100644 index 00000000..a0de1c12 --- /dev/null +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java @@ -0,0 +1,35 @@ +/* + * 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.fido; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + BasicWebAuthnClientInstrumentedTests.class, + Ctap2BioEnrollmentInstrumentedTests.class, + Ctap2ClientPinInstrumentedTests.class, + Ctap2ConfigInstrumentedTests.class, + Ctap2CredentialManagementInstrumentedTests.class, + Ctap2SessionInstrumentedTests.class, + Ctap2SessionResetInstrumentedTests.class, + EnterpriseAttestationInstrumentedTests.class, + UvDiscouragedInstrumentedTests.class, +}) +public class FidoTests { +} diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java index a9d0d601..f8b077fd 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java @@ -16,40 +16,23 @@ package com.yubico.yubikit.testing.fido; -import androidx.test.filters.LargeTest; - import com.yubico.yubikit.fido.client.PinRequiredClientError; -import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; -@LargeTest public class UvDiscouragedInstrumentedTests extends FidoInstrumentedTests { - - static boolean hasPin(Ctap2Session session) { - final Ctap2Session.InfoData info = session.getCachedInfo(); - return Boolean.TRUE.equals(info.getOptions().get("clientPin")); - } - @Test public void testMakeCredentialGetAssertion() throws Throwable { - withCtap2Session( - "This device has a PIN set", - (device, session) -> !hasPin(session), - BasicWebAuthnClientTests::testUvDiscouragedMakeCredentialGetAssertion); + withCtap2Session(BasicWebAuthnClientTests::testUvDiscouragedMakeCredentialGetAssertionWithoutPin); } - /** * Run this test only on devices with PIN set * Expected to fail with PinRequiredClientError */ @Test(expected = PinRequiredClientError.class) public void testMakeCredentialGetAssertionOnProtected() throws Throwable { - withCtap2Session( - "This device has no PIN set", - (device, session) -> hasPin(session), - BasicWebAuthnClientTests::testUvDiscouragedMakeCredentialGetAssertion); + withCtap2Session(BasicWebAuthnClientTests::testUvDiscouragedMakeCredentialGetAssertionWithPin); } } 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 9c8343c2..e63b0f11 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,13 +16,14 @@ package com.yubico.yubikit.testing.openpgp; +import javax.annotation.Nullable; + +import androidx.test.filters.LargeTest; + import com.yubico.yubikit.core.smartcard.scp.ScpKid; -import com.yubico.yubikit.testing.SlowTest; -import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.OpenPgpInstrumentedTests; import org.junit.Test; -import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -76,7 +77,7 @@ public void testSetPinAttempts() throws Throwable { } @Test - @Category(SlowTest.class) + @LargeTest public void testGenerateRsaKeys() throws Throwable { withOpenPgpSession(OpenPgpDeviceTests::testGenerateRsaKeys); } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java index 430ca3e7..fab6ba5a 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java @@ -19,14 +19,12 @@ import javax.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; import com.yubico.yubikit.core.smartcard.scp.ScpKid; -import com.yubico.yubikit.testing.SlowTest; -import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.PivInstrumentedTests; import org.junit.Test; -import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @@ -34,13 +32,13 @@ public class PivJcaProviderTests { public static class NoScpTests extends PivInstrumentedTests { @Test - @Category(SlowTest.class) + @LargeTest public void testGenerateKeys() throws Throwable { withPivSession(PivJcaDeviceTests::testGenerateKeys); } @Test - @Category(SlowTest.class) + @LargeTest public void testGenerateKeysPreferBC() throws Throwable { withPivSession(PivJcaDeviceTests::testGenerateKeysPreferBC); } @@ -52,13 +50,13 @@ public void testImportKeys() throws Throwable { } @Test - @Category(SlowTest.class) + @LargeTest public void testSigning() throws Throwable { withPivSession(PivJcaSigningTests::testSign); } @Test - @Category(SlowTest.class) + @LargeTest public void testDecrypt() throws Throwable { withPivSession(PivJcaDecryptTests::testDecrypt); } diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/SlowTest.java b/testing-android/src/main/java/com/yubico/yubikit/testing/PinUvAuthProtocolV1Category.java similarity index 93% rename from testing-android/src/main/java/com/yubico/yubikit/testing/SlowTest.java rename to testing-android/src/main/java/com/yubico/yubikit/testing/PinUvAuthProtocolV1Category.java index 2ce9ac4d..2da8d3a0 100644 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/SlowTest.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/PinUvAuthProtocolV1Category.java @@ -16,5 +16,5 @@ package com.yubico.yubikit.testing; -public interface SlowTest { +public interface PinUvAuthProtocolV1Category { } diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java index 0d00d2b7..b505e70c 100755 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.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,68 +16,28 @@ package com.yubico.yubikit.testing.framework; -import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.fido.ctap.Ctap2Session; - -import org.junit.Assume; +import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; +import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2; +import com.yubico.yubikit.testing.fido.FidoTestUtils; import java.util.Optional; import java.util.concurrent.LinkedBlockingQueue; -import javax.annotation.Nullable; - -/** - * @noinspection unused - */ public class FidoInstrumentedTests extends YKInstrumentedTests { - - public interface TestCondition { - boolean invoke(YubiKeyDevice device, Ctap2Session value) throws Throwable; - } - public interface Callback { - void invoke(Ctap2Session session, Object... args) throws Throwable; + void invoke(Ctap2Session session) throws Throwable; } - protected void withCtap2Session( - Callback callback, - Object... args) throws Throwable { - withCtap2Session(null, null, callback, args); - } - - protected void withCtap2Session( - @Nullable TestCondition testCondition, - Callback callback, - Object... args) throws Throwable { - withCtap2Session(null, testCondition, callback, args); - } + protected void withCtap2Session(Callback callback) throws Throwable { - protected void withCtap2Session( - @Nullable TestCondition testCondition, - Callback callback) throws Throwable { - withCtap2Session((String) null, testCondition, callback); - } + FidoTestUtils.verifyAndSetup(device, getPinUvAuthProtocol()); - protected void withCtap2Session( - @Nullable String message, - @Nullable TestCondition testCondition, - Callback callback, - Object... args) throws Throwable { LinkedBlockingQueue> result = new LinkedBlockingQueue<>(); Ctap2Session.create(device, value -> { try { Ctap2Session session = value.getValue(); - if (testCondition != null) { - if (message != null) { - Assume.assumeTrue( - message, - testCondition.invoke(device, session)); - } else { - Assume.assumeTrue( - testCondition.invoke(device, session)); - } - } - callback.invoke(session, args); + callback.invoke(session); result.offer(Optional.empty()); } catch (Throwable e) { result.offer(Optional.of(e)); @@ -89,4 +49,9 @@ protected void withCtap2Session( throw exception.get(); } } + + protected PinUvAuthProtocol getPinUvAuthProtocol() { + // default is protocol V2 + return new PinUvAuthProtocolV2(); + } } \ No newline at end of file diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java index 87ea57b6..c9ba3034 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java @@ -26,6 +26,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; import com.yubico.yubikit.core.application.CommandException; import com.yubico.yubikit.core.fido.CtapException; @@ -34,6 +36,8 @@ import com.yubico.yubikit.fido.client.ClientError; import com.yubico.yubikit.fido.client.CredentialManager; import com.yubico.yubikit.fido.client.MultipleAssertionsAvailable; +import com.yubico.yubikit.fido.ctap.ClientPin; +import com.yubico.yubikit.fido.ctap.CredentialManagement; import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.fido.webauthn.AuthenticatorAssertionResponse; import com.yubico.yubikit.fido.webauthn.AuthenticatorAttestationResponse; @@ -67,13 +71,16 @@ public class BasicWebAuthnClientTests { - public static void testMakeCredentialGetAssertion( - Ctap2Session session, - Object... args) throws Throwable { + public static void testMakeCredentialGetAssertionTokenUvOnly(Ctap2Session session) throws Throwable { + assumeTrue("UV Token not supported", ClientPin.isTokenSupported(session.getCachedInfo())); + testMakeCredentialGetAssertion(session); + } + + public static void testMakeCredentialGetAssertion(Ctap2Session session) throws Throwable { - Ctap2ClientPinTests.ensureDefaultPinSet(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + // // Ctap2ClientPinTests.ensureDefaultPinSet(session); - char[] pin = (char[]) args[1]; + char[] pin = TestData.PIN; BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); List deleteCredIds = new ArrayList<>(); @@ -155,9 +162,19 @@ public static void testMakeCredentialGetAssertion( deleteCredentials(webauthn, deleteCredIds); } - public static void testUvDiscouragedMakeCredentialGetAssertion( - Ctap2Session session, - Object... ignoredUnusedArgs) throws Throwable { + public static void testUvDiscouragedMakeCredentialGetAssertionWithPin(Ctap2Session session) throws Throwable { + assumeTrue("Device has a PIN set", + Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); + testUvDiscouragedMakeCredentialGetAssertion(session); + } + + public static void testUvDiscouragedMakeCredentialGetAssertionWithoutPin(Ctap2Session session) throws Throwable { + assumeFalse("Device has no PIN set", + Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); + testUvDiscouragedMakeCredentialGetAssertion(session); + } + + private static void testUvDiscouragedMakeCredentialGetAssertion(Ctap2Session session) throws Throwable { BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); @@ -268,9 +285,9 @@ public static void testUvDiscouragedMakeCredentialGetAssertion( } } - public static void testGetAssertionMultipleUsersRk(Ctap2Session session, Object... args) throws Throwable { + public static void testGetAssertionMultipleUsersRk(Ctap2Session session) throws Throwable { - Ctap2ClientPinTests.ensureDefaultPinSet(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + // Ctap2ClientPinTests.ensureDefaultPinSet(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); List deleteCredIds = new ArrayList<>(); @@ -348,9 +365,9 @@ public static void testGetAssertionMultipleUsersRk(Ctap2Session session, Object. deleteCredentials(webauthn, deleteCredIds); } - public static void testGetAssertionWithAllowList(Ctap2Session session, Object... args) throws Throwable { + public static void testGetAssertionWithAllowList(Ctap2Session session) throws Throwable { - Ctap2ClientPinTests.ensureDefaultPinSet(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + // Ctap2ClientPinTests.ensureDefaultPinSet(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); @@ -438,9 +455,9 @@ public static void testGetAssertionWithAllowList(Ctap2Session session, Object... assertArrayEquals(credId2, credential.getRawId()); } - public static void testMakeCredentialWithExcludeList(Ctap2Session session, Object... args) throws Throwable { + public static void testMakeCredentialWithExcludeList(Ctap2Session session) throws Throwable { - Ctap2ClientPinTests.ensureDefaultPinSet(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + // Ctap2ClientPinTests.ensureDefaultPinSet(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); List excludeList = new ArrayList<>(); @@ -507,8 +524,8 @@ public static void testMakeCredentialWithExcludeList(Ctap2Session session, Objec ); } - public static void testMakeCredentialKeyAlgorithms(Ctap2Session session, Object... args) throws Throwable { - Ctap2ClientPinTests.ensureDefaultPinSet(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + public static void testMakeCredentialKeyAlgorithms(Ctap2Session session) throws Throwable { + // Ctap2ClientPinTests.ensureDefaultPinSet(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); List allCredParams = Arrays.asList( TestData.PUB_KEY_CRED_PARAMS_ES256, @@ -583,8 +600,8 @@ public static void testMakeCredentialKeyAlgorithms(Ctap2Session session, Object. assertEquals(credParams.get(0).getAlg(), alg); } - public static void testClientPinManagement(Ctap2Session session, Object... args) throws Throwable { - Ctap2ClientPinTests.ensureDefaultPinSet(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + public static void testClientPinManagement(Ctap2Session session) throws Throwable { + // Ctap2ClientPinTests.ensureDefaultPinSet(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); assertTrue(webauthn.isPinSupported()); @@ -608,8 +625,10 @@ public static void testClientPinManagement(Ctap2Session session, Object... args) } - public static void testClientCredentialManagement(Ctap2Session session, Object... args) throws Throwable { - Ctap2ClientPinTests.ensureDefaultPinSet(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + public static void testClientCredentialManagement(Ctap2Session session) throws Throwable { + assumeTrue("Credential management not supported", + CredentialManagement.isSupported(session.getCachedInfo())); + // Ctap2ClientPinTests.ensureDefaultPinSet(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); PublicKeyCredentialCreationOptions creationOptions = getCreateOptions(null, true, Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java index 295a8685..ce5c9ed7 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java @@ -16,59 +16,36 @@ package com.yubico.yubikit.testing.fido; +import static com.yubico.yubikit.testing.fido.Ctap2ClientPinTests.ensureDefaultPinSet; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; 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.fido.ctap.BioEnrollment; import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.fido.ctap.FingerprintBioEnrollment; -import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.testing.piv.PivCertificateTests; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.util.Arrays; import java.util.Map; -import java.util.Objects; public class Ctap2BioEnrollmentTests { private static final Logger logger = LoggerFactory.getLogger(PivCertificateTests.class); - static PinUvAuthProtocol getPinUvAuthProtocol(Object... args) { - assertThat("Missing required argument: PinUvAuthProtocol", args.length > 0); - return (PinUvAuthProtocol) args[0]; - } - - /** - * Attempts to set (or verify) the default PIN, or fails. - */ - static void ensureDefaultPinSet(Ctap2Session session, PinUvAuthProtocol pinUvAuthProtocol) - throws IOException, CommandException { + public static void testFingerprintEnrollment(Ctap2Session session) throws Throwable { - Ctap2Session.InfoData info = session.getCachedInfo(); + assumeTrue(" Bio enrollment not supported", + BioEnrollment.isSupported(session.getCachedInfo())); - ClientPin pin = new ClientPin(session, pinUvAuthProtocol); - boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); - - if (!pinSet) { - pin.setPin(TestData.PIN); - } else { - pin.getPinToken( - TestData.PIN, - ClientPin.PIN_PERMISSION_BE, - "localhost"); - } - } - - public static void testFingerprintEnrollment(Ctap2Session session, Object... args) throws Throwable { - final FingerprintBioEnrollment fingerprintBioEnrollment = fpBioEnrollment(session, args); + final FingerprintBioEnrollment fingerprintBioEnrollment = fpBioEnrollment(session); removeAllFingerprints(fingerprintBioEnrollment); @@ -123,21 +100,17 @@ private static byte[] enrollFingerprint(FingerprintBioEnrollment bioEnrollment) } private static FingerprintBioEnrollment fpBioEnrollment( - Ctap2Session session, - Object... args) throws Throwable { - - assertThat("Missing required argument: PinUvAuthProtocol", args.length > 0); - final PinUvAuthProtocol pinUvAuthProtocol = getPinUvAuthProtocol(args); + Ctap2Session session) throws Throwable { - ensureDefaultPinSet(session, pinUvAuthProtocol); + // ensureDefaultPinSet(session); - final ClientPin pin = new ClientPin(session, pinUvAuthProtocol); + final ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); final byte[] pinToken = pin.getPinToken( TestData.PIN, ClientPin.PIN_PERMISSION_BE, "localhost"); - return new FingerprintBioEnrollment(session, pinUvAuthProtocol, pinToken); + return new FingerprintBioEnrollment(session, TestData.PIN_UV_AUTH_PROTOCOL, pinToken); } public static void renameFingerprint( diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java index 68cae8f6..0820504d 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2023 Yubico. + * Copyright (C) 2020-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,28 +25,19 @@ import com.yubico.yubikit.core.fido.CtapException; 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.PinUvAuthProtocolV1; import java.io.IOException; import java.util.Objects; public class Ctap2ClientPinTests { - - static PinUvAuthProtocol getPinUvAuthProtocol(Object... args) { - assertThat("Missing required argument: PinUvAuthProtocol", args.length > 0); - return (PinUvAuthProtocol) args[0]; - } - /** * Attempts to set (or verify) the default PIN, or fails. */ - static void ensureDefaultPinSet(Ctap2Session session, PinUvAuthProtocol pinUvAuthProtocol) - throws IOException, CommandException { + static void ensureDefaultPinSet(Ctap2Session session) throws IOException, CommandException { - Ctap2Session.InfoData info = session.getCachedInfo(); + Ctap2Session.InfoData info = session.getInfo(); - ClientPin pin = new ClientPin(session, pinUvAuthProtocol); + ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); if (!pinSet) { @@ -59,21 +50,16 @@ static void ensureDefaultPinSet(Ctap2Session session, PinUvAuthProtocol pinUvAut } } - public static void testSetPinProtocol(Ctap2Session session, Object... args) throws Throwable { - - assertThat("Missing required argument: PinUvAuthProtocol", args.length > 0); - - final PinUvAuthProtocol pinUvAuthProtocol = (PinUvAuthProtocol) args[0]; - - char[] otherPin = "123123".toCharArray(); + public static void testClientPin(Ctap2Session session) throws Throwable { + char[] otherPin = "12312312".toCharArray(); Integer permissions = ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA; String permissionRpId = "localhost"; - ensureDefaultPinSet(session, pinUvAuthProtocol); + // ensureDefaultPinSet(session); - ClientPin pin = new ClientPin(session, new PinUvAuthProtocolV1()); - assertThat(pin.getPinUvAuth().getVersion(), is(1)); + ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); + assertThat(pin.getPinUvAuth().getVersion(), is(TestData.PIN_UV_AUTH_PROTOCOL.getVersion())); assertThat(pin.getPinRetries().getCount(), is(8)); pin.changePin(TestData.PIN, otherPin); @@ -90,5 +76,4 @@ public static void testSetPinProtocol(Ctap2Session session, Object... args) thro assertThat(pin.getPinRetries().getCount(), is(8)); pin.changePin(otherPin, TestData.PIN); } - } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java index 2bbbb766..241b01be 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java @@ -21,47 +21,55 @@ import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import static java.lang.Boolean.TRUE; import com.yubico.yubikit.core.application.CommandException; import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.Config; import com.yubico.yubikit.fido.ctap.Ctap2Session; -import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import java.io.IOException; public class Ctap2ConfigTests { - static Config getConfig(Ctap2Session session, PinUvAuthProtocol pinUvAuthProtocol) throws IOException, CommandException { - Ctap2ClientPinTests.ensureDefaultPinSet(session, pinUvAuthProtocol); - ClientPin clientPin = new ClientPin(session, pinUvAuthProtocol); + static Config getConfig(Ctap2Session session) throws IOException, CommandException { + // Ctap2ClientPinTests.ensureDefaultPinSet(session); + ClientPin clientPin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); byte[] pinToken = clientPin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_ACFG, null); - return new Config(session, pinUvAuthProtocol, pinToken); + return new Config(session, TestData.PIN_UV_AUTH_PROTOCOL, pinToken); } - public static void testReadWriteEnterpriseAttestation(Ctap2Session session, Object... args) throws Throwable { - Config config = getConfig(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + public static void testReadWriteEnterpriseAttestation(Ctap2Session session) throws Throwable { + assumeTrue("Enterprise attestation not supported", + session.getInfo().getOptions().containsKey("ep")); + Config config = getConfig(session); config.enableEnterpriseAttestation(); assertSame(TRUE, session.getInfo().getOptions().get("ep")); } - public static void testToggleAlwaysUv(Ctap2Session session, Object... args) throws Throwable { - Config config = getConfig(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + public static void testToggleAlwaysUv(Ctap2Session session) throws Throwable { + assumeTrue("Device does not support alwaysUv", + session.getInfo().getOptions().containsKey("alwaysUv")); + Config config = getConfig(session); Object alwaysUv = getAlwaysUv(session); config.toggleAlwaysUv(); assertNotSame(getAlwaysUv(session), alwaysUv); } - public static void testSetForcePinChange(Ctap2Session session, Object... args) throws Throwable { + public static void testSetForcePinChange(Ctap2Session session) throws Throwable { + assumeTrue("authenticatorConfig not supported", + Config.isSupported(session.getCachedInfo())); assertFalse(session.getInfo().getForcePinChange()); - Config config = getConfig(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + Config config = getConfig(session); config.setMinPinLength(null, null, true); assertTrue(session.getInfo().getForcePinChange()); } - public static void testSetMinPinLength(Ctap2Session session, Object... args) throws Throwable { - Config config = getConfig(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + public static void testSetMinPinLength(Ctap2Session session) throws Throwable { + assumeTrue("authenticatorConfig not supported", + Config.isSupported(session.getCachedInfo())); + Config config = getConfig(session); // after calling this the key must be reset to get the default min pin length value config.setMinPinLength(50, null, null); assertEquals(50, session.getInfo().getMinPinLength()); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java index b3bb3f57..917c87a6 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java @@ -20,12 +20,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assume.assumeTrue; import com.yubico.yubikit.core.application.CommandException; import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.CredentialManagement; import com.yubico.yubikit.fido.ctap.Ctap2Session; -import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.webauthn.SerializationType; import java.io.IOException; @@ -52,12 +52,14 @@ public static void deleteAllCredentials(CredentialManagement credentialManagemen } private static CredentialManagement setupCredentialManagement( - Ctap2Session session, - PinUvAuthProtocol pinUvAuthProtocol + Ctap2Session session ) throws IOException, CommandException { - Ctap2ClientPinTests.ensureDefaultPinSet(session, pinUvAuthProtocol); - ClientPin clientPin = new ClientPin(session, pinUvAuthProtocol); + assumeTrue("Credential management not supported", + CredentialManagement.isSupported(session.getCachedInfo())); + + // Ctap2ClientPinTests.ensureDefaultPinSet(session); + ClientPin clientPin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); return new CredentialManagement( session, @@ -66,10 +68,8 @@ private static CredentialManagement setupCredentialManagement( ); } - public static void testReadMetadata(Ctap2Session session, Object... args) throws Throwable { - CredentialManagement credentialManagement = setupCredentialManagement( - session, - Ctap2ClientPinTests.getPinUvAuthProtocol(args)); + public static void testReadMetadata(Ctap2Session session) throws Throwable { + CredentialManagement credentialManagement = setupCredentialManagement(session); CredentialManagement.Metadata metadata = credentialManagement.getMetadata(); @@ -77,10 +77,9 @@ public static void testReadMetadata(Ctap2Session session, Object... args) throws assertThat(metadata.getMaxPossibleRemainingResidentCredentialsCount(), greaterThan(0)); } - public static void testManagement(Ctap2Session session, Object... args) throws Throwable { + public static void testManagement(Ctap2Session session) throws Throwable { - final PinUvAuthProtocol pinUvAuthProtocol = Ctap2ClientPinTests.getPinUvAuthProtocol(args); - CredentialManagement credentialManagement = setupCredentialManagement(session, pinUvAuthProtocol); + CredentialManagement credentialManagement = setupCredentialManagement(session); final SerializationType cborType = SerializationType.CBOR; @@ -101,14 +100,14 @@ public static void testManagement(Ctap2Session session, Object... args) throws T null, options, pinAuth, - pinUvAuthProtocol.getVersion(), + TestData.PIN_UV_AUTH_PROTOCOL.getVersion(), null, null ); // this sets correct permission for handling credential management commands - credentialManagement = setupCredentialManagement(session, pinUvAuthProtocol); + credentialManagement = setupCredentialManagement(session); List rps = credentialManagement.enumerateRps(); assertThat(rps.size(), equalTo(1)); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java index e7dd0941..758fac8d 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2023 Yubico. + * Copyright (C) 2020-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,13 +24,13 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; import com.yubico.yubikit.core.application.CommandState; import com.yubico.yubikit.core.fido.CtapException; 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.PinUvAuthProtocolV1; import com.yubico.yubikit.fido.webauthn.SerializationType; import java.util.Collections; @@ -68,17 +68,18 @@ public static void testCtap2GetInfo(Ctap2Session session, Object... ignoredArgs) assertTrue("PIN protocol incorrect", pinUvAuthProtocols.contains(1)); } - public static void testCancelCborCommandImmediate(Ctap2Session session, Object... args) throws Throwable { - doTestCancelCborCommand(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args), false); + public static void testCancelCborCommandImmediate(Ctap2Session session) throws Throwable { + doTestCancelCborCommand(session, false); } - public static void testCancelCborCommandAfterDelay(Ctap2Session session, Object... args) throws Throwable { - - doTestCancelCborCommand(session, Ctap2ClientPinTests.getPinUvAuthProtocol(args), true); + public static void testCancelCborCommandAfterDelay(Ctap2Session session) throws Throwable { + doTestCancelCborCommand(session, true); } - public static void testReset(Ctap2Session session, Object... ignoredArgs) throws Throwable { - // assumeThat("Connected over NFC", device, instanceOf(NfcYubiKeyDevice.class)); + public static void testReset(Ctap2Session session) throws Throwable { + assumeFalse("Skipping reset test - authenticator supports bio enrollment", + session.getCachedInfo().getOptions().containsKey("bioEnroll")); + session.reset(null); // Verify that the pin is no longer configured @@ -89,18 +90,21 @@ public static void testReset(Ctap2Session session, Object... ignoredArgs) throws private static void doTestCancelCborCommand( Ctap2Session session, - PinUvAuthProtocol pinUvAuthProtocol, boolean delay ) throws Throwable { - ensureDefaultPinSet(session, pinUvAuthProtocol); - ClientPin pin = new ClientPin(session, new PinUvAuthProtocolV1()); + assumeTrue("Not a USB connection", TestData.TRANSPORT_USB); + + // ensureDefaultPinSet(session); + + ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); byte[] pinToken = pin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_MC, TestData.RP.getId()); byte[] pinAuth = pin.getPinUvAuth().authenticate(pinToken, TestData.CLIENT_DATA_HASH); CommandState state = new CommandState(); if (delay) { - Executors.newSingleThreadScheduledExecutor().schedule(state::cancel, 500, TimeUnit.MILLISECONDS); + Executors.newSingleThreadScheduledExecutor() + .schedule(state::cancel, 500, TimeUnit.MILLISECONDS); } else { state.cancel(); } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java index 94a92e4f..9dcd3ac6 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import static java.lang.Boolean.FALSE; import com.yubico.yubikit.core.application.CommandException; @@ -28,7 +29,6 @@ import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.Config; import com.yubico.yubikit.fido.ctap.Ctap2Session; -import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.webauthn.AttestationConveyancePreference; import com.yubico.yubikit.fido.webauthn.AuthenticatorAttestationResponse; import com.yubico.yubikit.fido.webauthn.AuthenticatorResponse; @@ -49,24 +49,25 @@ @SuppressWarnings("unchecked") public class EnterpriseAttestationTests { - static void enableEp(Ctap2Session session, PinUvAuthProtocol pinUvAuthProtocol) + static void enableEp(Ctap2Session session) throws CommandException, IOException { // enable ep if not enabled if (session.getCachedInfo().getOptions().get("ep") == FALSE) { - ClientPin clientPin = new ClientPin(session, pinUvAuthProtocol); + ClientPin clientPin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); byte[] pinToken = clientPin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_ACFG, null); - final Config config = new Config(session, pinUvAuthProtocol, pinToken); + final Config config = new Config(session, TestData.PIN_UV_AUTH_PROTOCOL, pinToken); config.enableEnterpriseAttestation(); } } // test with RP ID in platform RP ID list - public static void testSupportedPlatformManagedEA(Ctap2Session session, Object... args) throws Throwable { - PinUvAuthProtocol pinUvAuthProtocol = Ctap2ClientPinTests.getPinUvAuthProtocol(args); - Ctap2ClientPinTests.ensureDefaultPinSet(session, pinUvAuthProtocol); - enableEp(session, pinUvAuthProtocol); + public static void testSupportedPlatformManagedEA(Ctap2Session session) throws Throwable { + assumeTrue("Enterprise attestation not supported", + session.getCachedInfo().getOptions().containsKey("ep")); + // // Ctap2ClientPinTests.ensureDefaultPinSet(session); + enableEp(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); webauthn.getUserAgentConfiguration().setEpSupportedRpIds(Collections.singletonList(TestData.RP_ID)); @@ -78,10 +79,11 @@ public static void testSupportedPlatformManagedEA(Ctap2Session session, Object.. } // test with RP ID which is not in platform RP ID list - public static void testUnsupportedPlatformManagedEA(Ctap2Session session, Object... args) throws Throwable { - PinUvAuthProtocol pinUvAuthProtocol = Ctap2ClientPinTests.getPinUvAuthProtocol(args); - Ctap2ClientPinTests.ensureDefaultPinSet(session, pinUvAuthProtocol); - enableEp(session, pinUvAuthProtocol); + public static void testUnsupportedPlatformManagedEA(Ctap2Session session) throws Throwable { + assumeTrue("Enterprise attestation not supported", + session.getCachedInfo().getOptions().containsKey("ep")); + // Ctap2ClientPinTests.ensureDefaultPinSet(session); + enableEp(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 2); @@ -92,10 +94,11 @@ public static void testUnsupportedPlatformManagedEA(Ctap2Session session, Object FALSE.equals(attestationObject.get("epAtt"))); } - public static void testVendorFacilitatedEA(Ctap2Session session, Object... args) throws Throwable { - PinUvAuthProtocol pinUvAuthProtocol = Ctap2ClientPinTests.getPinUvAuthProtocol(args); - Ctap2ClientPinTests.ensureDefaultPinSet(session, pinUvAuthProtocol); - enableEp(session, pinUvAuthProtocol); + public static void testVendorFacilitatedEA(Ctap2Session session) throws Throwable { + assumeTrue("Enterprise attestation not supported", + session.getCachedInfo().getOptions().containsKey("ep")); + // Ctap2ClientPinTests.ensureDefaultPinSet(session); + enableEp(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); webauthn.getUserAgentConfiguration().setEpSupportedRpIds(Collections.singletonList(TestData.RP_ID)); @@ -108,10 +111,11 @@ public static void testVendorFacilitatedEA(Ctap2Session session, Object... args) // test with different PublicKeyCredentialCreationOptions AttestationConveyancePreference // values - public static void testCreateOptionsAttestationPreference(Ctap2Session session, Object... args) throws Throwable { - PinUvAuthProtocol pinUvAuthProtocol = Ctap2ClientPinTests.getPinUvAuthProtocol(args); - Ctap2ClientPinTests.ensureDefaultPinSet(session, pinUvAuthProtocol); - enableEp(session, pinUvAuthProtocol); + public static void testCreateOptionsAttestationPreference(Ctap2Session session) throws Throwable { + assumeTrue("Enterprise attestation not supported", + session.getCachedInfo().getOptions().containsKey("ep")); + // Ctap2ClientPinTests.ensureDefaultPinSet(session); + enableEp(session); // setup BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestUtils.java new file mode 100644 index 00000000..2e6601c2 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestUtils.java @@ -0,0 +1,145 @@ +/* + * 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.fido; + +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.YubiKeyConnection; +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.smartcard.SmartCardConnection; +import com.yubico.yubikit.fido.ctap.Config; +import com.yubico.yubikit.fido.ctap.Ctap2Session; +import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; +import com.yubico.yubikit.management.Capability; +import com.yubico.yubikit.management.DeviceInfo; +import com.yubico.yubikit.management.ManagementSession; + +import java.io.IOException; +import java.util.List; + +public class FidoTestUtils { + public static void verifyAndSetup( + YubiKeyDevice device, + PinUvAuthProtocol pinUvAuthProtocol) + throws Throwable { + + boolean isFidoFipsCapable; + + try (YubiKeyConnection connection = openConnection(device)) { + + ManagementSession managementSession = getManagementSession(connection); + DeviceInfo deviceInfo = managementSession.getDeviceInfo(); + assertNotNull(deviceInfo); + + isFidoFipsCapable = + (deviceInfo.getFipsCapable() & Capability.FIDO2.bit) == Capability.FIDO2.bit; + + Ctap2Session session = getCtap2Session(connection); + assumeTrue( + "PIN UV Protocol not supported", + supportsPinUvAuthProtocol(session, pinUvAuthProtocol)); + + if (isFidoFipsCapable) { + assumeTrue("Ignoring FIPS tests which don't use PinUvAuthProtocolV2", + pinUvAuthProtocol.getVersion() == 2); + } + + TestData.PIN_UV_AUTH_PROTOCOL = pinUvAuthProtocol; + TestData.TRANSPORT_USB = device.getTransport() == Transport.USB; + +// // cannot reset over neither transport +// +// if (!TestData.TRANSPORT_USB) { +// // only reset FIDO over NFC +// session.reset(null); +// } + + // always set a PIN + Ctap2ClientPinTests.ensureDefaultPinSet(session); + + if (isFidoFipsCapable && + Boolean.FALSE.equals(session.getInfo().getOptions().get("alwaysUv"))) { + // set always UV on + Config config = Ctap2ConfigTests.getConfig(session); + config.toggleAlwaysUv(); + } + + deviceInfo = managementSession.getDeviceInfo(); + TestData.FIPS_APPROVED = + (deviceInfo.getFipsApproved() & Capability.FIDO2.bit) == Capability.FIDO2.bit; + + // after changing the user and admin PINs, we expect a FIPS capable device + // to be FIPS approved + if (isFidoFipsCapable) { + assertNotNull(deviceInfo); + assertTrue("Device not FIDO FIPS approved as expected", TestData.FIPS_APPROVED); + } + } + + } + + private static boolean supportsPinUvAuthProtocol( + Ctap2Session session, + PinUvAuthProtocol pinUvAuthProtocol) { + final List pinUvAuthProtocols = session.getCachedInfo().getPinUvAuthProtocols(); + return pinUvAuthProtocols.contains(pinUvAuthProtocol.getVersion()); + } + + private 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"); + } + + private static Ctap2Session getCtap2Session(YubiKeyConnection connection) + throws IOException, CommandException { + Ctap2Session session = (connection instanceof FidoConnection) + ? new Ctap2Session((FidoConnection) connection) + : connection instanceof SmartCardConnection + ? new Ctap2Session((SmartCardConnection) connection) + : null; + + if (session == null) { + throw new IllegalArgumentException("Connection does not support Ctap2Session"); + } + + return session; + } + + private static ManagementSession getManagementSession(YubiKeyConnection connection) throws IOException, CommandException { + ManagementSession session = (connection instanceof FidoConnection) + ? new ManagementSession((FidoConnection) connection) + : connection instanceof SmartCardConnection + ? new ManagementSession((SmartCardConnection) connection) + : 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/fido/TestData.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java index 1f266347..d0e6512d 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java @@ -17,6 +17,7 @@ package com.yubico.yubikit.testing.fido; import com.squareup.moshi.Moshi; +import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialParameters; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRpEntity; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialType; @@ -42,7 +43,12 @@ public ClientData(String type, String origin, byte[] challenge, String androidPa } } - public static final char[] PIN = "123456".toCharArray(); + // state + public static PinUvAuthProtocol PIN_UV_AUTH_PROTOCOL; + public static boolean TRANSPORT_USB = false; + public static boolean FIPS_APPROVED = false; + + public static final char[] PIN = "11234567".toCharArray(); public static final String RP_ID = "example.com"; public static final String RP_NAME = "Example Company"; From 135ed4eee387b1d57975964c047393934d9b69d9 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 15 Jul 2024 17:39:26 +0200 Subject: [PATCH 25/46] improve FIDO integration tests --- .../yubico/yubikit/testing/DeviceTests.java | 7 +- ...lDeviceTests.java => FastDeviceTests.java} | 2 - .../fido/Ctap2ConfigInstrumentedTests.java | 80 +++++++++++------ .../Ctap2SessionResetInstrumentedTests.java | 10 ++- .../yubikit/testing/fido/FidoTests.java | 19 +++- .../fido/UvDiscouragedInstrumentedTests.java | 21 ++++- .../yubikit/testing/openpgp/OpenPgpTests.java | 6 +- .../testing/piv/PivJcaProviderTests.java | 11 +-- .../testing/AlwaysManualTestCategory.java | 20 +++++ .../com/yubico/yubikit/testing/SlowTest.java | 20 +++++ .../framework/FidoInstrumentedTests.java | 8 +- .../fido/BasicWebAuthnClientTests.java | 43 +++------ .../testing/fido/Ctap2BioEnrollmentTests.java | 3 +- .../testing/fido/Ctap2ClientPinTests.java | 34 +------- .../testing/fido/Ctap2ConfigTests.java | 19 +++- .../fido/Ctap2CredentialManagementTests.java | 3 +- .../testing/fido/Ctap2SessionTests.java | 3 - .../fido/EnterpriseAttestationTests.java | 5 +- .../yubikit/testing/fido/FidoTestUtils.java | 87 ++++++++++++++++--- .../yubico/yubikit/testing/fido/TestData.java | 3 +- 20 files changed, 266 insertions(+), 138 deletions(-) rename testing-android/src/androidTest/java/com/yubico/yubikit/testing/{SmallDeviceTests.java => FastDeviceTests.java} (96%) create mode 100644 testing-android/src/main/java/com/yubico/yubikit/testing/AlwaysManualTestCategory.java create mode 100644 testing-android/src/main/java/com/yubico/yubikit/testing/SlowTest.java 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 b72581cc..421c435a 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 @@ -26,7 +26,12 @@ import org.junit.runners.Suite; /** - * All integration tests. + * All integration tests for PIV, OpenPGP and OATH. + *

+ * The YubiKey applications will be reset several times. + *

+ * FIDO integration tests cannot be ran in this suite and can be found in + * {@link com.yubico.yubikit.testing.fido.FidoTests} */ @RunWith(Suite.class) @Suite.SuiteClasses({ diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmallDeviceTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java similarity index 96% rename from testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmallDeviceTests.java rename to testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java index 0b0531d3..3499bc93 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmallDeviceTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java @@ -16,8 +16,6 @@ package com.yubico.yubikit.testing; -import androidx.test.filters.LargeTest; - import org.junit.experimental.categories.Categories; import org.junit.runner.RunWith; import org.junit.runners.Suite; diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java index fece1025..5b23f4ce 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java @@ -16,43 +16,67 @@ package com.yubico.yubikit.testing.fido; +import com.yubico.yubikit.testing.AlwaysManualTestCategory; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; +import org.junit.experimental.categories.Categories; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; /** - * NOTE: Run the testcases in this suite manually one by one. See test case documentation - * and reset the FIDO application where needed. + * Config tests. + *

+ * These tests will change FIDO2 application configuration through authenticatorConfig. As these changes + * are irreversible. + *

+ * Read documentation for each test for more information. */ -public class Ctap2ConfigInstrumentedTests extends FidoInstrumentedTests { +@RunWith(Categories.class) +@Suite.SuiteClasses(Ctap2ConfigInstrumentedTests.ConfigTests.class) +@Categories.ExcludeCategory(AlwaysManualTestCategory.class) +public class Ctap2ConfigInstrumentedTests { - @Test - public void testReadWriteEnterpriseAttestation() throws Throwable { - withCtap2Session(Ctap2ConfigTests::testReadWriteEnterpriseAttestation); - } + public static class ConfigTests extends FidoInstrumentedTests { + @Test + public void testReadWriteEnterpriseAttestation() throws Throwable { + withCtap2Session(Ctap2ConfigTests::testReadWriteEnterpriseAttestation); + } - @Test - public void testToggleAlwaysUv() throws Throwable { - withCtap2Session(Ctap2ConfigTests::testToggleAlwaysUv); - } + /** + * Toggles the {@code alwaysUv} option to opposite value. It is not possible to set this + * option to `false` on a FIPS approved YubiKey. + * + * @throws Throwable if an error occurs + */ + @Test + @Category(AlwaysManualTestCategory.class) + public void testToggleAlwaysUv() throws Throwable { + withCtap2Session(Ctap2ConfigTests::testToggleAlwaysUv); + } - /** - * Reset the FIDO application after calling this test case. - * - * @throws Throwable on any error - */ - @Test - public void testSetForcePinChange() throws Throwable { - withCtap2Session(Ctap2ConfigTests::testSetForcePinChange); - } + /** + * Sets the {@code forcePinChange} flag, verifies that and then changes the PIN twice so + * that the device uses the {@code TestUtil.PIN}. + * + * @throws Throwable if an error occurs + */ + @Test + public void testSetForcePinChange() throws Throwable { + withCtap2Session(Ctap2ConfigTests::testSetForcePinChange); + } - /** - * Reset the FIDO application after calling this test case. - * - * @throws Throwable on any error - */ - @Test - public void testSetMinPinLength() throws Throwable { - withCtap2Session(Ctap2ConfigTests::testSetMinPinLength); + /** + * Changes the {@code minPinLength} value. This change is irreversible and after running + * this test, the YubiKey should be reset. + * + * @throws Throwable if an error occurs + */ + @Test + @Category(AlwaysManualTestCategory.class) + public void testSetMinPinLength() throws Throwable { + withCtap2Session(Ctap2ConfigTests::testSetMinPinLength); + } } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java index 51217e27..adf25c08 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java @@ -16,22 +16,24 @@ package com.yubico.yubikit.testing.fido; +import com.yubico.yubikit.testing.AlwaysManualTestCategory; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; +import org.junit.experimental.categories.Category; /** * Tests FIDO Reset. *

- * Notes: + * This is a manual test which will reset the FIDO application. *

    - *
  • The tests for different protocols are meant to be ran separately.
  • - *
  • Before running any of the tests, disconnect the security Key from the device
  • - *
  • Bio devices are currently ignored.
  • + *
  • Before running the test, disconnect the YubiKey from the Android device.
  • + *
  • YubiKey Bio devices are currently ignored.
  • *
*/ public class Ctap2SessionResetInstrumentedTests extends FidoInstrumentedTests { @Test + @Category(AlwaysManualTestCategory.class) public void testReset() throws Throwable { withCtap2Session(Ctap2SessionTests::testReset); } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java index a0de1c12..7b8c8b5f 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java @@ -16,20 +16,31 @@ package com.yubico.yubikit.testing.fido; +import com.yubico.yubikit.testing.AlwaysManualTestCategory; + +import org.junit.experimental.categories.Categories; import org.junit.runner.RunWith; import org.junit.runners.Suite; -@RunWith(Suite.class) +/** + * Setup YubiKey before running the integration tests: + *
    + *
  • reset the FIDO application
  • + *
  • optionally set PIN to `11234567`
  • + *
+ */ +@RunWith(Categories.class) @Suite.SuiteClasses({ BasicWebAuthnClientInstrumentedTests.class, - Ctap2BioEnrollmentInstrumentedTests.class, Ctap2ClientPinInstrumentedTests.class, - Ctap2ConfigInstrumentedTests.class, Ctap2CredentialManagementInstrumentedTests.class, Ctap2SessionInstrumentedTests.class, - Ctap2SessionResetInstrumentedTests.class, EnterpriseAttestationInstrumentedTests.class, UvDiscouragedInstrumentedTests.class, + Ctap2ConfigInstrumentedTests.class, + Ctap2BioEnrollmentInstrumentedTests.class, + Ctap2SessionResetInstrumentedTests.class, }) +@Categories.ExcludeCategory(AlwaysManualTestCategory.class) public class FidoTests { } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java index f8b077fd..a61996e8 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java @@ -17,22 +17,35 @@ package com.yubico.yubikit.testing.fido; import com.yubico.yubikit.fido.client.PinRequiredClientError; +import com.yubico.yubikit.testing.AlwaysManualTestCategory; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; +import org.junit.experimental.categories.Category; public class UvDiscouragedInstrumentedTests extends FidoInstrumentedTests { + /** + * Reset the FIDO application before running this test. + *

+ * The test will make credential/get assertion without using the PIN which is acceptable for + * {@code UserVerificationRequirement.DISCOURAGED}. + *

+ * Skipped on FIPS approved devices. + */ @Test + @Category(AlwaysManualTestCategory.class) public void testMakeCredentialGetAssertion() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testUvDiscouragedMakeCredentialGetAssertionWithoutPin); + withCtap2Session(BasicWebAuthnClientTests::testUvDiscouragedMcGa_noPin, false); } /** - * Run this test only on devices with PIN set + * This test will make credential without passing PIN value on a device which is protected by + * PIN. + *

* Expected to fail with PinRequiredClientError */ @Test(expected = PinRequiredClientError.class) - public void testMakeCredentialGetAssertionOnProtected() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testUvDiscouragedMakeCredentialGetAssertionWithPin); + public void testMakeCredentialGetAssertionWithPin() throws Throwable { + withCtap2Session(BasicWebAuthnClientTests::testUvDiscouragedMcGa_withPin); } } 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 e63b0f11..d7f976f4 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 @@ -18,12 +18,12 @@ import javax.annotation.Nullable; -import androidx.test.filters.LargeTest; - import com.yubico.yubikit.core.smartcard.scp.ScpKid; +import com.yubico.yubikit.testing.SlowTest; import com.yubico.yubikit.testing.framework.OpenPgpInstrumentedTests; import org.junit.Test; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -77,7 +77,7 @@ public void testSetPinAttempts() throws Throwable { } @Test - @LargeTest + @Category(SlowTest.class) public void testGenerateRsaKeys() throws Throwable { withOpenPgpSession(OpenPgpDeviceTests::testGenerateRsaKeys); } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java index fab6ba5a..164ac4bd 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java @@ -19,12 +19,13 @@ import javax.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.LargeTest; import com.yubico.yubikit.core.smartcard.scp.ScpKid; +import com.yubico.yubikit.testing.SlowTest; import com.yubico.yubikit.testing.framework.PivInstrumentedTests; import org.junit.Test; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @@ -32,13 +33,13 @@ public class PivJcaProviderTests { public static class NoScpTests extends PivInstrumentedTests { @Test - @LargeTest + @Category(SlowTest.class) public void testGenerateKeys() throws Throwable { withPivSession(PivJcaDeviceTests::testGenerateKeys); } @Test - @LargeTest + @Category(SlowTest.class) public void testGenerateKeysPreferBC() throws Throwable { withPivSession(PivJcaDeviceTests::testGenerateKeysPreferBC); } @@ -50,13 +51,13 @@ public void testImportKeys() throws Throwable { } @Test - @LargeTest + @Category(SlowTest.class) public void testSigning() throws Throwable { withPivSession(PivJcaSigningTests::testSign); } @Test - @LargeTest + @Category(SlowTest.class) public void testDecrypt() throws Throwable { withPivSession(PivJcaDecryptTests::testDecrypt); } diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/AlwaysManualTestCategory.java b/testing-android/src/main/java/com/yubico/yubikit/testing/AlwaysManualTestCategory.java new file mode 100644 index 00000000..7c40dfbc --- /dev/null +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/AlwaysManualTestCategory.java @@ -0,0 +1,20 @@ +/* + * 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; + +public interface AlwaysManualTestCategory { +} diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/SlowTest.java b/testing-android/src/main/java/com/yubico/yubikit/testing/SlowTest.java new file mode 100644 index 00000000..2ce9ac4d --- /dev/null +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/SlowTest.java @@ -0,0 +1,20 @@ +/* + * 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; + +public interface SlowTest { +} diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java index b505e70c..d7e358fb 100755 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java @@ -24,14 +24,20 @@ import java.util.Optional; import java.util.concurrent.LinkedBlockingQueue; +import javax.annotation.Nullable; + public class FidoInstrumentedTests extends YKInstrumentedTests { public interface Callback { void invoke(Ctap2Session session) throws Throwable; } protected void withCtap2Session(Callback callback) throws Throwable { + withCtap2Session(callback, true); + } + + protected void withCtap2Session(Callback callback, boolean setPin) throws Throwable { - FidoTestUtils.verifyAndSetup(device, getPinUvAuthProtocol()); + FidoTestUtils.verifyAndSetup(device, getPinUvAuthProtocol(), setPin); LinkedBlockingQueue> result = new LinkedBlockingQueue<>(); Ctap2Session.create(device, value -> { diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java index c9ba3034..854ef699 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java @@ -77,11 +77,6 @@ public static void testMakeCredentialGetAssertionTokenUvOnly(Ctap2Session sessio } public static void testMakeCredentialGetAssertion(Ctap2Session session) throws Throwable { - - // // Ctap2ClientPinTests.ensureDefaultPinSet(session); - - char[] pin = TestData.PIN; - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); List deleteCredIds = new ArrayList<>(); @@ -100,7 +95,7 @@ public static void testMakeCredentialGetAssertion(Ctap2Session session) throws T TestData.CLIENT_DATA_JSON_CREATE, creationOptionsNonRk, Objects.requireNonNull(creationOptionsNonRk.getRp().getId()), - pin, + TestData.PIN, null, null ); @@ -123,7 +118,7 @@ public static void testMakeCredentialGetAssertion(Ctap2Session session) throws T TestData.CLIENT_DATA_JSON_CREATE, creationOptionsRk, Objects.requireNonNull(creationOptionsRk.getRp().getId()), - pin, + TestData.PIN, null, null ); @@ -148,7 +143,7 @@ public static void testMakeCredentialGetAssertion(Ctap2Session session) throws T TestData.CLIENT_DATA_JSON_GET, requestOptions, TestData.RP_ID, - pin, + TestData.PIN, null ); AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) credential.getResponse(); @@ -162,19 +157,21 @@ public static void testMakeCredentialGetAssertion(Ctap2Session session) throws T deleteCredentials(webauthn, deleteCredIds); } - public static void testUvDiscouragedMakeCredentialGetAssertionWithPin(Ctap2Session session) throws Throwable { - assumeTrue("Device has a PIN set", + public static void testUvDiscouragedMcGa_withPin(Ctap2Session session) throws Throwable { + assumeTrue("Device has no PIN set", Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); testUvDiscouragedMakeCredentialGetAssertion(session); } - public static void testUvDiscouragedMakeCredentialGetAssertionWithoutPin(Ctap2Session session) throws Throwable { - assumeFalse("Device has no PIN set", + public static void testUvDiscouragedMcGa_noPin(Ctap2Session session) throws Throwable { + assumeFalse("Device has PIN set. Reset and try again.", Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); + assumeFalse("Ignoring FIPS approved devices", TestData.FIPS_APPROVED); testUvDiscouragedMakeCredentialGetAssertion(session); } - private static void testUvDiscouragedMakeCredentialGetAssertion(Ctap2Session session) throws Throwable { + private static void testUvDiscouragedMakeCredentialGetAssertion(Ctap2Session session) + throws Throwable { BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); @@ -231,7 +228,6 @@ private static void testUvDiscouragedMakeCredentialGetAssertion(Ctap2Session ses fail("Got MultipleAssertionsAvailable even though there should only be one credential"); } - // test rk credential PublicKeyCredentialCreationOptions creationOptionsRk = getCreateOptions( new PublicKeyCredentialUserEntity( @@ -286,9 +282,6 @@ private static void testUvDiscouragedMakeCredentialGetAssertion(Ctap2Session ses } public static void testGetAssertionMultipleUsersRk(Ctap2Session session) throws Throwable { - - // Ctap2ClientPinTests.ensureDefaultPinSet(session); - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); List deleteCredIds = new ArrayList<>(); @@ -367,8 +360,6 @@ public static void testGetAssertionMultipleUsersRk(Ctap2Session session) throws public static void testGetAssertionWithAllowList(Ctap2Session session) throws Throwable { - // Ctap2ClientPinTests.ensureDefaultPinSet(session); - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); // Make 2 new credentials @@ -457,8 +448,6 @@ public static void testGetAssertionWithAllowList(Ctap2Session session) throws Th public static void testMakeCredentialWithExcludeList(Ctap2Session session) throws Throwable { - // Ctap2ClientPinTests.ensureDefaultPinSet(session); - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); List excludeList = new ArrayList<>(); @@ -525,7 +514,6 @@ public static void testMakeCredentialWithExcludeList(Ctap2Session session) throw } public static void testMakeCredentialKeyAlgorithms(Ctap2Session session) throws Throwable { - // Ctap2ClientPinTests.ensureDefaultPinSet(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); List allCredParams = Arrays.asList( TestData.PUB_KEY_CRED_PARAMS_ES256, @@ -601,18 +589,14 @@ public static void testMakeCredentialKeyAlgorithms(Ctap2Session session) throws } public static void testClientPinManagement(Ctap2Session session) throws Throwable { - // Ctap2ClientPinTests.ensureDefaultPinSet(session); - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); assertTrue(webauthn.isPinSupported()); assertTrue(webauthn.isPinConfigured()); - char[] otherPin = "123123".toCharArray(); - - webauthn.changePin(TestData.PIN, otherPin); + webauthn.changePin(TestData.PIN, TestData.OTHER_PIN); try { - webauthn.changePin(TestData.PIN, otherPin); + webauthn.changePin(TestData.PIN, TestData.OTHER_PIN); fail("Wrong PIN was accepted"); } catch (ClientError e) { assertThat(e.getErrorCode(), equalTo(ClientError.Code.BAD_REQUEST)); @@ -621,14 +605,13 @@ public static void testClientPinManagement(Ctap2Session session) throws Throwabl is(CtapException.ERR_PIN_INVALID)); } - webauthn.changePin(otherPin, TestData.PIN); + webauthn.changePin(TestData.OTHER_PIN, TestData.PIN); } public static void testClientCredentialManagement(Ctap2Session session) throws Throwable { assumeTrue("Credential management not supported", CredentialManagement.isSupported(session.getCachedInfo())); - // Ctap2ClientPinTests.ensureDefaultPinSet(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); PublicKeyCredentialCreationOptions creationOptions = getCreateOptions(null, true, Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java index ce5c9ed7..3a6ec960 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java @@ -16,7 +16,6 @@ package com.yubico.yubikit.testing.fido; -import static com.yubico.yubikit.testing.fido.Ctap2ClientPinTests.ensureDefaultPinSet; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -42,7 +41,7 @@ public class Ctap2BioEnrollmentTests { public static void testFingerprintEnrollment(Ctap2Session session) throws Throwable { - assumeTrue(" Bio enrollment not supported", + assumeTrue("Bio enrollment not supported", BioEnrollment.isSupported(session.getCachedInfo())); final FingerprintBioEnrollment fingerprintBioEnrollment = fpBioEnrollment(session); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java index 0820504d..8a848f3a 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java @@ -21,48 +21,20 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.fail; -import com.yubico.yubikit.core.application.CommandException; import com.yubico.yubikit.core.fido.CtapException; import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.Ctap2Session; -import java.io.IOException; -import java.util.Objects; - public class Ctap2ClientPinTests { - /** - * Attempts to set (or verify) the default PIN, or fails. - */ - static void ensureDefaultPinSet(Ctap2Session session) throws IOException, CommandException { - - Ctap2Session.InfoData info = session.getInfo(); - - ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); - boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); - - if (!pinSet) { - pin.setPin(TestData.PIN); - } else { - pin.getPinToken( - TestData.PIN, - ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA, - "localhost"); - } - } - public static void testClientPin(Ctap2Session session) throws Throwable { - char[] otherPin = "12312312".toCharArray(); - Integer permissions = ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA; String permissionRpId = "localhost"; - // ensureDefaultPinSet(session); - ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); assertThat(pin.getPinUvAuth().getVersion(), is(TestData.PIN_UV_AUTH_PROTOCOL.getVersion())); assertThat(pin.getPinRetries().getCount(), is(8)); - pin.changePin(TestData.PIN, otherPin); + pin.changePin(TestData.PIN, TestData.OTHER_PIN); try { pin.getPinToken(TestData.PIN, permissions, permissionRpId); fail("Wrong PIN was accepted"); @@ -72,8 +44,8 @@ public static void testClientPin(Ctap2Session session) throws Throwable { } assertThat(pin.getPinRetries().getCount(), is(7)); - assertThat(pin.getPinToken(otherPin, permissions, permissionRpId), notNullValue()); + assertThat(pin.getPinToken(TestData.OTHER_PIN, permissions, permissionRpId), notNullValue()); assertThat(pin.getPinRetries().getCount(), is(8)); - pin.changePin(otherPin, TestData.PIN); + pin.changePin(TestData.OTHER_PIN, TestData.PIN); } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java index 241b01be..f7b2d077 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java @@ -16,11 +16,14 @@ package com.yubico.yubikit.testing.fido; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import static java.lang.Boolean.TRUE; @@ -34,7 +37,6 @@ public class Ctap2ConfigTests { static Config getConfig(Ctap2Session session) throws IOException, CommandException { - // Ctap2ClientPinTests.ensureDefaultPinSet(session); ClientPin clientPin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); byte[] pinToken = clientPin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_ACFG, null); return new Config(session, TestData.PIN_UV_AUTH_PROTOCOL, pinToken); @@ -60,10 +62,23 @@ public static void testToggleAlwaysUv(Ctap2Session session) throws Throwable { public static void testSetForcePinChange(Ctap2Session session) throws Throwable { assumeTrue("authenticatorConfig not supported", Config.isSupported(session.getCachedInfo())); - assertFalse(session.getInfo().getForcePinChange()); + assumeFalse("Force PIN change already set. Reset key and retry", session.getInfo().getForcePinChange()); Config config = getConfig(session); config.setMinPinLength(null, null, true); assertTrue(session.getInfo().getForcePinChange()); + + // set a new PIN + ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); + assertThat(pin.getPinUvAuth().getVersion(), is(TestData.PIN_UV_AUTH_PROTOCOL.getVersion())); + assertThat(pin.getPinRetries().getCount(), is(8)); + + pin.changePin(TestData.PIN, TestData.OTHER_PIN); + assertFalse(session.getInfo().getForcePinChange()); + + // set to a default PIN + pin.changePin(TestData.OTHER_PIN, TestData.PIN); + assertFalse(session.getInfo().getForcePinChange()); + } public static void testSetMinPinLength(Ctap2Session session) throws Throwable { diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java index 917c87a6..d4871bd2 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2023 Yubico. + * Copyright (C) 2020-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,7 +58,6 @@ private static CredentialManagement setupCredentialManagement( assumeTrue("Credential management not supported", CredentialManagement.isSupported(session.getCachedInfo())); - // Ctap2ClientPinTests.ensureDefaultPinSet(session); ClientPin clientPin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); return new CredentialManagement( diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java index 758fac8d..239ae70e 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java @@ -16,7 +16,6 @@ package com.yubico.yubikit.testing.fido; -import static com.yubico.yubikit.testing.fido.Ctap2ClientPinTests.ensureDefaultPinSet; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; @@ -95,8 +94,6 @@ private static void doTestCancelCborCommand( assumeTrue("Not a USB connection", TestData.TRANSPORT_USB); - // ensureDefaultPinSet(session); - ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); byte[] pinToken = pin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_MC, TestData.RP.getId()); byte[] pinAuth = pin.getPinUvAuth().authenticate(pinToken, TestData.CLIENT_DATA_HASH); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java index 9dcd3ac6..d02d6bd8 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2023 Yubico. + * Copyright (C) 2020-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,6 @@ import javax.annotation.Nullable; -@SuppressWarnings("unchecked") public class EnterpriseAttestationTests { static void enableEp(Ctap2Session session) @@ -66,7 +65,6 @@ static void enableEp(Ctap2Session session) public static void testSupportedPlatformManagedEA(Ctap2Session session) throws Throwable { assumeTrue("Enterprise attestation not supported", session.getCachedInfo().getOptions().containsKey("ep")); - // // Ctap2ClientPinTests.ensureDefaultPinSet(session); enableEp(session); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); webauthn.getUserAgentConfiguration().setEpSupportedRpIds(Collections.singletonList(TestData.RP_ID)); @@ -183,6 +181,7 @@ private static PublicKeyCredentialCreationOptions getCredentialCreationOptions( /** * Helper method which extracts AuthenticatorAttestationResponse from the credential */ + @SuppressWarnings("unchecked") private static Map getAttestationObject(AuthenticatorResponse response) { AuthenticatorAttestationResponse authenticatorAttestationResponse = (AuthenticatorAttestationResponse) response; diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestUtils.java index 2e6601c2..128176a2 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestUtils.java @@ -16,8 +16,10 @@ package com.yubico.yubikit.testing.fido; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import com.yubico.yubikit.core.Transport; @@ -26,20 +28,41 @@ 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.fido.client.BasicWebAuthnClient; +import com.yubico.yubikit.fido.client.ClientError; +import com.yubico.yubikit.fido.client.CredentialManager; +import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.Config; import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; +import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor; +import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialUserEntity; import com.yubico.yubikit.management.Capability; import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.management.ManagementSession; import java.io.IOException; import java.util.List; +import java.util.Map; +import java.util.Objects; public class FidoTestUtils { + + /** + * Prepares the device for a test and sets variables in {@link TestData}. If {@code setPin} + * is true, then sets the FIDO2 PIN, verifies FIPS approval state and removes existing + * credentials. + * + * @param device The YubiKey which should be setup for a test. + * @param pinUvAuthProtocol Pin UV/Auth protocol to use during the test. + * @param setPin Whether a FIDO2 PIN should be set. Several tests need a device without a PIN. + * + * @throws Throwable if an error occurred + */ public static void verifyAndSetup( YubiKeyDevice device, - PinUvAuthProtocol pinUvAuthProtocol) + PinUvAuthProtocol pinUvAuthProtocol, + boolean setPin) throws Throwable { boolean isFidoFipsCapable; @@ -66,15 +89,9 @@ public static void verifyAndSetup( TestData.PIN_UV_AUTH_PROTOCOL = pinUvAuthProtocol; TestData.TRANSPORT_USB = device.getTransport() == Transport.USB; -// // cannot reset over neither transport -// -// if (!TestData.TRANSPORT_USB) { -// // only reset FIDO over NFC -// session.reset(null); -// } - - // always set a PIN - Ctap2ClientPinTests.ensureDefaultPinSet(session); + if (setPin) { + verifyOrSetPin(session); + } if (isFidoFipsCapable && Boolean.FALSE.equals(session.getInfo().getOptions().get("alwaysUv"))) { @@ -83,18 +100,24 @@ public static void verifyAndSetup( config.toggleAlwaysUv(); } + managementSession = getManagementSession(connection); deviceInfo = managementSession.getDeviceInfo(); TestData.FIPS_APPROVED = (deviceInfo.getFipsApproved() & Capability.FIDO2.bit) == Capability.FIDO2.bit; // after changing the user and admin PINs, we expect a FIPS capable device // to be FIPS approved - if (isFidoFipsCapable) { + if (setPin && isFidoFipsCapable) { assertNotNull(deviceInfo); assertTrue("Device not FIDO FIPS approved as expected", TestData.FIPS_APPROVED); } - } + // remove existing credentials + if (setPin) { + // cannot use CredentialManager if there is no PIN set + deleteExistingCredentials(session); + } + } } private static boolean supportsPinUvAuthProtocol( @@ -142,4 +165,44 @@ private static ManagementSession getManagementSession(YubiKeyConnection connecti return session; } + + private static void deleteExistingCredentials(Ctap2Session session) + throws IOException, CommandException, ClientError { + final BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + final CredentialManager credentialManager = webauthn.getCredentialManager(TestData.PIN); + final List rpIds = credentialManager.getRpIdList(); + for (String rpId : rpIds) { + Map credentials + = credentialManager.getCredentials(rpId); + for (PublicKeyCredentialDescriptor credential : credentials.keySet()) { + credentialManager.deleteCredential(credential); + } + } + assertEquals("Failed to remove all credentials", 0, credentialManager.getCredentialCount()); + } + + /** + * Attempts to set (or verify) the default PIN, or fails. + */ + private static void verifyOrSetPin(Ctap2Session session) throws IOException, CommandException { + + Ctap2Session.InfoData info = session.getInfo(); + + ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); + boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); + + try { + if (!pinSet) { + pin.setPin(TestData.PIN); + } else { + pin.getPinToken( + TestData.PIN, + ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA, + "localhost"); + } + } catch (CommandException e) { + fail("YubiKey cannot be used for test, failed to set/verify PIN. Please reset " + + "and try again."); + } + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java index d0e6512d..69d5b8b1 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2023 Yubico. + * Copyright (C) 2020-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,7 @@ public ClientData(String type, String origin, byte[] challenge, String androidPa public static boolean FIPS_APPROVED = false; public static final char[] PIN = "11234567".toCharArray(); + public static final char[] OTHER_PIN = "11231234".toCharArray(); public static final String RP_ID = "example.com"; public static final String RP_NAME = "Example Company"; From df7e8d623b8c0a8a9b48aa2a3058bb5c333e03cf Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 22 Jul 2024 10:20:15 +0200 Subject: [PATCH 26/46] batter category names --- .../java/com/yubico/yubikit/testing/FastDeviceTests.java | 8 +++++++- .../fido/BasicWebAuthnClientInstrumentedTests.java | 4 ++-- .../testing/fido/Ctap2ClientPinInstrumentedTests.java | 4 ++-- .../testing/fido/Ctap2ConfigInstrumentedTests.java | 8 ++++---- .../fido/Ctap2CredentialManagementInstrumentedTests.java | 4 ++-- .../testing/fido/Ctap2SessionInstrumentedTests.java | 4 ++-- .../testing/fido/Ctap2SessionResetInstrumentedTests.java | 4 ++-- .../fido/EnterpriseAttestationInstrumentedTests.java | 4 ++-- .../java/com/yubico/yubikit/testing/fido/FidoTests.java | 4 ++-- .../testing/fido/UvDiscouragedInstrumentedTests.java | 4 ++-- ...lwaysManualTestCategory.java => AlwaysManualTest.java} | 2 +- ...otocolV1Category.java => PinUvAuthProtocolV1Test.java} | 2 +- 12 files changed, 29 insertions(+), 23 deletions(-) rename testing-android/src/main/java/com/yubico/yubikit/testing/{AlwaysManualTestCategory.java => AlwaysManualTest.java} (93%) rename testing-android/src/main/java/com/yubico/yubikit/testing/{PinUvAuthProtocolV1Category.java => PinUvAuthProtocolV1Test.java} (93%) diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java index 3499bc93..1c49ae70 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/FastDeviceTests.java @@ -24,10 +24,16 @@ * These tests are here to make testing a bit faster and exclude following: *

    *
  • {@link SlowTest}
  • + *
  • {@link PinUvAuthProtocolV1Test}
  • + *
  • {@link AlwaysManualTest}
  • *
*/ @RunWith(Categories.class) @Suite.SuiteClasses(DeviceTests.class) -@Categories.ExcludeCategory(SlowTest.class) +@Categories.ExcludeCategory({ + SlowTest.class, + PinUvAuthProtocolV1Test.class, + AlwaysManualTest.class +}) public class FastDeviceTests { } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java index c0d960a8..a37aa4b5 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java @@ -18,7 +18,7 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; -import com.yubico.yubikit.testing.PinUvAuthProtocolV1Category; +import com.yubico.yubikit.testing.PinUvAuthProtocolV1Test; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -74,7 +74,7 @@ public void testClientCredentialManagement() throws Throwable { } } - @Category(PinUvAuthProtocolV1Category.class) + @Category(PinUvAuthProtocolV1Test.class) public static class PinUvAuthV1Test extends PinUvAuthV2Test { @Override protected PinUvAuthProtocol getPinUvAuthProtocol() { 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 63b5a8e2..bd9ba900 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 @@ -18,7 +18,7 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; -import com.yubico.yubikit.testing.PinUvAuthProtocolV1Category; +import com.yubico.yubikit.testing.PinUvAuthProtocolV1Test; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -39,7 +39,7 @@ public void testClientPin() throws Throwable { } } - @Category(PinUvAuthProtocolV1Category.class) + @Category(PinUvAuthProtocolV1Category.class) public static class PinUvAuthV1Test extends PinUvAuthV2Test { @Override protected PinUvAuthProtocol getPinUvAuthProtocol() { diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java index 5b23f4ce..70f23a17 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2ConfigInstrumentedTests.java @@ -16,7 +16,7 @@ package com.yubico.yubikit.testing.fido; -import com.yubico.yubikit.testing.AlwaysManualTestCategory; +import com.yubico.yubikit.testing.AlwaysManualTest; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -35,7 +35,7 @@ */ @RunWith(Categories.class) @Suite.SuiteClasses(Ctap2ConfigInstrumentedTests.ConfigTests.class) -@Categories.ExcludeCategory(AlwaysManualTestCategory.class) +@Categories.ExcludeCategory(AlwaysManualTest.class) public class Ctap2ConfigInstrumentedTests { public static class ConfigTests extends FidoInstrumentedTests { @@ -51,7 +51,7 @@ public void testReadWriteEnterpriseAttestation() throws Throwable { * @throws Throwable if an error occurs */ @Test - @Category(AlwaysManualTestCategory.class) + @Category(AlwaysManualTest.class) public void testToggleAlwaysUv() throws Throwable { withCtap2Session(Ctap2ConfigTests::testToggleAlwaysUv); } @@ -74,7 +74,7 @@ public void testSetForcePinChange() throws Throwable { * @throws Throwable if an error occurs */ @Test - @Category(AlwaysManualTestCategory.class) + @Category(AlwaysManualTest.class) public void testSetMinPinLength() throws Throwable { withCtap2Session(Ctap2ConfigTests::testSetMinPinLength); } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java index f92ea0c3..71bab164 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java @@ -18,7 +18,7 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; -import com.yubico.yubikit.testing.PinUvAuthProtocolV1Category; +import com.yubico.yubikit.testing.PinUvAuthProtocolV1Test; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -44,7 +44,7 @@ public void testManagement() throws Throwable { } } - @Category(PinUvAuthProtocolV1Category.class) + @Category(PinUvAuthProtocolV1Test.class) public static class PinUvAuthV1Test extends PinUvAuthV2Test { @Override protected PinUvAuthProtocol getPinUvAuthProtocol() { diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.java index ce287225..31d215de 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.java @@ -18,7 +18,7 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; -import com.yubico.yubikit.testing.PinUvAuthProtocolV1Category; +import com.yubico.yubikit.testing.PinUvAuthProtocolV1Test; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -49,7 +49,7 @@ public void testCancelCborCommandAfterDelay() throws Throwable { } } - @Category(PinUvAuthProtocolV1Category.class) + @Category(PinUvAuthProtocolV1Test.class) public static class PinUvAuthV1Test extends PinUvAuthV2Test { @Override protected PinUvAuthProtocol getPinUvAuthProtocol() { diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java index adf25c08..3c820e5d 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java @@ -16,7 +16,7 @@ package com.yubico.yubikit.testing.fido; -import com.yubico.yubikit.testing.AlwaysManualTestCategory; +import com.yubico.yubikit.testing.AlwaysManualTest; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -33,7 +33,7 @@ */ public class Ctap2SessionResetInstrumentedTests extends FidoInstrumentedTests { @Test - @Category(AlwaysManualTestCategory.class) + @Category(AlwaysManualTest.class) public void testReset() throws Throwable { withCtap2Session(Ctap2SessionTests::testReset); } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java index c856e6f2..6977789a 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java @@ -18,7 +18,7 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; -import com.yubico.yubikit.testing.PinUvAuthProtocolV1Category; +import com.yubico.yubikit.testing.PinUvAuthProtocolV1Test; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -54,7 +54,7 @@ public void testVendorFacilitatedEA() throws Throwable { } } - @Category(PinUvAuthProtocolV1Category.class) + @Category(PinUvAuthProtocolV1Test.class) public static class PinUvAuthV1Test extends PinUvAuthV2Test { @Override protected PinUvAuthProtocol getPinUvAuthProtocol() { diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java index 7b8c8b5f..d246b60b 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/FidoTests.java @@ -16,7 +16,7 @@ package com.yubico.yubikit.testing.fido; -import com.yubico.yubikit.testing.AlwaysManualTestCategory; +import com.yubico.yubikit.testing.AlwaysManualTest; import org.junit.experimental.categories.Categories; import org.junit.runner.RunWith; @@ -41,6 +41,6 @@ Ctap2BioEnrollmentInstrumentedTests.class, Ctap2SessionResetInstrumentedTests.class, }) -@Categories.ExcludeCategory(AlwaysManualTestCategory.class) +@Categories.ExcludeCategory(AlwaysManualTest.class) public class FidoTests { } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java index a61996e8..b3844d1d 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java @@ -17,7 +17,7 @@ package com.yubico.yubikit.testing.fido; import com.yubico.yubikit.fido.client.PinRequiredClientError; -import com.yubico.yubikit.testing.AlwaysManualTestCategory; +import com.yubico.yubikit.testing.AlwaysManualTest; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -33,7 +33,7 @@ public class UvDiscouragedInstrumentedTests extends FidoInstrumentedTests { * Skipped on FIPS approved devices. */ @Test - @Category(AlwaysManualTestCategory.class) + @Category(AlwaysManualTest.class) public void testMakeCredentialGetAssertion() throws Throwable { withCtap2Session(BasicWebAuthnClientTests::testUvDiscouragedMcGa_noPin, false); } diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/AlwaysManualTestCategory.java b/testing-android/src/main/java/com/yubico/yubikit/testing/AlwaysManualTest.java similarity index 93% rename from testing-android/src/main/java/com/yubico/yubikit/testing/AlwaysManualTestCategory.java rename to testing-android/src/main/java/com/yubico/yubikit/testing/AlwaysManualTest.java index 7c40dfbc..741514a2 100644 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/AlwaysManualTestCategory.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/AlwaysManualTest.java @@ -16,5 +16,5 @@ package com.yubico.yubikit.testing; -public interface AlwaysManualTestCategory { +public interface AlwaysManualTest { } diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/PinUvAuthProtocolV1Category.java b/testing-android/src/main/java/com/yubico/yubikit/testing/PinUvAuthProtocolV1Test.java similarity index 93% rename from testing-android/src/main/java/com/yubico/yubikit/testing/PinUvAuthProtocolV1Category.java rename to testing-android/src/main/java/com/yubico/yubikit/testing/PinUvAuthProtocolV1Test.java index 2da8d3a0..ae497767 100644 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/PinUvAuthProtocolV1Category.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/PinUvAuthProtocolV1Test.java @@ -16,5 +16,5 @@ package com.yubico.yubikit.testing; -public interface PinUvAuthProtocolV1Category { +public interface PinUvAuthProtocolV1Test { } From 935b84eaf830fd4c19c7eb52cab59d783d241d79 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 19 Jul 2024 20:38:41 +0200 Subject: [PATCH 27/46] refactor FIDO device tests --- .../yubikit/fido/ctap/Ctap2Session.java | 40 +- .../yubikit/testing/SmokeDeviceTests.java | 2 +- .../BasicWebAuthnClientInstrumentedTests.java | 19 +- .../fido/Ctap2ClientPinInstrumentedTests.java | 8 +- ...CredentialManagementInstrumentedTests.java | 2 + .../fido/Ctap2SessionInstrumentedTests.java | 2 + .../fido/UvDiscouragedInstrumentedTests.java | 4 +- .../yubikit/testing/openpgp/OpenPgpTests.java | 2 + .../testing/piv/PivJcaProviderTests.java | 1 + .../framework/FidoInstrumentedTests.java | 52 +- .../framework/YKInstrumentedTests.java | 12 + .../com/yubico/yubikit/testing/TestState.java | 3 +- .../fido/BasicWebAuthnClientTests.java | 979 ++++++++++-------- .../testing/fido/Ctap2BioEnrollmentTests.java | 12 +- .../testing/fido/Ctap2ClientPinTests.java | 6 +- .../testing/fido/Ctap2ConfigTests.java | 26 +- .../fido/Ctap2CredentialManagementTests.java | 16 +- .../testing/fido/Ctap2SessionTests.java | 27 +- .../fido/EnterpriseAttestationTests.java | 22 +- ...{FidoTestUtils.java => FidoTestState.java} | 172 +-- .../yubico/yubikit/testing/fido/TestData.java | 6 - 21 files changed, 780 insertions(+), 633 deletions(-) rename testing/src/main/java/com/yubico/yubikit/testing/fido/{FidoTestUtils.java => FidoTestState.java} (54%) diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java index 3180a885..0d6ab6a8 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java @@ -28,9 +28,11 @@ import com.yubico.yubikit.core.fido.FidoProtocol; import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.smartcard.Apdu; +import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.core.smartcard.AppId; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.core.smartcard.SmartCardProtocol; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Result; import com.yubico.yubikit.core.util.StringUtils; @@ -57,7 +59,6 @@ * * @see Client to Authenticator Protocol (CTAP) */ -@SuppressWarnings("unused") public class Ctap2Session extends ApplicationSession { private static final byte NFCCTAP_MSG = 0x10; @@ -103,21 +104,35 @@ public static void create(YubiKeyDevice device, Callback backend) } } - private static Backend getSmartCardBackend(SmartCardConnection connection) + private static Backend getSmartCardBackend(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) throws IOException, ApplicationNotAvailableException { final SmartCardProtocol protocol = new SmartCardProtocol(connection); protocol.select(AppId.FIDO); + if (scpKeyParams != null) { + try { + protocol.initScp(scpKeyParams); + } catch (ApduException | BadResponseException e) { + throw new IOException("Failed setting up SCP session", e); + } + } return new Backend(protocol) { byte[] sendCbor(byte[] data, @Nullable CommandState state) throws IOException, CommandException { @@ -163,6 +185,10 @@ byte[] sendCbor(byte[] data, @Nullable CommandState state) } private Ctap2Session(FidoProtocol protocol) throws IOException, CommandException { + this(protocol, null); + } + + private Ctap2Session(FidoProtocol protocol, @Nullable ScpKeyParams scpKeyParams) throws IOException, CommandException { this(protocol.getVersion(), new Backend(protocol) { @Override byte[] sendCbor(byte[] data, @Nullable CommandState state) throws IOException { diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java index f13335bc..ab04822d 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java @@ -29,4 +29,4 @@ }) @Suite.SuiteClasses(DeviceTests.class) public class SmokeDeviceTests { -} \ No newline at end of file +} diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java index a37aa4b5..20e42142 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java @@ -19,6 +19,7 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; import com.yubico.yubikit.testing.PinUvAuthProtocolV1Test; +import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -33,44 +34,46 @@ }) public class BasicWebAuthnClientInstrumentedTests { public static class PinUvAuthV2Test extends FidoInstrumentedTests { + @Test + @Category(SmokeTest.class) public void testMakeCredentialGetAssertion() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testMakeCredentialGetAssertion); + withDevice(BasicWebAuthnClientTests::testMakeCredentialGetAssertion); } @Test public void testMakeCredentialGetAssertionTokenUvOnly() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testMakeCredentialGetAssertionTokenUvOnly); + withDevice(BasicWebAuthnClientTests::testMakeCredentialGetAssertionTokenUvOnly); } @Test public void testGetAssertionMultipleUsersRk() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testGetAssertionMultipleUsersRk); + withDevice(BasicWebAuthnClientTests::testGetAssertionMultipleUsersRk); } @Test public void testGetAssertionWithAllowList() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testGetAssertionWithAllowList); + withDevice(BasicWebAuthnClientTests::testGetAssertionWithAllowList); } @Test public void testMakeCredentialWithExcludeList() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testMakeCredentialWithExcludeList); + withDevice(BasicWebAuthnClientTests::testMakeCredentialWithExcludeList); } @Test public void testMakeCredentialKeyAlgorithms() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testMakeCredentialKeyAlgorithms); + withDevice(BasicWebAuthnClientTests::testMakeCredentialKeyAlgorithms); } @Test public void testClientPinManagement() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testClientPinManagement); + withDevice(BasicWebAuthnClientTests::testClientPinManagement); } @Test public void testClientCredentialManagement() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testClientCredentialManagement); + withDevice(BasicWebAuthnClientTests::testClientCredentialManagement); } } 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 bd9ba900..02286d38 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 @@ -18,6 +18,7 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; +import com.yubico.yubikit.testing.PinComplexityDeviceTests; import com.yubico.yubikit.testing.PinUvAuthProtocolV1Test; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; @@ -37,9 +38,14 @@ public static class PinUvAuthV2Test extends FidoInstrumentedTests { public void testClientPin() throws Throwable { withCtap2Session(Ctap2ClientPinTests::testClientPin); } + + @Test + public void testPinComplexity() throws Throwable { + withDevice(PinComplexityDeviceTests::testFido2PinComplexity); + } } - @Category(PinUvAuthProtocolV1Category.class) + @Category(PinUvAuthProtocolV1Test.class) public static class PinUvAuthV1Test extends PinUvAuthV2Test { @Override protected PinUvAuthProtocol getPinUvAuthProtocol() { diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java index 71bab164..7c89d887 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java @@ -19,6 +19,7 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; import com.yubico.yubikit.testing.PinUvAuthProtocolV1Test; +import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -39,6 +40,7 @@ public void testReadMetadata() throws Throwable { } @Test + @Category(SmokeTest.class) public void testManagement() throws Throwable { withCtap2Session(Ctap2CredentialManagementTests::testManagement); } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.java index 31d215de..83d57e26 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionInstrumentedTests.java @@ -19,6 +19,7 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; import com.yubico.yubikit.testing.PinUvAuthProtocolV1Test; +import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; @@ -34,6 +35,7 @@ public class Ctap2SessionInstrumentedTests { public static class PinUvAuthV2Test extends FidoInstrumentedTests { @Test + @Category(SmokeTest.class) public void testCtap2GetInfo() throws Throwable { withCtap2Session(Ctap2SessionTests::testCtap2GetInfo); } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java index b3844d1d..3dc7d58f 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/UvDiscouragedInstrumentedTests.java @@ -35,7 +35,7 @@ public class UvDiscouragedInstrumentedTests extends FidoInstrumentedTests { @Test @Category(AlwaysManualTest.class) public void testMakeCredentialGetAssertion() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testUvDiscouragedMcGa_noPin, false); + withDevice(false, BasicWebAuthnClientTests::testUvDiscouragedMcGa_noPin); } /** @@ -46,6 +46,6 @@ public void testMakeCredentialGetAssertion() throws Throwable { */ @Test(expected = PinRequiredClientError.class) public void testMakeCredentialGetAssertionWithPin() throws Throwable { - withCtap2Session(BasicWebAuthnClientTests::testUvDiscouragedMcGa_withPin); + withDevice(BasicWebAuthnClientTests::testUvDiscouragedMcGa_withPin); } } 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 d7f976f4..ef459921 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 @@ -19,7 +19,9 @@ 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; import org.junit.Test; diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java index 164ac4bd..430ca3e7 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/piv/PivJcaProviderTests.java @@ -22,6 +22,7 @@ import com.yubico.yubikit.core.smartcard.scp.ScpKid; import com.yubico.yubikit.testing.SlowTest; +import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.PivInstrumentedTests; import org.junit.Test; diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java index d7e358fb..86f37840 100755 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java @@ -16,44 +16,44 @@ package com.yubico.yubikit.testing.framework; +import androidx.annotation.Nullable; + +import com.yubico.yubikit.core.Transport; +import com.yubico.yubikit.core.smartcard.scp.ScpKid; 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.testing.fido.FidoTestUtils; - -import java.util.Optional; -import java.util.concurrent.LinkedBlockingQueue; - -import javax.annotation.Nullable; +import com.yubico.yubikit.testing.TestState; +import com.yubico.yubikit.testing.fido.FidoTestState; public class FidoInstrumentedTests extends YKInstrumentedTests { - public interface Callback { - void invoke(Ctap2Session session) throws Throwable; - } - protected void withCtap2Session(Callback callback) throws Throwable { - withCtap2Session(callback, true); + protected void withDevice(TestState.StatefulDeviceCallback callback) throws Throwable { + withDevice(true, callback); } - protected void withCtap2Session(Callback callback, boolean setPin) throws Throwable { + protected void withDevice(boolean setPin, TestState.StatefulDeviceCallback callback) throws Throwable { + FidoTestState state = new FidoTestState(device, this::reconnectDevice, getScpKid(), getPinUvAuthProtocol(), setPin); + state.withDeviceCallback(callback); + } - FidoTestUtils.verifyAndSetup(device, getPinUvAuthProtocol(), setPin); + protected void withCtap2Session(TestState.SessionCallback callback) throws Throwable { + FidoTestState state = new FidoTestState(device, this::reconnectDevice, getScpKid(), getPinUvAuthProtocol(), true); + state.withCtap2(callback); + } - LinkedBlockingQueue> result = new LinkedBlockingQueue<>(); - Ctap2Session.create(device, value -> { - try { - Ctap2Session session = value.getValue(); - callback.invoke(session); - result.offer(Optional.empty()); - } catch (Throwable e) { - result.offer(Optional.of(e)); - } - }); + protected void withCtap2Session(TestState.StatefulSessionCallback callback) throws Throwable { + FidoTestState state = new FidoTestState(device, this::reconnectDevice, getScpKid(), getPinUvAuthProtocol(), true); + state.withCtap2(callback); + } - Optional exception = result.take(); - if (exception.isPresent()) { - throw exception.get(); + @Nullable + @Override + protected Byte getScpKid() { + if (device.getTransport() == Transport.NFC) { + return ScpKid.SCP11b; } + return null; } protected PinUvAuthProtocol getPinUvAuthProtocol() { 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 0fe5b597..f052f9c2 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 @@ -69,6 +69,18 @@ protected YubiKeyDevice reconnectDevice() { } } + protected YubiKeyDevice reconnectDevice() { + try { + if (device.getTransport() == Transport.NFC) { + releaseYubiKey(); + getYubiKey(); + } + return device; + } catch (InterruptedException e) { + throw new RuntimeException("Failure during reconnect", e); + } + } + @Nullable protected Byte getScpKid() { return null; diff --git a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java index 2b06152f..2c64e606 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -41,7 +41,6 @@ import javax.annotation.Nullable; public class TestState { - public static class Builder> { final protected YubiKeyDevice device; @Nullable @@ -84,7 +83,7 @@ protected TestState(Builder builder) { this.scpParameters = new ScpParameters(builder.device, this.scpKid); this.reconnectDeviceCallback = builder.reconnectDeviceCallback; this.isUsbTransport = builder.device.getTransport() == Transport.USB; - } + } public boolean isUsbTransport() { return isUsbTransport; diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java index 854ef699..86d90695 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java @@ -38,7 +38,6 @@ import com.yubico.yubikit.fido.client.MultipleAssertionsAvailable; import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.CredentialManagement; -import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.fido.webauthn.AuthenticatorAssertionResponse; import com.yubico.yubikit.fido.webauthn.AuthenticatorAttestationResponse; import com.yubico.yubikit.fido.webauthn.AuthenticatorSelectionCriteria; @@ -71,247 +70,275 @@ public class BasicWebAuthnClientTests { - public static void testMakeCredentialGetAssertionTokenUvOnly(Ctap2Session session) throws Throwable { - assumeTrue("UV Token not supported", ClientPin.isTokenSupported(session.getCachedInfo())); - testMakeCredentialGetAssertion(session); + public static void testMakeCredentialGetAssertionTokenUvOnly(FidoTestState state) throws Throwable { + state.withCtap2((session) -> { + assumeTrue("UV Token not supported", ClientPin.isTokenSupported(session.getCachedInfo())); + }); + testMakeCredentialGetAssertion(state); } - public static void testMakeCredentialGetAssertion(Ctap2Session session) throws Throwable { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + public static void testMakeCredentialGetAssertion(FidoTestState state) throws Throwable { List deleteCredIds = new ArrayList<>(); // Make a non rk credential - PublicKeyCredentialCreationOptions creationOptionsNonRk = getCreateOptions( - new PublicKeyCredentialUserEntity( - "rkuser", - "rkuser".getBytes(StandardCharsets.UTF_8), - "RkUser" - ), - false, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null - ); - PublicKeyCredential credNonRk = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptionsNonRk, - Objects.requireNonNull(creationOptionsNonRk.getRp().getId()), - TestData.PIN, - null, - null - ); - AuthenticatorAttestationResponse responseNonRk = (AuthenticatorAttestationResponse) credNonRk.getResponse(); - assertNotNull("Failed to make non resident key credential", responseNonRk); - assertNotNull("Credential missing attestation object", responseNonRk.getAttestationObject()); - assertNotNull("Credential missing client data JSON", responseNonRk.getClientDataJson()); + state.withCtap2((session) -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + PublicKeyCredentialCreationOptions creationOptionsNonRk = getCreateOptions( + new PublicKeyCredentialUserEntity( + "user", + "user".getBytes(StandardCharsets.UTF_8), + "User" + ), + false, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null + ); + PublicKeyCredential credNonRk = webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptionsNonRk, + Objects.requireNonNull(creationOptionsNonRk.getRp().getId()), + TestData.PIN, + null, + null + ); + AuthenticatorAttestationResponse responseNonRk = (AuthenticatorAttestationResponse) credNonRk.getResponse(); + assertNotNull("Failed to make non resident key credential", responseNonRk); + assertNotNull("Credential missing attestation object", responseNonRk.getAttestationObject()); + assertNotNull("Credential missing client data JSON", responseNonRk.getClientDataJson()); + }); // make a rk credential - PublicKeyCredentialCreationOptions creationOptionsRk = getCreateOptions( - new PublicKeyCredentialUserEntity( - "user", - "user".getBytes(StandardCharsets.UTF_8), - "User" - ), - true, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null); - PublicKeyCredential credRk = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptionsRk, - Objects.requireNonNull(creationOptionsRk.getRp().getId()), - TestData.PIN, - null, - null - ); - AuthenticatorAttestationResponse responseRk = (AuthenticatorAttestationResponse) credRk.getResponse(); - assertNotNull("Failed to make resident key credential", responseRk); - assertNotNull("Credential missing attestation object", responseRk.getAttestationObject()); - assertNotNull("Credential missing client data JSON", responseRk.getClientDataJson()); - deleteCredIds.add((byte[]) parseCredentialData(getAuthenticatorDataFromAttestationResponse(responseRk)).get("credId")); + state.withCtap2((session) -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialCreationOptions creationOptionsRk = getCreateOptions( + new PublicKeyCredentialUserEntity( + "rkuser", + "rkuser".getBytes(StandardCharsets.UTF_8), + "RkUser" + ), + true, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null); + PublicKeyCredential credRk = webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptionsRk, + Objects.requireNonNull(creationOptionsRk.getRp().getId()), + TestData.PIN, + null, + null + ); + AuthenticatorAttestationResponse responseRk = (AuthenticatorAttestationResponse) credRk.getResponse(); + assertNotNull("Failed to make resident key credential", responseRk); + assertNotNull("Credential missing attestation object", responseRk.getAttestationObject()); + assertNotNull("Credential missing client data JSON", responseRk.getClientDataJson()); + deleteCredIds.add((byte[]) parseCredentialData(getAuthenticatorDataFromAttestationResponse(responseRk)).get("credId")); + }); // Get assertions - PublicKeyCredentialRequestOptions requestOptions = new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, - (long) 90000, - TestData.RP_ID, - null, - null, - null - ); - - try { - PublicKeyCredential credential = webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptions, + state.withCtap2((session) -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialRequestOptions requestOptions = new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, + (long) 90000, TestData.RP_ID, - TestData.PIN, + null, + null, null ); - AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) credential.getResponse(); - assertNotNull("Assertion response missing authenticator data", response.getAuthenticatorData()); - assertNotNull("Assertion response missing signature", response.getSignature()); - assertNotNull("Assertion response missing user handle", response.getUserHandle()); - } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { - fail("Got MultipleAssertionsAvailable even though there should only be one credential"); - } - deleteCredentials(webauthn, deleteCredIds); - } + try { + PublicKeyCredential credential = webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, + requestOptions, + TestData.RP_ID, + TestData.PIN, + null + ); + AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) credential.getResponse(); + assertNotNull("Assertion response missing authenticator data", response.getAuthenticatorData()); + assertNotNull("Assertion response missing signature", response.getSignature()); + assertNotNull("Assertion response missing user handle", response.getUserHandle()); + } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { + fail("Got MultipleAssertionsAvailable even though there should only be one credential"); + } - public static void testUvDiscouragedMcGa_withPin(Ctap2Session session) throws Throwable { - assumeTrue("Device has no PIN set", - Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); - testUvDiscouragedMakeCredentialGetAssertion(session); + deleteCredentials(webauthn, deleteCredIds); + }); } - public static void testUvDiscouragedMcGa_noPin(Ctap2Session session) throws Throwable { - assumeFalse("Device has PIN set. Reset and try again.", - Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); - assumeFalse("Ignoring FIPS approved devices", TestData.FIPS_APPROVED); - testUvDiscouragedMakeCredentialGetAssertion(session); + public static void testUvDiscouragedMcGa_withPin(FidoTestState state) throws Throwable { + state.withCtap2(session -> { + assumeTrue("Device has no PIN set", + Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); + }); + testUvDiscouragedMakeCredentialGetAssertion(state); } - private static void testUvDiscouragedMakeCredentialGetAssertion(Ctap2Session session) - throws Throwable { - - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + public static void testUvDiscouragedMcGa_noPin(FidoTestState state) throws Throwable { + state.withCtap2(session -> { + assumeFalse("Device has PIN set. Reset and try again.", + Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); + assumeFalse("Ignoring FIPS approved devices", state.isFipsApproved()); + }); + testUvDiscouragedMakeCredentialGetAssertion(state); + } + private static void testUvDiscouragedMakeCredentialGetAssertion(FidoTestState state) throws Throwable { // Test non rk credential - PublicKeyCredentialCreationOptions creationOptionsNonRk = getCreateOptions( - new PublicKeyCredentialUserEntity( - "user", - "user".getBytes(StandardCharsets.UTF_8), - "User" - ), - false, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null, - UserVerificationRequirement.DISCOURAGED - ); - PublicKeyCredential credNonRk = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptionsNonRk, - Objects.requireNonNull(creationOptionsNonRk.getRp().getId()), - null, - null, - null - ); + PublicKeyCredential credNonRk = state.withCtap2((session) -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + PublicKeyCredentialCreationOptions creationOptionsNonRk = getCreateOptions( + new PublicKeyCredentialUserEntity( + "user", + "user".getBytes(StandardCharsets.UTF_8), + "User" + ), + false, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null, + UserVerificationRequirement.DISCOURAGED + ); + PublicKeyCredential publicKeyCredential = webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptionsNonRk, + Objects.requireNonNull(creationOptionsNonRk.getRp().getId()), + null, + null, + null + ); - AuthenticatorAttestationResponse responseNonRk = (AuthenticatorAttestationResponse) credNonRk.getResponse(); - assertNotNull("Failed to make non resident key credential", responseNonRk); - assertNotNull("Credential missing attestation object", responseNonRk.getAttestationObject()); - assertNotNull("Credential missing client data JSON", responseNonRk.getClientDataJson()); + AuthenticatorAttestationResponse responseNonRk = (AuthenticatorAttestationResponse) publicKeyCredential.getResponse(); + assertNotNull("Failed to make non resident key credential", responseNonRk); + assertNotNull("Credential missing attestation object", responseNonRk.getAttestationObject()); + assertNotNull("Credential missing client data JSON", responseNonRk.getClientDataJson()); + return publicKeyCredential; + }); // Get assertions - PublicKeyCredentialRequestOptions requestOptionsNonRk = new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, - (long) 90000, - TestData.RP_ID, - Collections.singletonList(new PublicKeyCredentialDescriptor(credNonRk.getType(), credNonRk.getRawId())), - UserVerificationRequirement.DISCOURAGED, - null - ); - - try { - PublicKeyCredential credential = webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptionsNonRk, + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialRequestOptions requestOptionsNonRk = new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, + (long) 90000, TestData.RP_ID, - null, + Collections.singletonList(new PublicKeyCredentialDescriptor(credNonRk.getType(), credNonRk.getRawId())), + UserVerificationRequirement.DISCOURAGED, null ); - AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) credential.getResponse(); - assertNotNull("Assertion response missing authenticator data", response.getAuthenticatorData()); - assertNotNull("Assertion response missing signature", response.getSignature()); - // User identifiable information (name, DisplayName, icon) MUST NOT be returned if user verification is not done by the authenticator. - assertNull("Assertion response contains user handle", response.getUserHandle()); - } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { - fail("Got MultipleAssertionsAvailable even though there should only be one credential"); - } + + try { + PublicKeyCredential credential = webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, + requestOptionsNonRk, + TestData.RP_ID, + null, + null + ); + AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) credential.getResponse(); + assertNotNull("Assertion response missing authenticator data", response.getAuthenticatorData()); + assertNotNull("Assertion response missing signature", response.getSignature()); + // User identifiable information (name, DisplayName, icon) MUST NOT be returned if user verification is not done by the authenticator. + assertNull("Assertion response contains user handle", response.getUserHandle()); + } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { + fail("Got MultipleAssertionsAvailable even though there should only be one credential"); + } + }); // test rk credential - PublicKeyCredentialCreationOptions creationOptionsRk = getCreateOptions( - new PublicKeyCredentialUserEntity( - "rkuser", - "rkuser".getBytes(StandardCharsets.UTF_8), - "RkUser" - ), - true, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null, - UserVerificationRequirement.DISCOURAGED - ); - PublicKeyCredential credRk = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptionsRk, - Objects.requireNonNull(creationOptionsRk.getRp().getId()), - null, - null, - null - ); + PublicKeyCredential credRk = state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialCreationOptions creationOptionsRk = getCreateOptions( + new PublicKeyCredentialUserEntity( + "rkuser", + "rkuser".getBytes(StandardCharsets.UTF_8), + "RkUser" + ), + true, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null, + UserVerificationRequirement.DISCOURAGED + ); + PublicKeyCredential publicKeyCredential = webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptionsRk, + Objects.requireNonNull(creationOptionsRk.getRp().getId()), + null, + null, + null + ); - AuthenticatorAttestationResponse responseRk = (AuthenticatorAttestationResponse) credRk.getResponse(); - assertNotNull("Failed to make non resident key credential", responseRk); - assertNotNull("Credential missing attestation object", responseRk.getAttestationObject()); - assertNotNull("Credential missing client data JSON", responseRk.getClientDataJson()); + AuthenticatorAttestationResponse responseRk = (AuthenticatorAttestationResponse) publicKeyCredential.getResponse(); + assertNotNull("Failed to make non resident key credential", responseRk); + assertNotNull("Credential missing attestation object", responseRk.getAttestationObject()); + assertNotNull("Credential missing client data JSON", responseRk.getClientDataJson()); + return publicKeyCredential; + }); // Get assertions - PublicKeyCredentialRequestOptions requestOptionsRk = new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, - (long) 90000, - TestData.RP_ID, - Collections.singletonList(new PublicKeyCredentialDescriptor(credRk.getType(), credRk.getRawId())), - UserVerificationRequirement.DISCOURAGED, - null - ); - - try { - PublicKeyCredential credential = webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptionsRk, + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialRequestOptions requestOptionsRk = new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, + (long) 90000, TestData.RP_ID, - null, + Collections.singletonList(new PublicKeyCredentialDescriptor(credRk.getType(), credRk.getRawId())), + UserVerificationRequirement.DISCOURAGED, null ); - AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) credential.getResponse(); - assertNotNull("Assertion response missing authenticator data", response.getAuthenticatorData()); - assertNotNull("Assertion response missing signature", response.getSignature()); - assertNotNull("Assertion response missing user handle", response.getUserHandle()); - } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { - fail("Got MultipleAssertionsAvailable even though there should only be one credential"); - } + + try { + PublicKeyCredential credential = webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, + requestOptionsRk, + TestData.RP_ID, + null, + null + ); + AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) credential.getResponse(); + assertNotNull("Assertion response missing authenticator data", response.getAuthenticatorData()); + assertNotNull("Assertion response missing signature", response.getSignature()); + assertNotNull("Assertion response missing user handle", response.getUserHandle()); + } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { + fail("Got MultipleAssertionsAvailable even though there should only be one credential"); + } + }); } - public static void testGetAssertionMultipleUsersRk(Ctap2Session session) throws Throwable { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + public static void testGetAssertionMultipleUsersRk(FidoTestState state) throws Throwable { List deleteCredIds = new ArrayList<>(); - Map userIdCredIdMap = new HashMap<>(); // make 3 rk credential for (int i = 0; i < 3; i++) { - PublicKeyCredentialUserEntity user = new PublicKeyCredentialUserEntity( - "user" + i, - ("user" + i).getBytes(StandardCharsets.UTF_8), - "User" + i - ); - PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( - user, - true, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null - ); - PublicKeyCredential credential = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions, - Objects.requireNonNull(creationOptions.getRp().getId()), - TestData.PIN, - null, - null - ); - AuthenticatorAttestationResponse response = (AuthenticatorAttestationResponse) credential.getResponse(); - byte[] credId = (byte[]) parseCredentialData(getAuthenticatorDataFromAttestationResponse(response)).get("credId"); - userIdCredIdMap.put(user.getId(), credId); - deleteCredIds.add(credId); + final int userIndex = i; + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialUserEntity user = new PublicKeyCredentialUserEntity( + "user" + userIndex, + ("user" + userIndex).getBytes(StandardCharsets.UTF_8), + "User" + userIndex + ); + PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( + user, + true, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null + ); + PublicKeyCredential credential = webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions, + Objects.requireNonNull(creationOptions.getRp().getId()), + TestData.PIN, + null, + null + ); + AuthenticatorAttestationResponse response = (AuthenticatorAttestationResponse) credential.getResponse(); + byte[] credId = (byte[]) parseCredentialData(getAuthenticatorDataFromAttestationResponse(response)).get("credId"); + userIdCredIdMap.put(user.getId(), credId); + deleteCredIds.add(credId); + }); } // Get assertions @@ -325,164 +352,218 @@ public static void testGetAssertionMultipleUsersRk(Ctap2Session session) throws ); for (int i = 0; i < 3; i++) { - try { - webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptions, - TestData.RP_ID, - TestData.PIN, - null - ); - fail("Got single assertion even though multiple credentials exist"); - } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { - List users = multipleAssertionsAvailable.getUsers(); - assertNotNull("Assertion failed to return user list", users); - assertTrue("There should be at least 3 users found", users.size() >= 3); - PublicKeyCredentialUserEntity user = users.get(i); - assertNotNull(user.getId()); - assertNotNull(user.getName()); - assertNotNull(user.getDisplayName()); - if (userIdCredIdMap.containsKey(user.getId())) { - PublicKeyCredential credential = multipleAssertionsAvailable.select(i); - AuthenticatorAssertionResponse assertion = (AuthenticatorAssertionResponse) credential.getResponse(); - assertNotNull("Failed to get assertion", assertion); - assertNotNull("Assertion response missing authenticator data", assertion.getAuthenticatorData()); - assertNotNull("Assertion response missing signature", assertion.getSignature()); - assertNotNull("Assertion response missing user handle", assertion.getUserHandle()); - - assertArrayEquals(userIdCredIdMap.get(users.get(i).getId()), credential.getRawId()); + final int userIndex = i; + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + try { + webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, + requestOptions, + TestData.RP_ID, + TestData.PIN, + null + ); + fail("Got single assertion even though multiple credentials exist"); + } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { + List users = multipleAssertionsAvailable.getUsers(); + assertNotNull("Assertion failed to return user list", users); + assertTrue("There should be at least 3 users found", users.size() >= 3); + PublicKeyCredentialUserEntity user = users.get(userIndex); + assertNotNull(user.getId()); + assertNotNull(user.getName()); + assertNotNull(user.getDisplayName()); + if (userIdCredIdMap.containsKey(user.getId())) { + PublicKeyCredential credential = multipleAssertionsAvailable.select(userIndex); + AuthenticatorAssertionResponse assertion = (AuthenticatorAssertionResponse) credential.getResponse(); + assertNotNull("Failed to get assertion", assertion); + assertNotNull("Assertion response missing authenticator data", assertion.getAuthenticatorData()); + assertNotNull("Assertion response missing signature", assertion.getSignature()); + assertNotNull("Assertion response missing user handle", assertion.getUserHandle()); + assertArrayEquals(userIdCredIdMap.get(users.get(userIndex) + .getId()), credential.getRawId()); + } } - } + }); } - deleteCredentials(webauthn, deleteCredIds); + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + if (CredentialManagement.isSupported(session.getCachedInfo())) { + deleteCredentials(webauthn, deleteCredIds); + } + }); } - public static void testGetAssertionWithAllowList(Ctap2Session session) throws Throwable { + public static void testGetAssertionWithAllowList(FidoTestState state) throws Throwable { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredential cred1 = state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - // Make 2 new credentials - PublicKeyCredentialCreationOptions creationOptions1 = getCreateOptions( - new PublicKeyCredentialUserEntity( - "user1", - "user1".getBytes(StandardCharsets.UTF_8), - "testUser1" - ), - false, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null - ); - PublicKeyCredentialCreationOptions creationOptions2 = getCreateOptions( - new PublicKeyCredentialUserEntity( - "user2", - "user2".getBytes(StandardCharsets.UTF_8), - "testUser2" - ), - false, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null - ); + // Make 2 new credentials + PublicKeyCredentialCreationOptions creationOptions1 = getCreateOptions( + new PublicKeyCredentialUserEntity( + "user1", + "user1".getBytes(StandardCharsets.UTF_8), + "testUser1" + ), + false, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null + ); - PublicKeyCredential cred1 = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions1, - Objects.requireNonNull(TestData.RP.getId()), - TestData.PIN, - null, - null - ); - byte[] credId1 = cred1.getRawId(); + return webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions1, + Objects.requireNonNull(TestData.RP.getId()), + TestData.PIN, + null, + null + ); + }); + + PublicKeyCredential cred2 = state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + PublicKeyCredentialCreationOptions creationOptions2 = getCreateOptions( + new PublicKeyCredentialUserEntity( + "user2", + "user2".getBytes(StandardCharsets.UTF_8), + "testUser2" + ), + false, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null + ); + + return webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions2, + Objects.requireNonNull(TestData.RP.getId()), + TestData.PIN, + null, + null + ); + }); - PublicKeyCredential cred2 = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions2, - Objects.requireNonNull(TestData.RP.getId()), - TestData.PIN, - null, - null - ); - byte[] credId2 = cred2.getRawId(); - // GetAssertions with allowList containing only credId1 - List allowCreds = Collections.singletonList( - new PublicKeyCredentialDescriptor( - PublicKeyCredentialType.PUBLIC_KEY, - credId1, - null - ) - ); - PublicKeyCredentialRequestOptions requestOptions = new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, - (long) 90000, - TestData.RP_ID, - allowCreds, - null, - null - ); + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - PublicKeyCredential credential = webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptions, - TestData.RP_ID, - TestData.PIN, - null - ); - assertArrayEquals(credId1, credential.getRawId()); + // GetAssertions with allowList containing only credId1 + List allowCreds = Collections.singletonList( + new PublicKeyCredentialDescriptor( + PublicKeyCredentialType.PUBLIC_KEY, + cred1.getRawId(), + null + ) + ); + PublicKeyCredentialRequestOptions requestOptions = new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, + (long) 90000, + TestData.RP_ID, + allowCreds, + null, + null + ); - // GetAssertions with allowList containing only credId2 - allowCreds = Collections.singletonList(new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, credId2, null)); - requestOptions = new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, (long) 90000, TestData.RP_ID, allowCreds, null, null); + PublicKeyCredential credential = webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, + requestOptions, + TestData.RP_ID, + TestData.PIN, + null + ); + assertArrayEquals(cred1.getRawId(), credential.getRawId()); - credential = webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptions, - TestData.RP_ID, - TestData.PIN, - null - ); - assertArrayEquals(credId2, credential.getRawId()); - } + }); - public static void testMakeCredentialWithExcludeList(Ctap2Session session) throws Throwable { + state.withCtap2(session -> { + // GetAssertions with allowList containing only credId2 + List allowCreds = Collections.singletonList( + new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, cred2.getRawId(), null)); + PublicKeyCredentialRequestOptions requestOptions = new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, (long) 90000, TestData.RP_ID, allowCreds, null, null); + + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredential credential = webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, + requestOptions, + TestData.RP_ID, + TestData.PIN, + null + ); + assertArrayEquals(cred2.getRawId(), credential.getRawId()); + }); + } - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + public static void testMakeCredentialWithExcludeList(FidoTestState state) throws Throwable { List excludeList = new ArrayList<>(); - // Make a non RK credential - PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( - null, - false, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null - ); + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - PublicKeyCredential credential = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions, - Objects.requireNonNull(creationOptions.getRp().getId()), - TestData.PIN, - null, - null - ); - excludeList.add( - new PublicKeyCredentialDescriptor( - PublicKeyCredentialType.PUBLIC_KEY, - credential.getRawId(), - null - ) - ); + // Make a non RK credential + PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( + null, + false, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null + ); + + PublicKeyCredential credential = webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions, + Objects.requireNonNull(creationOptions.getRp().getId()), + TestData.PIN, + null, + null + ); + excludeList.add( + new PublicKeyCredentialDescriptor( + PublicKeyCredentialType.PUBLIC_KEY, + credential.getRawId(), + null + ) + ); + }); // Make another non RK credential with exclude list including credId. Should fail - creationOptions = getCreateOptions( - null, - false, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - excludeList - ); - try { + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( + null, + false, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + excludeList + ); + try { + webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions, + Objects.requireNonNull(creationOptions.getRp().getId()), + TestData.PIN, + null, + null + ); + fail("Succeeded in making credential even though the credential was excluded"); + } catch (ClientError clientError) { + assertEquals(ClientError.Code.DEVICE_INELIGIBLE, clientError.getErrorCode()); + } + }); + + + // Make another non RK credential with exclude list null. Should succeed + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( + null, + false, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null + ); webauthn.makeCredential( TestData.CLIENT_DATA_JSON_CREATE, creationOptions, @@ -491,38 +572,53 @@ public static void testMakeCredentialWithExcludeList(Ctap2Session session) throw null, null ); - fail("Succeeded in making credential even though the credential was excluded"); - } catch (ClientError clientError) { - assertEquals(ClientError.Code.DEVICE_INELIGIBLE, clientError.getErrorCode()); - } + }); - // Make another non RK credential with exclude list null. Should succeed - creationOptions = getCreateOptions( - null, - false, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null - ); - webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions, - Objects.requireNonNull(creationOptions.getRp().getId()), - TestData.PIN, - null, - null - ); } - public static void testMakeCredentialKeyAlgorithms(Ctap2Session session) throws Throwable { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + public static void testMakeCredentialKeyAlgorithms(FidoTestState state) throws Throwable { + List allCredParams = Arrays.asList( TestData.PUB_KEY_CRED_PARAMS_ES256, TestData.PUB_KEY_CRED_PARAMS_EDDSA); // Test individual algorithms for (PublicKeyCredentialParameters param : allCredParams) { + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( + null, false, Collections.singletonList(param), null); + PublicKeyCredential credential = webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions, + Objects.requireNonNull(creationOptions.getRp().getId()), + TestData.PIN, + null, + null + ); + AuthenticatorAttestationResponse attestation = (AuthenticatorAttestationResponse) credential.getResponse(); + int alg = (Integer) Objects.requireNonNull( + parseCredentialData( + getAuthenticatorDataFromAttestationResponse(attestation) + ).get("keyAlgo") + ); + assertEquals(param.getAlg(), alg); + }); + } + + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + // Test algorithm order: ES256 - EdDSA + List credParams = Arrays.asList( + allCredParams.get(0), + allCredParams.get(1)); PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( - null, false, Collections.singletonList(param), null); + null, + false, + credParams, + null + ); PublicKeyCredential credential = webauthn.makeCredential( TestData.CLIENT_DATA_JSON_CREATE, creationOptions, @@ -537,109 +633,90 @@ public static void testMakeCredentialKeyAlgorithms(Ctap2Session session) throws getAuthenticatorDataFromAttestationResponse(attestation) ).get("keyAlgo") ); - assertEquals(param.getAlg(), alg); - } - - // Test algorithm order: ES256 - EdDSA - List credParams = Arrays.asList( - allCredParams.get(0), - allCredParams.get(1)); - PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( - null, - false, - credParams, - null - ); - PublicKeyCredential credential = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions, - Objects.requireNonNull(creationOptions.getRp().getId()), - TestData.PIN, - null, - null - ); - AuthenticatorAttestationResponse attestation = (AuthenticatorAttestationResponse) credential.getResponse(); - int alg = (Integer) Objects.requireNonNull( - parseCredentialData( - getAuthenticatorDataFromAttestationResponse(attestation) - ).get("keyAlgo") - ); - assertEquals(credParams.get(0).getAlg(), alg); - - // Test algorithm order: ALG_EdDSA - ALG_ES256 - credParams = Arrays.asList( - allCredParams.get(1), - allCredParams.get(0)); - creationOptions = getCreateOptions(null, false, credParams, null); - credential = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions, - Objects.requireNonNull(creationOptions.getRp().getId()), - TestData.PIN, - null, - null - ); - attestation = (AuthenticatorAttestationResponse) credential.getResponse(); - alg = (Integer) Objects.requireNonNull( - parseCredentialData( - getAuthenticatorDataFromAttestationResponse(attestation) - ).get("keyAlgo") - ); - assertEquals(credParams.get(0).getAlg(), alg); + assertEquals(credParams.get(0).getAlg(), alg); + }); + + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + // Test algorithm order: ALG_EdDSA - ALG_ES256 + List credParams = Arrays.asList( + allCredParams.get(1), + allCredParams.get(0)); + PublicKeyCredentialCreationOptions creationOptions = getCreateOptions(null, false, credParams, null); + PublicKeyCredential credential = webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions, + Objects.requireNonNull(creationOptions.getRp().getId()), + TestData.PIN, + null, + null + ); + AuthenticatorAttestationResponse attestation = (AuthenticatorAttestationResponse) credential.getResponse(); + int alg = (Integer) Objects.requireNonNull( + parseCredentialData( + getAuthenticatorDataFromAttestationResponse(attestation) + ).get("keyAlgo") + ); + assertEquals(credParams.get(0).getAlg(), alg); + }); } - public static void testClientPinManagement(Ctap2Session session) throws Throwable { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - assertTrue(webauthn.isPinSupported()); - assertTrue(webauthn.isPinConfigured()); - - webauthn.changePin(TestData.PIN, TestData.OTHER_PIN); + public static void testClientPinManagement(FidoTestState state) throws Throwable { + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + assumeTrue("Pin not supported", webauthn.isPinSupported()); + assertTrue(webauthn.isPinConfigured()); - try { webauthn.changePin(TestData.PIN, TestData.OTHER_PIN); - fail("Wrong PIN was accepted"); - } catch (ClientError e) { - assertThat(e.getErrorCode(), equalTo(ClientError.Code.BAD_REQUEST)); - assertThat(e.getCause(), instanceOf(CtapException.class)); - assertThat(((CtapException) Objects.requireNonNull(e.getCause())).getCtapError(), - is(CtapException.ERR_PIN_INVALID)); - } - webauthn.changePin(TestData.OTHER_PIN, TestData.PIN); - } + try { + webauthn.changePin(TestData.PIN, TestData.OTHER_PIN); + fail("Wrong PIN was accepted"); + } catch (ClientError e) { + assertThat(e.getErrorCode(), equalTo(ClientError.Code.BAD_REQUEST)); + assertThat(e.getCause(), instanceOf(CtapException.class)); + assertThat(((CtapException) Objects.requireNonNull(e.getCause())).getCtapError(), + is(CtapException.ERR_PIN_INVALID)); + } + webauthn.changePin(TestData.OTHER_PIN, TestData.PIN); + }); + } - public static void testClientCredentialManagement(Ctap2Session session) throws Throwable { - assumeTrue("Credential management not supported", - CredentialManagement.isSupported(session.getCachedInfo())); - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - PublicKeyCredentialCreationOptions creationOptions = getCreateOptions(null, true, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null); - webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions, - Objects.requireNonNull(creationOptions.getRp().getId()), - TestData.PIN, - null, - null); + public static void testClientCredentialManagement(FidoTestState state) throws Throwable { + state.withCtap2(session -> { + assumeTrue("Credential management not supported", + CredentialManagement.isSupported(session.getCachedInfo())); + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialCreationOptions creationOptions = getCreateOptions(null, true, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null); + webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions, + Objects.requireNonNull(creationOptions.getRp().getId()), + TestData.PIN, + null, + null); - CredentialManager credentialManager = webauthn.getCredentialManager(TestData.PIN); + CredentialManager credentialManager = webauthn.getCredentialManager(TestData.PIN); - assertThat(credentialManager.getCredentialCount(), equalTo(1)); + assertThat(credentialManager.getCredentialCount(), equalTo(1)); - List rpIds = credentialManager.getRpIdList(); - assertThat(rpIds, equalTo(Collections.singletonList(TestData.RP_ID))); + List rpIds = credentialManager.getRpIdList(); + assertThat(rpIds, equalTo(Collections.singletonList(TestData.RP_ID))); - Map credentials = credentialManager.getCredentials(TestData.RP_ID); - assertThat(credentials.size(), equalTo(1)); - PublicKeyCredentialDescriptor key = credentials.keySet().iterator().next(); - assertThat(Objects.requireNonNull(credentials.get(key)).getId(), equalTo(TestData.USER_ID)); + Map credentials = credentialManager.getCredentials(TestData.RP_ID); + assertThat(credentials.size(), equalTo(1)); + PublicKeyCredentialDescriptor key = credentials.keySet().iterator().next(); + assertThat(Objects.requireNonNull(credentials.get(key)) + .getId(), equalTo(TestData.USER_ID)); - credentialManager.deleteCredential(key); - assertThat(credentialManager.getCredentialCount(), equalTo(0)); - assertTrue(credentialManager.getCredentials(TestData.RP_ID).isEmpty()); - assertTrue(credentialManager.getRpIdList().isEmpty()); + credentialManager.deleteCredential(key); + assertThat(credentialManager.getCredentialCount(), equalTo(0)); + assertTrue(credentialManager.getCredentials(TestData.RP_ID).isEmpty()); + assertTrue(credentialManager.getRpIdList().isEmpty()); + }); } private static PublicKeyCredentialCreationOptions getCreateOptions( diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java index 3a6ec960..3fb6acc2 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java @@ -39,12 +39,13 @@ public class Ctap2BioEnrollmentTests { private static final Logger logger = LoggerFactory.getLogger(PivCertificateTests.class); - public static void testFingerprintEnrollment(Ctap2Session session) throws Throwable { + public static void testFingerprintEnrollment(Ctap2Session session, FidoTestState state) + throws Throwable { assumeTrue("Bio enrollment not supported", BioEnrollment.isSupported(session.getCachedInfo())); - final FingerprintBioEnrollment fingerprintBioEnrollment = fpBioEnrollment(session); + final FingerprintBioEnrollment fingerprintBioEnrollment = fpBioEnrollment(session, state); removeAllFingerprints(fingerprintBioEnrollment); @@ -98,18 +99,17 @@ private static byte[] enrollFingerprint(FingerprintBioEnrollment bioEnrollment) return templateId; } - private static FingerprintBioEnrollment fpBioEnrollment( - Ctap2Session session) throws Throwable { + private static FingerprintBioEnrollment fpBioEnrollment(Ctap2Session session, FidoTestState state) throws Throwable { // ensureDefaultPinSet(session); - final ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); + final ClientPin pin = new ClientPin(session, state.getPinUvAuthProtocol()); final byte[] pinToken = pin.getPinToken( TestData.PIN, ClientPin.PIN_PERMISSION_BE, "localhost"); - return new FingerprintBioEnrollment(session, TestData.PIN_UV_AUTH_PROTOCOL, pinToken); + return new FingerprintBioEnrollment(session, state.getPinUvAuthProtocol(), pinToken); } public static void renameFingerprint( diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java index 8a848f3a..fda94669 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java @@ -26,12 +26,12 @@ import com.yubico.yubikit.fido.ctap.Ctap2Session; public class Ctap2ClientPinTests { - public static void testClientPin(Ctap2Session session) throws Throwable { + public static void testClientPin(Ctap2Session session, FidoTestState state) throws Throwable { Integer permissions = ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA; String permissionRpId = "localhost"; - ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); - assertThat(pin.getPinUvAuth().getVersion(), is(TestData.PIN_UV_AUTH_PROTOCOL.getVersion())); + ClientPin pin = new ClientPin(session, state.getPinUvAuthProtocol()); + assertThat(pin.getPinUvAuth().getVersion(), is(state.getPinUvAuthProtocol().getVersion())); assertThat(pin.getPinRetries().getCount(), is(8)); pin.changePin(TestData.PIN, TestData.OTHER_PIN); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java index f7b2d077..6e9fbb9c 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java @@ -36,40 +36,40 @@ public class Ctap2ConfigTests { - static Config getConfig(Ctap2Session session) throws IOException, CommandException { - ClientPin clientPin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); + static Config getConfig(Ctap2Session session, FidoTestState state) throws IOException, CommandException { + ClientPin clientPin = new ClientPin(session, state.getPinUvAuthProtocol()); byte[] pinToken = clientPin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_ACFG, null); - return new Config(session, TestData.PIN_UV_AUTH_PROTOCOL, pinToken); + return new Config(session, state.getPinUvAuthProtocol(), pinToken); } - public static void testReadWriteEnterpriseAttestation(Ctap2Session session) throws Throwable { + public static void testReadWriteEnterpriseAttestation(Ctap2Session session, FidoTestState state) throws Throwable { assumeTrue("Enterprise attestation not supported", session.getInfo().getOptions().containsKey("ep")); - Config config = getConfig(session); + Config config = getConfig(session, state); config.enableEnterpriseAttestation(); assertSame(TRUE, session.getInfo().getOptions().get("ep")); } - public static void testToggleAlwaysUv(Ctap2Session session) throws Throwable { + public static void testToggleAlwaysUv(Ctap2Session session, FidoTestState state) throws Throwable { assumeTrue("Device does not support alwaysUv", session.getInfo().getOptions().containsKey("alwaysUv")); - Config config = getConfig(session); + Config config = getConfig(session, state); Object alwaysUv = getAlwaysUv(session); config.toggleAlwaysUv(); assertNotSame(getAlwaysUv(session), alwaysUv); } - public static void testSetForcePinChange(Ctap2Session session) throws Throwable { + public static void testSetForcePinChange(Ctap2Session session, FidoTestState state) throws Throwable { assumeTrue("authenticatorConfig not supported", Config.isSupported(session.getCachedInfo())); assumeFalse("Force PIN change already set. Reset key and retry", session.getInfo().getForcePinChange()); - Config config = getConfig(session); + Config config = getConfig(session, state); config.setMinPinLength(null, null, true); assertTrue(session.getInfo().getForcePinChange()); // set a new PIN - ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); - assertThat(pin.getPinUvAuth().getVersion(), is(TestData.PIN_UV_AUTH_PROTOCOL.getVersion())); + ClientPin pin = new ClientPin(session, state.getPinUvAuthProtocol()); + assertThat(pin.getPinUvAuth().getVersion(), is(state.getPinUvAuthProtocol().getVersion())); assertThat(pin.getPinRetries().getCount(), is(8)); pin.changePin(TestData.PIN, TestData.OTHER_PIN); @@ -81,10 +81,10 @@ public static void testSetForcePinChange(Ctap2Session session) throws Throwable } - public static void testSetMinPinLength(Ctap2Session session) throws Throwable { + public static void testSetMinPinLength(Ctap2Session session, FidoTestState state) throws Throwable { assumeTrue("authenticatorConfig not supported", Config.isSupported(session.getCachedInfo())); - Config config = getConfig(session); + Config config = getConfig(session, state); // after calling this the key must be reset to get the default min pin length value config.setMinPinLength(50, null, null); assertEquals(50, session.getInfo().getMinPinLength()); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java index d4871bd2..66e821e1 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java @@ -52,13 +52,13 @@ public static void deleteAllCredentials(CredentialManagement credentialManagemen } private static CredentialManagement setupCredentialManagement( - Ctap2Session session + Ctap2Session session, FidoTestState state ) throws IOException, CommandException { assumeTrue("Credential management not supported", CredentialManagement.isSupported(session.getCachedInfo())); - ClientPin clientPin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); + ClientPin clientPin = new ClientPin(session, state.getPinUvAuthProtocol()); return new CredentialManagement( session, @@ -67,8 +67,8 @@ private static CredentialManagement setupCredentialManagement( ); } - public static void testReadMetadata(Ctap2Session session) throws Throwable { - CredentialManagement credentialManagement = setupCredentialManagement(session); + public static void testReadMetadata(Ctap2Session session, FidoTestState state) throws Throwable { + CredentialManagement credentialManagement = setupCredentialManagement(session, state); CredentialManagement.Metadata metadata = credentialManagement.getMetadata(); @@ -76,9 +76,9 @@ public static void testReadMetadata(Ctap2Session session) throws Throwable { assertThat(metadata.getMaxPossibleRemainingResidentCredentialsCount(), greaterThan(0)); } - public static void testManagement(Ctap2Session session) throws Throwable { + public static void testManagement(Ctap2Session session, FidoTestState state) throws Throwable { - CredentialManagement credentialManagement = setupCredentialManagement(session); + CredentialManagement credentialManagement = setupCredentialManagement(session, state); final SerializationType cborType = SerializationType.CBOR; @@ -99,14 +99,14 @@ public static void testManagement(Ctap2Session session) throws Throwable { null, options, pinAuth, - TestData.PIN_UV_AUTH_PROTOCOL.getVersion(), + state.getPinUvAuthProtocol().getVersion(), null, null ); // this sets correct permission for handling credential management commands - credentialManagement = setupCredentialManagement(session); + credentialManagement = setupCredentialManagement(session, state); List rps = credentialManagement.enumerateRps(); assertThat(rps.size(), equalTo(1)); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java index 239ae70e..9ecb93d1 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java @@ -17,6 +17,7 @@ package com.yubico.yubikit.testing.fido; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; @@ -40,7 +41,7 @@ public class Ctap2SessionTests { - public static void testCtap2GetInfo(Ctap2Session session, Object... ignoredArgs) { + public static void testCtap2GetInfo(Ctap2Session session, FidoTestState state) { Ctap2Session.InfoData info = session.getCachedInfo(); List versions = info.getVersions(); @@ -64,15 +65,24 @@ public static void testCtap2GetInfo(Ctap2Session session, Object... ignoredArgs) // Check PIN/UV Auth protocol List pinUvAuthProtocols = info.getPinUvAuthProtocols(); assertThat("Number of PIN protocols incorrect", pinUvAuthProtocols.size(), greaterThanOrEqualTo(1)); - assertTrue("PIN protocol incorrect", pinUvAuthProtocols.contains(1)); + + if (state.isFipsApproved() && !state.isUsbTransport()) { + // FIPS only supports PIN/UV Auth protocol 2 over NFC + assertThat("Number of PIN protocols incorrect", pinUvAuthProtocols.size(), equalTo(1)); + assertTrue("PIN protocol incorrect", pinUvAuthProtocols.contains(2)); + } else { + // we expect at least protocol 1 to be present + assertThat("Number of PIN protocols incorrect", pinUvAuthProtocols.size(), greaterThanOrEqualTo(1)); + assertTrue("PIN protocol incorrect", pinUvAuthProtocols.contains(1)); + } } - public static void testCancelCborCommandImmediate(Ctap2Session session) throws Throwable { - doTestCancelCborCommand(session, false); + public static void testCancelCborCommandImmediate(Ctap2Session session, FidoTestState state) throws Throwable { + doTestCancelCborCommand(session, state, false); } - public static void testCancelCborCommandAfterDelay(Ctap2Session session) throws Throwable { - doTestCancelCborCommand(session, true); + public static void testCancelCborCommandAfterDelay(Ctap2Session session, FidoTestState state) throws Throwable { + doTestCancelCborCommand(session, state, true); } public static void testReset(Ctap2Session session) throws Throwable { @@ -89,12 +99,13 @@ public static void testReset(Ctap2Session session) throws Throwable { private static void doTestCancelCborCommand( Ctap2Session session, + FidoTestState testState, boolean delay ) throws Throwable { - assumeTrue("Not a USB connection", TestData.TRANSPORT_USB); + assumeTrue("Not a USB connection", testState.isUsbTransport()); - ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); + ClientPin pin = new ClientPin(session, testState.getPinUvAuthProtocol()); byte[] pinToken = pin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_MC, TestData.RP.getId()); byte[] pinAuth = pin.getPinUvAuth().authenticate(pinToken, TestData.CLIENT_DATA_HASH); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java index d02d6bd8..a5e0d872 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java @@ -48,24 +48,24 @@ public class EnterpriseAttestationTests { - static void enableEp(Ctap2Session session) + static void enableEp(Ctap2Session session, FidoTestState state) throws CommandException, IOException { // enable ep if not enabled if (session.getCachedInfo().getOptions().get("ep") == FALSE) { - ClientPin clientPin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); + ClientPin clientPin = new ClientPin(session, state.getPinUvAuthProtocol()); byte[] pinToken = clientPin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_ACFG, null); - final Config config = new Config(session, TestData.PIN_UV_AUTH_PROTOCOL, pinToken); + final Config config = new Config(session, state.getPinUvAuthProtocol(), pinToken); config.enableEnterpriseAttestation(); } } // test with RP ID in platform RP ID list - public static void testSupportedPlatformManagedEA(Ctap2Session session) throws Throwable { + public static void testSupportedPlatformManagedEA(Ctap2Session session, FidoTestState state) throws Throwable { assumeTrue("Enterprise attestation not supported", session.getCachedInfo().getOptions().containsKey("ep")); - enableEp(session); + enableEp(session, state); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); webauthn.getUserAgentConfiguration().setEpSupportedRpIds(Collections.singletonList(TestData.RP_ID)); @@ -77,11 +77,11 @@ public static void testSupportedPlatformManagedEA(Ctap2Session session) throws T } // test with RP ID which is not in platform RP ID list - public static void testUnsupportedPlatformManagedEA(Ctap2Session session) throws Throwable { + public static void testUnsupportedPlatformManagedEA(Ctap2Session session, FidoTestState state) throws Throwable { assumeTrue("Enterprise attestation not supported", session.getCachedInfo().getOptions().containsKey("ep")); // Ctap2ClientPinTests.ensureDefaultPinSet(session); - enableEp(session); + enableEp(session, state); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 2); @@ -92,11 +92,11 @@ public static void testUnsupportedPlatformManagedEA(Ctap2Session session) throws FALSE.equals(attestationObject.get("epAtt"))); } - public static void testVendorFacilitatedEA(Ctap2Session session) throws Throwable { + public static void testVendorFacilitatedEA(Ctap2Session session, FidoTestState state) throws Throwable { assumeTrue("Enterprise attestation not supported", session.getCachedInfo().getOptions().containsKey("ep")); // Ctap2ClientPinTests.ensureDefaultPinSet(session); - enableEp(session); + enableEp(session, state); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); webauthn.getUserAgentConfiguration().setEpSupportedRpIds(Collections.singletonList(TestData.RP_ID)); @@ -109,11 +109,11 @@ public static void testVendorFacilitatedEA(Ctap2Session session) throws Throwabl // test with different PublicKeyCredentialCreationOptions AttestationConveyancePreference // values - public static void testCreateOptionsAttestationPreference(Ctap2Session session) throws Throwable { + public static void testCreateOptionsAttestationPreference(Ctap2Session session, FidoTestState state) throws Throwable { assumeTrue("Enterprise attestation not supported", session.getCachedInfo().getOptions().containsKey("ep")); // Ctap2ClientPinTests.ensureDefaultPinSet(session); - enableEp(session); + enableEp(session, state); // setup BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java similarity index 54% rename from testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestUtils.java rename to testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java index 128176a2..756159b0 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java @@ -16,67 +16,71 @@ package com.yubico.yubikit.testing.fido; +import static com.yubico.yubikit.testing.TestUtils.getCtap2Session; +import static com.yubico.yubikit.testing.TestUtils.openConnection; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; 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.CommandException; -import com.yubico.yubikit.core.fido.FidoConnection; -import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.fido.client.BasicWebAuthnClient; import com.yubico.yubikit.fido.client.ClientError; import com.yubico.yubikit.fido.client.CredentialManager; import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.Config; +import com.yubico.yubikit.fido.ctap.CredentialManagement; import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialUserEntity; import com.yubico.yubikit.management.Capability; import com.yubico.yubikit.management.DeviceInfo; -import com.yubico.yubikit.management.ManagementSession; +import com.yubico.yubikit.support.DeviceUtil; +import com.yubico.yubikit.testing.TestState; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Objects; -public class FidoTestUtils { +import javax.annotation.Nullable; - /** - * Prepares the device for a test and sets variables in {@link TestData}. If {@code setPin} - * is true, then sets the FIDO2 PIN, verifies FIPS approval state and removes existing - * credentials. - * - * @param device The YubiKey which should be setup for a test. - * @param pinUvAuthProtocol Pin UV/Auth protocol to use during the test. - * @param setPin Whether a FIDO2 PIN should be set. Several tests need a device without a PIN. - * - * @throws Throwable if an error occurred - */ - public static void verifyAndSetup( +public class FidoTestState extends TestState { + + private final PinUvAuthProtocol pinUvAuthProtocol; + private final boolean isFipsApproved; + public Boolean alwaysUv = false; + + public FidoTestState( YubiKeyDevice device, + ReconnectDeviceCallback reconnect, + @Nullable Byte scpKid, PinUvAuthProtocol pinUvAuthProtocol, boolean setPin) throws Throwable { + super(device, reconnect, scpKid); - boolean isFidoFipsCapable; + boolean isFidoFipsCapable = false; + DeviceInfo deviceInfo = null; try (YubiKeyConnection connection = openConnection(device)) { + try { + deviceInfo = DeviceUtil.readInfo(connection, null); + assertNotNull(deviceInfo); + isFidoFipsCapable = + (deviceInfo.getFipsCapable() & Capability.FIDO2.bit) == Capability.FIDO2.bit; - ManagementSession managementSession = getManagementSession(connection); - DeviceInfo deviceInfo = managementSession.getDeviceInfo(); - assertNotNull(deviceInfo); - - isFidoFipsCapable = - (deviceInfo.getFipsCapable() & Capability.FIDO2.bit) == Capability.FIDO2.bit; + assumeTrue("This YubiKey does not support FIDO2", + deviceInfo.getVersion().isAtLeast(5, 0, 0)); + } catch (IllegalArgumentException ignored) { + // failed to get device info, this is not a YubiKey + } - Ctap2Session session = getCtap2Session(connection); + Ctap2Session session = getCtap2Session(connection, scpParameters); assumeTrue( "PIN UV Protocol not supported", supportsPinUvAuthProtocol(session, pinUvAuthProtocol)); @@ -86,90 +90,70 @@ public static void verifyAndSetup( pinUvAuthProtocol.getVersion() == 2); } - TestData.PIN_UV_AUTH_PROTOCOL = pinUvAuthProtocol; - TestData.TRANSPORT_USB = device.getTransport() == Transport.USB; + this.pinUvAuthProtocol = pinUvAuthProtocol; if (setPin) { verifyOrSetPin(session); } - if (isFidoFipsCapable && - Boolean.FALSE.equals(session.getInfo().getOptions().get("alwaysUv"))) { + this.alwaysUv = (Boolean) session.getInfo().getOptions().get("alwaysUv"); + if (isFidoFipsCapable && Boolean.FALSE.equals(this.alwaysUv)) { // set always UV on - Config config = Ctap2ConfigTests.getConfig(session); + Config config = Ctap2ConfigTests.getConfig(session, this); config.toggleAlwaysUv(); + this.alwaysUv = true; } - managementSession = getManagementSession(connection); - deviceInfo = managementSession.getDeviceInfo(); - TestData.FIPS_APPROVED = - (deviceInfo.getFipsApproved() & Capability.FIDO2.bit) == Capability.FIDO2.bit; + boolean fipsApproved = false; + + try { + deviceInfo = DeviceUtil.readInfo(connection, null); + fipsApproved = + (deviceInfo.getFipsApproved() & Capability.FIDO2.bit) == Capability.FIDO2.bit; + } catch (IllegalArgumentException ignored) { + // not a YubiKey + } - // after changing the user and admin PINs, we expect a FIPS capable device + this.isFipsApproved = fipsApproved; + + // after changing the PIN and setting alwaysUv, we expect a FIPS capable device // to be FIPS approved if (setPin && isFidoFipsCapable) { assertNotNull(deviceInfo); - assertTrue("Device not FIDO FIPS approved as expected", TestData.FIPS_APPROVED); + assertTrue("Device not FIDO FIPS approved as expected", this.isFipsApproved); } // remove existing credentials if (setPin) { // cannot use CredentialManager if there is no PIN set + session = getCtap2Session(connection, scpParameters); deleteExistingCredentials(session); } } } - private static boolean supportsPinUvAuthProtocol( - Ctap2Session session, - PinUvAuthProtocol pinUvAuthProtocol) { - final List pinUvAuthProtocols = session.getCachedInfo().getPinUvAuthProtocols(); - return pinUvAuthProtocols.contains(pinUvAuthProtocol.getVersion()); - } - - private 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"); + public boolean isFipsApproved() { + return isFipsApproved; } - private static Ctap2Session getCtap2Session(YubiKeyConnection connection) - throws IOException, CommandException { - Ctap2Session session = (connection instanceof FidoConnection) - ? new Ctap2Session((FidoConnection) connection) - : connection instanceof SmartCardConnection - ? new Ctap2Session((SmartCardConnection) connection) - : null; - - if (session == null) { - throw new IllegalArgumentException("Connection does not support Ctap2Session"); - } - - return session; + public PinUvAuthProtocol getPinUvAuthProtocol() { + return pinUvAuthProtocol; } - private static ManagementSession getManagementSession(YubiKeyConnection connection) throws IOException, CommandException { - ManagementSession session = (connection instanceof FidoConnection) - ? new ManagementSession((FidoConnection) connection) - : connection instanceof SmartCardConnection - ? new ManagementSession((SmartCardConnection) connection) - : null; - - if (session == null) { - throw new IllegalArgumentException("Connection does not support ManagementSession"); - } - - return session; + boolean supportsPinUvAuthProtocol( + Ctap2Session session, + PinUvAuthProtocol pinUvAuthProtocol) { + final List pinUvAuthProtocols = session.getCachedInfo().getPinUvAuthProtocols(); + return pinUvAuthProtocols.contains(pinUvAuthProtocol.getVersion()); } - private static void deleteExistingCredentials(Ctap2Session session) + void deleteExistingCredentials(Ctap2Session session) throws IOException, CommandException, ClientError { final BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - final CredentialManager credentialManager = webauthn.getCredentialManager(TestData.PIN); + if (!CredentialManagement.isSupported(session.getCachedInfo())) { + return; + } + CredentialManager credentialManager = webauthn.getCredentialManager(TestData.PIN); final List rpIds = credentialManager.getRpIdList(); for (String rpId : rpIds) { Map credentials @@ -184,11 +168,11 @@ private static void deleteExistingCredentials(Ctap2Session session) /** * Attempts to set (or verify) the default PIN, or fails. */ - private static void verifyOrSetPin(Ctap2Session session) throws IOException, CommandException { + void verifyOrSetPin(Ctap2Session session) throws IOException, CommandException { Ctap2Session.InfoData info = session.getInfo(); - ClientPin pin = new ClientPin(session, TestData.PIN_UV_AUTH_PROTOCOL); + ClientPin pin = new ClientPin(session, pinUvAuthProtocol); boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); try { @@ -205,4 +189,32 @@ private static void verifyOrSetPin(Ctap2Session session) throws IOException, Com "and try again."); } } + + public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { + callback.invoke(this); + } + + public void withCtap2(SessionCallback callback) throws Throwable { + try (YubiKeyConnection connection = openConnection(currentDevice)) { + callback.invoke(getCtap2Session(connection, scpParameters)); + } + reconnect(); + } + + public void withCtap2(StatefulSessionCallback callback) throws Throwable { + try (YubiKeyConnection connection = openConnection(currentDevice)) { + callback.invoke(getCtap2Session(connection, scpParameters), (FidoTestState) this); + } + reconnect(); + } + + public T withCtap2(SessionCallbackT callback) throws Throwable { + T result; + try (YubiKeyConnection connection = openConnection(currentDevice)) { + result = callback.invoke(getCtap2Session(connection, scpParameters)); + } + reconnect(); + return result; + } + } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java index 69d5b8b1..5684899f 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java @@ -17,7 +17,6 @@ package com.yubico.yubikit.testing.fido; import com.squareup.moshi.Moshi; -import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialParameters; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRpEntity; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialType; @@ -43,11 +42,6 @@ public ClientData(String type, String origin, byte[] challenge, String androidPa } } - // state - public static PinUvAuthProtocol PIN_UV_AUTH_PROTOCOL; - public static boolean TRANSPORT_USB = false; - public static boolean FIPS_APPROVED = false; - public static final char[] PIN = "11234567".toCharArray(); public static final char[] OTHER_PIN = "11231234".toCharArray(); From 18856b2331549a80eb6d37ec462b63219b7cc810 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 22 Jul 2024 16:57:19 +0200 Subject: [PATCH 28/46] inject state into tests --- .../yubikit/fido/ctap/Ctap2Session.java | 33 ++----- .../framework/FidoInstrumentedTests.java | 21 ++++- .../framework/PivInstrumentedTests.java | 2 +- .../com/yubico/yubikit/testing/TestState.java | 2 +- .../yubikit/testing/fido/FidoTestState.java | 87 ++++++++++++------- .../yubikit/testing/oath/OathDeviceTests.java | 1 + .../yubikit/testing/oath/OathTestState.java | 8 ++ .../testing/openpgp/OpenPgpDeviceTests.java | 4 - .../testing/openpgp/OpenPgpTestState.java | 10 +-- .../yubikit/testing/piv/PivTestState.java | 12 +-- 10 files changed, 105 insertions(+), 75 deletions(-) diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java index 0d6ab6a8..622a8adb 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java @@ -104,19 +104,6 @@ public static void create(YubiKeyDevice device, Callback backend) } } - private static Backend getSmartCardBackend(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) + private static Backend getSmartCardBackend(SmartCardConnection connection) throws IOException, ApplicationNotAvailableException { final SmartCardProtocol protocol = new SmartCardProtocol(connection); protocol.select(AppId.FIDO); - if (scpKeyParams != null) { - try { - protocol.initScp(scpKeyParams); - } catch (ApduException | BadResponseException e) { - throw new IOException("Failed setting up SCP session", e); - } - } return new Backend(protocol) { byte[] sendCbor(byte[] data, @Nullable CommandState state) throws IOException, CommandException { diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java index 86f37840..8e830024 100755 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java @@ -33,17 +33,32 @@ protected void withDevice(TestState.StatefulDeviceCallback callba } protected void withDevice(boolean setPin, TestState.StatefulDeviceCallback callback) throws Throwable { - FidoTestState state = new FidoTestState(device, this::reconnectDevice, getScpKid(), getPinUvAuthProtocol(), setPin); + FidoTestState state = new FidoTestState.Builder(device, getPinUvAuthProtocol()) + .scpKid(getScpKid()) + .reconnectDeviceCallback(this::reconnectDevice) + .setPin(setPin) + .build(); + state.withDeviceCallback(callback); } protected void withCtap2Session(TestState.SessionCallback callback) throws Throwable { - FidoTestState state = new FidoTestState(device, this::reconnectDevice, getScpKid(), getPinUvAuthProtocol(), true); + FidoTestState state = new FidoTestState.Builder(device, getPinUvAuthProtocol()) + .scpKid(getScpKid()) + .reconnectDeviceCallback(this::reconnectDevice) + .setPin(true) + .build(); + state.withCtap2(callback); } protected void withCtap2Session(TestState.StatefulSessionCallback callback) throws Throwable { - FidoTestState state = new FidoTestState(device, this::reconnectDevice, getScpKid(), getPinUvAuthProtocol(), true); + FidoTestState state = new FidoTestState.Builder(device, getPinUvAuthProtocol()) + .scpKid(getScpKid()) + .reconnectDeviceCallback(this::reconnectDevice) + .setPin(true) + .build(); + state.withCtap2(callback); } 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 05d767bf..d18c2361 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 @@ -25,4 +25,4 @@ protected void withPivSession(TestState.StatefulSessionCallback builder) { this.scpParameters = new ScpParameters(builder.device, this.scpKid); this.reconnectDeviceCallback = builder.reconnectDeviceCallback; this.isUsbTransport = builder.device.getTransport() == Transport.USB; - } + } public boolean isUsbTransport() { return isUsbTransport; diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java index 756159b0..1a19e11b 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java @@ -16,8 +16,6 @@ package com.yubico.yubikit.testing.fido; -import static com.yubico.yubikit.testing.TestUtils.getCtap2Session; -import static com.yubico.yubikit.testing.TestUtils.openConnection; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -27,6 +25,8 @@ import com.yubico.yubikit.core.YubiKeyConnection; 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.smartcard.SmartCardConnection; import com.yubico.yubikit.fido.client.BasicWebAuthnClient; import com.yubico.yubikit.fido.client.ClientError; import com.yubico.yubikit.fido.client.CredentialManager; @@ -53,21 +53,37 @@ public class FidoTestState extends TestState { private final PinUvAuthProtocol pinUvAuthProtocol; private final boolean isFipsApproved; - public Boolean alwaysUv = false; + public final boolean alwaysUv; - public FidoTestState( - YubiKeyDevice device, - ReconnectDeviceCallback reconnect, - @Nullable Byte scpKid, - PinUvAuthProtocol pinUvAuthProtocol, - boolean setPin) - throws Throwable { - super(device, reconnect, scpKid); + public static class Builder extends TestState.Builder { + + private final PinUvAuthProtocol pinUvAuthProtocol; + private boolean setPin = false; + + public Builder(YubiKeyDevice device, PinUvAuthProtocol pinUvAuthProtocol) { + super(device); + this.pinUvAuthProtocol = pinUvAuthProtocol; + } + + public Builder setPin(boolean setPin) { + this.setPin = setPin; + return this; + } + + public FidoTestState build() throws Throwable { + return new FidoTestState(this); + } + } + + private FidoTestState(Builder builder) throws Throwable { + super(builder); + + this.pinUvAuthProtocol = builder.pinUvAuthProtocol; boolean isFidoFipsCapable = false; DeviceInfo deviceInfo = null; - try (YubiKeyConnection connection = openConnection(device)) { + try (YubiKeyConnection connection = openConnection()) { try { deviceInfo = DeviceUtil.readInfo(connection, null); assertNotNull(deviceInfo); @@ -80,7 +96,7 @@ public FidoTestState( // failed to get device info, this is not a YubiKey } - Ctap2Session session = getCtap2Session(connection, scpParameters); + Ctap2Session session = getCtap2Session(connection); assumeTrue( "PIN UV Protocol not supported", supportsPinUvAuthProtocol(session, pinUvAuthProtocol)); @@ -90,22 +106,21 @@ public FidoTestState( pinUvAuthProtocol.getVersion() == 2); } - this.pinUvAuthProtocol = pinUvAuthProtocol; - - if (setPin) { + if (builder.setPin) { verifyOrSetPin(session); } - this.alwaysUv = (Boolean) session.getInfo().getOptions().get("alwaysUv"); - if (isFidoFipsCapable && Boolean.FALSE.equals(this.alwaysUv)) { + @Nullable + Boolean alwaysUv = (Boolean) session.getInfo().getOptions().get("alwaysUv"); + if (isFidoFipsCapable && Boolean.FALSE.equals(alwaysUv)) { // set always UV on Config config = Ctap2ConfigTests.getConfig(session, this); config.toggleAlwaysUv(); - this.alwaysUv = true; + alwaysUv = true; } + this.alwaysUv = Boolean.TRUE.equals(alwaysUv); boolean fipsApproved = false; - try { deviceInfo = DeviceUtil.readInfo(connection, null); fipsApproved = @@ -118,15 +133,15 @@ public FidoTestState( // after changing the PIN and setting alwaysUv, we expect a FIPS capable device // to be FIPS approved - if (setPin && isFidoFipsCapable) { + if (builder.setPin && isFidoFipsCapable) { assertNotNull(deviceInfo); assertTrue("Device not FIDO FIPS approved as expected", this.isFipsApproved); } // remove existing credentials - if (setPin) { + if (builder.setPin) { // cannot use CredentialManager if there is no PIN set - session = getCtap2Session(connection, scpParameters); + session = getCtap2Session(connection); deleteExistingCredentials(session); } } @@ -195,26 +210,40 @@ public void withDeviceCallback(StatefulDeviceCallback callback) t } public void withCtap2(SessionCallback callback) throws Throwable { - try (YubiKeyConnection connection = openConnection(currentDevice)) { - callback.invoke(getCtap2Session(connection, scpParameters)); + try (YubiKeyConnection connection = openConnection()) { + callback.invoke(getCtap2Session(connection)); } reconnect(); } public void withCtap2(StatefulSessionCallback callback) throws Throwable { - try (YubiKeyConnection connection = openConnection(currentDevice)) { - callback.invoke(getCtap2Session(connection, scpParameters), (FidoTestState) this); + try (YubiKeyConnection connection = openConnection()) { + callback.invoke(getCtap2Session(connection), this); } reconnect(); } public T withCtap2(SessionCallbackT callback) throws Throwable { T result; - try (YubiKeyConnection connection = openConnection(currentDevice)) { - result = callback.invoke(getCtap2Session(connection, scpParameters)); + try (YubiKeyConnection connection = openConnection()) { + result = callback.invoke(getCtap2Session(connection)); } reconnect(); return result; } + private Ctap2Session getCtap2Session(YubiKeyConnection connection) + throws IOException, CommandException { + Ctap2Session session = (connection instanceof FidoConnection) + ? new Ctap2Session((FidoConnection) connection) + : connection instanceof SmartCardConnection + ? new Ctap2Session((SmartCardConnection) connection) + : null; + + if (session == null) { + throw new IllegalArgumentException("Connection does not support Ctap2Session"); + } + + return session; + } } 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 bcac4d47..49a8c562 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 @@ -22,6 +22,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.core.smartcard.SW; import com.yubico.yubikit.oath.Credential; diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java index 8e2972b9..a4bce027 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java @@ -19,13 +19,21 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; +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.CommandException; +import com.yubico.yubikit.core.fido.FidoConnection; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.management.Capability; import com.yubico.yubikit.oath.OathSession; +import com.yubico.yubikit.testing.ScpParameters; import com.yubico.yubikit.testing.TestState; +import java.io.IOException; + +import javax.annotation.Nullable; + public class OathTestState extends TestState { public boolean isFipsApproved; public char[] password; 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 41470ece..5fbddc98 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,10 +16,6 @@ package com.yubico.yubikit.testing.openpgp; -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; 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 b3582e3a..f5822844 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 @@ -21,15 +21,15 @@ 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.ScpParameters; import com.yubico.yubikit.testing.TestState; import org.junit.Assume; +import java.io.IOException; + +import javax.annotation.Nullable; + public class OpenPgpTestState extends TestState { private static final char[] COMPLEX_USER_PIN = "112345678".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 cfb77863..3f0909e1 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 @@ -29,8 +29,13 @@ import com.yubico.yubikit.piv.KeyType; import com.yubico.yubikit.piv.ManagementKeyType; import com.yubico.yubikit.piv.PivSession; +import com.yubico.yubikit.testing.ScpParameters; import com.yubico.yubikit.testing.TestState; +import java.io.IOException; + +import javax.annotation.Nullable; + public class PivTestState extends TestState { static final char[] DEFAULT_PIN = "123456".toCharArray(); @@ -75,10 +80,6 @@ protected PivTestState(Builder builder) throws Throwable { assumeTrue("No SmartCard support", currentDevice.supportsConnection(SmartCardConnection.class)); DeviceInfo deviceInfo = getDeviceInfo(); - - // skip MPE devices - assumeFalse("Ignoring MPE device", isMpe(deviceInfo)); - boolean isPivFipsCapable = isFipsCapable(deviceInfo, Capability.PIV); boolean hasPinComplexity = deviceInfo != null && deviceInfo.getPinComplexity(); @@ -94,13 +95,14 @@ protected PivTestState(Builder builder) throws Throwable { ); } - try (SmartCardConnection connection = openSmartCardConnection()) { + try (SmartCardConnection connection = currentDevice.openConnection(SmartCardConnection.class)) { PivSession pivSession = getPivSession(connection, scpParameters); assumeTrue("PIV not available", pivSession != null); try { pivSession.reset(); } catch (Exception ignored) { + } if (hasPinComplexity) { From 69eaa0772166c66f34ceb9898eda03c99222bca4 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 22 Jul 2024 17:13:49 +0200 Subject: [PATCH 29/46] self review --- .../java/com/yubico/yubikit/fido/ctap/Ctap2Session.java | 6 ------ .../java/com/yubico/yubikit/testing/SmokeDeviceTests.java | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java index 622a8adb..f1bc11b2 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java @@ -28,11 +28,9 @@ import com.yubico.yubikit.core.fido.FidoProtocol; import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.smartcard.Apdu; -import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.core.smartcard.AppId; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.core.smartcard.SmartCardProtocol; -import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Result; import com.yubico.yubikit.core.util.StringUtils; @@ -164,10 +162,6 @@ byte[] sendCbor(byte[] data, @Nullable CommandState state) } private Ctap2Session(FidoProtocol protocol) throws IOException, CommandException { - this(protocol, null); - } - - private Ctap2Session(FidoProtocol protocol, @Nullable ScpKeyParams scpKeyParams) throws IOException, CommandException { this(protocol.getVersion(), new Backend(protocol) { @Override byte[] sendCbor(byte[] data, @Nullable CommandState state) throws IOException { diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java index ab04822d..f13335bc 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/SmokeDeviceTests.java @@ -29,4 +29,4 @@ }) @Suite.SuiteClasses(DeviceTests.class) public class SmokeDeviceTests { -} +} \ No newline at end of file From 3997845cdede20bdd419aa03e00ce2bbe7e53cff Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 23 Jul 2024 15:29:50 +0200 Subject: [PATCH 30/46] rebase fixes --- .../fido/Ctap2ClientPinInstrumentedTests.java | 3 +- .../Ctap2SessionResetInstrumentedTests.java | 30 ++++++++-- .../yubikit/testing/openpgp/OpenPgpTests.java | 3 - .../framework/FidoInstrumentedTests.java | 10 ---- .../framework/YKInstrumentedTests.java | 12 ---- .../com/yubico/yubikit/testing/TestState.java | 20 +++++++ .../fido/BasicWebAuthnClientTests.java | 11 ++-- .../testing/fido/Ctap2ClientPinTests.java | 54 +++++++++++++++++ .../testing/fido/Ctap2SessionTests.java | 19 +++--- .../yubikit/testing/fido/FidoTestState.java | 48 +-------------- .../testing/openpgp/OpenPgpDeviceTests.java | 58 ++++++++++--------- .../testing/openpgp/OpenPgpTestState.java | 10 ++-- 12 files changed, 154 insertions(+), 124 deletions(-) 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 02286d38..1b81dde3 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 @@ -18,7 +18,6 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; -import com.yubico.yubikit.testing.PinComplexityDeviceTests; import com.yubico.yubikit.testing.PinUvAuthProtocolV1Test; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; @@ -41,7 +40,7 @@ public void testClientPin() throws Throwable { @Test public void testPinComplexity() throws Throwable { - withDevice(PinComplexityDeviceTests::testFido2PinComplexity); + withDevice(Ctap2ClientPinTests::testPinComplexity); } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java index 3c820e5d..f975daf5 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2SessionResetInstrumentedTests.java @@ -16,11 +16,16 @@ package com.yubico.yubikit.testing.fido; +import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; +import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1; import com.yubico.yubikit.testing.AlwaysManualTest; +import com.yubico.yubikit.testing.PinUvAuthProtocolV1Test; import com.yubico.yubikit.testing.framework.FidoInstrumentedTests; import org.junit.Test; import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; /** * Tests FIDO Reset. @@ -31,10 +36,25 @@ *
  • YubiKey Bio devices are currently ignored.
  • * */ -public class Ctap2SessionResetInstrumentedTests extends FidoInstrumentedTests { - @Test - @Category(AlwaysManualTest.class) - public void testReset() throws Throwable { - withCtap2Session(Ctap2SessionTests::testReset); +@RunWith(Suite.class) +@Suite.SuiteClasses({ + Ctap2SessionResetInstrumentedTests.PinUvAuthV2Test.class, + Ctap2SessionResetInstrumentedTests.PinUvAuthV1Test.class, +}) +public class Ctap2SessionResetInstrumentedTests { + public static class PinUvAuthV2Test extends FidoInstrumentedTests { + @Test + @Category(AlwaysManualTest.class) + public void testReset() throws Throwable { + withDevice(false, Ctap2SessionTests::testReset); + } + } + + @Category(PinUvAuthProtocolV1Test.class) + public static class PinUvAuthV1Test extends PinUvAuthV2Test { + @Override + protected PinUvAuthProtocol getPinUvAuthProtocol() { + return new PinUvAuthProtocolV1(); + } } } 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 ef459921..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; diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java index 8e830024..75a2e977 100755 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/FidoInstrumentedTests.java @@ -42,16 +42,6 @@ protected void withDevice(boolean setPin, TestState.StatefulDeviceCallback callback) throws Throwable { - FidoTestState state = new FidoTestState.Builder(device, getPinUvAuthProtocol()) - .scpKid(getScpKid()) - .reconnectDeviceCallback(this::reconnectDevice) - .setPin(true) - .build(); - - state.withCtap2(callback); - } - protected void withCtap2Session(TestState.StatefulSessionCallback callback) throws Throwable { FidoTestState state = new FidoTestState.Builder(device, getPinUvAuthProtocol()) .scpKid(getScpKid()) 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 f052f9c2..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 @@ -69,18 +69,6 @@ protected YubiKeyDevice reconnectDevice() { } } - protected YubiKeyDevice reconnectDevice() { - try { - if (device.getTransport() == Transport.NFC) { - releaseYubiKey(); - getYubiKey(); - } - return device; - } catch (InterruptedException e) { - throw new RuntimeException("Failure during reconnect", e); - } - } - @Nullable protected Byte getScpKid() { return null; diff --git a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java index f2d52b6f..aaaca475 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -167,6 +167,26 @@ protected PivSession getPivSession(SmartCardConnection connection, ScpParameters } // CTAP2 helpers + public R withCtap2(SessionCallbackT callback) throws Throwable { + R result; + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + result = callback.invoke(ctap2); + } + reconnect(); + return result; + } + + public void withCtap2(SessionCallback callback) throws Throwable { + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + callback.invoke(ctap2); + } + reconnect(); + } + public void withCtap2(StatefulSessionCallback callback) throws Throwable { try (YubiKeyConnection connection = openConnection()) { diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java index 86d90695..501bfd0f 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java @@ -71,7 +71,7 @@ public class BasicWebAuthnClientTests { public static void testMakeCredentialGetAssertionTokenUvOnly(FidoTestState state) throws Throwable { - state.withCtap2((session) -> { + state.withCtap2(session -> { assumeTrue("UV Token not supported", ClientPin.isTokenSupported(session.getCachedInfo())); }); testMakeCredentialGetAssertion(state); @@ -81,7 +81,7 @@ public static void testMakeCredentialGetAssertion(FidoTestState state) throws Th List deleteCredIds = new ArrayList<>(); // Make a non rk credential - state.withCtap2((session) -> { + state.withCtap2(session -> { BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); PublicKeyCredentialCreationOptions creationOptionsNonRk = getCreateOptions( @@ -109,7 +109,7 @@ public static void testMakeCredentialGetAssertion(FidoTestState state) throws Th }); // make a rk credential - state.withCtap2((session) -> { + state.withCtap2(session -> { BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); PublicKeyCredentialCreationOptions creationOptionsRk = getCreateOptions( new PublicKeyCredentialUserEntity( @@ -136,7 +136,7 @@ public static void testMakeCredentialGetAssertion(FidoTestState state) throws Th }); // Get assertions - state.withCtap2((session) -> { + state.withCtap2(session -> { BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); PublicKeyCredentialRequestOptions requestOptions = new PublicKeyCredentialRequestOptions( TestData.CHALLENGE, @@ -186,7 +186,7 @@ public static void testUvDiscouragedMcGa_noPin(FidoTestState state) throws Throw private static void testUvDiscouragedMakeCredentialGetAssertion(FidoTestState state) throws Throwable { // Test non rk credential - PublicKeyCredential credNonRk = state.withCtap2((session) -> { + PublicKeyCredential credNonRk = state.withCtap2(session -> { BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); PublicKeyCredentialCreationOptions creationOptionsNonRk = getCreateOptions( @@ -446,7 +446,6 @@ public static void testGetAssertionWithAllowList(FidoTestState state) throws Thr ); }); - state.withCtap2(session -> { BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java index fda94669..58459321 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java @@ -16,14 +16,22 @@ package com.yubico.yubikit.testing.fido; +import static com.yubico.yubikit.core.fido.CtapException.ERR_PIN_POLICY_VIOLATION; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; 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 java.util.Objects; public class Ctap2ClientPinTests { public static void testClientPin(Ctap2Session session, FidoTestState state) throws Throwable { @@ -48,4 +56,50 @@ public static void testClientPin(Ctap2Session session, FidoTestState state) thro assertThat(pin.getPinRetries().getCount(), is(8)); pin.changePin(TestData.OTHER_PIN, TestData.PIN); } + + public static void testPinComplexity(FidoTestState 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()); + + state.withCtap2(session -> { + PinUvAuthProtocol pinUvAuthProtocol = new PinUvAuthProtocolV2(); + char[] defaultPin = "11234567".toCharArray(); + + Ctap2Session.InfoData info = session.getCachedInfo(); + ClientPin pin = new ClientPin(session, pinUvAuthProtocol); + boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); + + try { + if (!pinSet) { + pin.setPin(defaultPin); + } else { + pin.getPinToken( + defaultPin, + ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA, + "localhost"); + } + } catch (ApduException e) { + fail("Failed to set or use PIN. Reset the device and try again"); + } + + 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/fido/Ctap2SessionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java index 9ecb93d1..d38e9023 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java @@ -85,16 +85,19 @@ public static void testCancelCborCommandAfterDelay(Ctap2Session session, FidoTes doTestCancelCborCommand(session, state, true); } - public static void testReset(Ctap2Session session) throws Throwable { - assumeFalse("Skipping reset test - authenticator supports bio enrollment", - session.getCachedInfo().getOptions().containsKey("bioEnroll")); + public static void testReset(FidoTestState state) throws Throwable { - session.reset(null); + state.withCtap2(session -> { + assumeFalse("Skipping reset test - authenticator supports bio enrollment", + session.getCachedInfo().getOptions().containsKey("bioEnroll")); - // Verify that the pin is no longer configured - Boolean clientPin = (Boolean) session.getInfo().getOptions().get("clientPin"); - boolean pinConfigured = (clientPin != null) && clientPin; - assertFalse("PIN should not be configured after a reset", pinConfigured); + session.reset(null); + + // Verify that the pin is no longer configured + Boolean clientPin = (Boolean) session.getInfo().getOptions().get("clientPin"); + boolean pinConfigured = (clientPin != null) && clientPin; + assertFalse("PIN should not be configured after a reset", pinConfigured); + }); } private static void doTestCancelCborCommand( diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java index 1a19e11b..2e9294f1 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java @@ -25,8 +25,6 @@ import com.yubico.yubikit.core.YubiKeyConnection; 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.smartcard.SmartCardConnection; import com.yubico.yubikit.fido.client.BasicWebAuthnClient; import com.yubico.yubikit.fido.client.ClientError; import com.yubico.yubikit.fido.client.CredentialManager; @@ -97,8 +95,8 @@ private FidoTestState(Builder builder) throws Throwable { } Ctap2Session session = getCtap2Session(connection); - assumeTrue( - "PIN UV Protocol not supported", + assumeTrue("CTAP2 not supported", session != null); + assumeTrue("PIN UV Protocol not supported", supportsPinUvAuthProtocol(session, pinUvAuthProtocol)); if (isFidoFipsCapable) { @@ -204,46 +202,4 @@ void verifyOrSetPin(Ctap2Session session) throws IOException, CommandException { "and try again."); } } - - public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { - callback.invoke(this); - } - - public void withCtap2(SessionCallback callback) throws Throwable { - try (YubiKeyConnection connection = openConnection()) { - callback.invoke(getCtap2Session(connection)); - } - reconnect(); - } - - public void withCtap2(StatefulSessionCallback callback) throws Throwable { - try (YubiKeyConnection connection = openConnection()) { - callback.invoke(getCtap2Session(connection), this); - } - reconnect(); - } - - public T withCtap2(SessionCallbackT callback) throws Throwable { - T result; - try (YubiKeyConnection connection = openConnection()) { - result = callback.invoke(getCtap2Session(connection)); - } - reconnect(); - return result; - } - - private Ctap2Session getCtap2Session(YubiKeyConnection connection) - throws IOException, CommandException { - Ctap2Session session = (connection instanceof FidoConnection) - ? new Ctap2Session((FidoConnection) connection) - : connection instanceof SmartCardConnection - ? new Ctap2Session((SmartCardConnection) connection) - : null; - - if (session == null) { - throw new IllegalArgumentException("Connection does not support Ctap2Session"); - } - - return session; - } } 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 5fbddc98..b8a25f3e 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,6 +16,10 @@ package com.yubico.yubikit.testing.openpgp; +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; @@ -85,7 +89,7 @@ public static void testGenerateRequiresAdmin(OpenPgpSession openpgp, OpenPgpTest try { openpgp.generateEcKey(KeyRef.DEC, OpenPgpCurve.BrainpoolP256R1); - Assert.fail(); + fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -93,7 +97,7 @@ public static void testGenerateRequiresAdmin(OpenPgpSession openpgp, OpenPgpTest if (!state.isFipsApproved) { try { openpgp.generateRsaKey(KeyRef.DEC, 2048); - Assert.fail(); + fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -116,7 +120,7 @@ public static void testResetPin(OpenPgpSession openpgp, OpenPgpTestState state) for (int i = remaining; i > 0; i--) { try { openpgp.verifyUserPin(CHANGED_PIN, false); - Assert.fail(); + fail(); } catch (InvalidPinException e) { Assert.assertEquals(e.getAttemptsRemaining(), i - 1); } @@ -125,7 +129,7 @@ public static void testResetPin(OpenPgpSession openpgp, OpenPgpTestState state) try { openpgp.resetPin(state.defaultUserPin, null); - Assert.fail(); + fail(); } catch (ApduException e) { Assert.assertEquals(e.getSw(), SW.SECURITY_CONDITION_NOT_SATISFIED); } @@ -138,7 +142,7 @@ public static void testResetPin(OpenPgpSession openpgp, OpenPgpTestState state) for (int i = remaining; i > 0; i--) { try { openpgp.verifyUserPin(CHANGED_PIN, false); - Assert.fail(); + fail(); } catch (InvalidPinException e) { Assert.assertEquals(e.getAttemptsRemaining(), i - 1); } @@ -153,7 +157,7 @@ public static void testResetPin(OpenPgpSession openpgp, OpenPgpTestState state) } public static void testSetPinAttempts(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_PIN_ATTEMPTS)); + assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_PIN_ATTEMPTS)); openpgp.verifyAdminPin(state.defaultAdminPin); openpgp.setPinAttempts(6, 3, 3); @@ -171,7 +175,7 @@ public static void testSetPinAttempts(OpenPgpSession openpgp, OpenPgpTestState s } public static void testGenerateRsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_RSA_GENERATION)); + assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_RSA_GENERATION)); openpgp.verifyAdminPin(state.defaultAdminPin); @@ -200,7 +204,7 @@ public static void testGenerateRsaKeys(OpenPgpSession openpgp, OpenPgpTestState } public static void testGenerateEcKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); @@ -239,7 +243,7 @@ public static void testGenerateEcKeys(OpenPgpSession openpgp, OpenPgpTestState s } public static void testGenerateEd25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); @@ -257,7 +261,7 @@ public static void testGenerateEd25519(OpenPgpSession openpgp, OpenPgpTestState } public static void testGenerateX25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", state.isFipsApproved); @@ -316,7 +320,7 @@ public static void testImportRsaKeys(OpenPgpSession openpgp, OpenPgpTestState st } public static void testImportEcDsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); @@ -364,7 +368,7 @@ public static void testImportEcDsaKeys(OpenPgpSession openpgp, OpenPgpTestState } public static void testImportEd25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); @@ -389,7 +393,7 @@ public static void testImportEd25519(OpenPgpSession openpgp, OpenPgpTestState st } public static void testImportX25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", state.isFipsApproved); Security.removeProvider("BC"); @@ -415,7 +419,7 @@ public static void testImportX25519(OpenPgpSession openpgp, OpenPgpTestState sta } public static void testAttestation(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("Attestation support", openpgp.supports(OpenPgpSession.FEATURE_ATTESTATION)); + assumeTrue("Attestation support", openpgp.supports(OpenPgpSession.FEATURE_ATTESTATION)); openpgp.verifyAdminPin(state.defaultAdminPin); @@ -439,7 +443,7 @@ public static void testSigPinPolicy(OpenPgpSession openpgp, OpenPgpTestState sta try { openpgp.sign(message); - Assert.fail(); + fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -450,7 +454,7 @@ public static void testSigPinPolicy(OpenPgpSession openpgp, OpenPgpTestState sta Assert.assertEquals(1, openpgp.getSignatureCounter()); try { openpgp.sign(message); - Assert.fail(); + fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -465,12 +469,12 @@ public static void testSigPinPolicy(OpenPgpSession openpgp, OpenPgpTestState sta } public static void testKdf(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("KDF Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.KDF)); + assumeTrue("KDF Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.KDF)); // Test setting KDF without admin PIN verification try { openpgp.setKdf(new Kdf.None()); - Assert.fail(); + fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -499,7 +503,7 @@ public static void testKdf(OpenPgpSession openpgp, OpenPgpTestState state) throw } public static void testUnverifyPin(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("Unverify PIN Support", openpgp.supports(OpenPgpSession.FEATURE_UNVERIFY_PIN)); + assumeTrue("Unverify PIN Support", openpgp.supports(OpenPgpSession.FEATURE_UNVERIFY_PIN)); KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); @@ -513,7 +517,7 @@ public static void testUnverifyPin(OpenPgpSession openpgp, OpenPgpTestState stat // Test import key after unverify try { openpgp.putKey(KeyRef.AUT, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - Assert.fail(); + fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -526,7 +530,7 @@ public static void testUnverifyPin(OpenPgpSession openpgp, OpenPgpTestState stat // Test sign after unverify try { openpgp.sign(message); - Assert.fail(); + fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -547,9 +551,9 @@ public static void testDeleteKey(OpenPgpSession openpgp, OpenPgpTestState state) openpgp.deleteKey(KeyRef.SIG); try { openpgp.sign(message); - Assert.fail(); + fail(); } catch (ApduException e) { - Assert.assertEquals(SW.CONDITIONS_NOT_SATISFIED, e.getSw()); + Assert.assertEquals(CONDITIONS_NOT_SATISFIED, e.getSw()); } } @@ -586,7 +590,7 @@ public static void testCertificateManagement(OpenPgpSession openpgp, OpenPgpTest } public static void testGetChallenge(OpenPgpSession openpgp, OpenPgpTestState ignored) throws Exception { - Assume.assumeTrue("Get Challenge Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.GET_CHALLENGE)); + assumeTrue("Get Challenge Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.GET_CHALLENGE)); byte[] challenge = openpgp.getChallenge(1); Assert.assertEquals(1, challenge.length); @@ -603,11 +607,11 @@ public static void testGetChallenge(OpenPgpSession openpgp, OpenPgpTestState ign } public static void testSetUif(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("UIF Support", openpgp.supports(OpenPgpSession.FEATURE_UIF)); + assumeTrue("UIF Support", openpgp.supports(OpenPgpSession.FEATURE_UIF)); try { openpgp.setUif(KeyRef.SIG, Uif.ON); - Assert.fail(); + fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -661,7 +665,7 @@ public static void testPinComplexity(OpenPgpSession openpgp, OpenPgpTestState st try { openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, complexPin); } catch (Exception e) { - Assert.fail("Unexpected exception"); + fail("Unexpected exception"); } // change the user pin to value stated in the state as it is correct for PIN complexity 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 f5822844..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 @@ -21,15 +21,15 @@ import static org.junit.Assume.assumeTrue; import com.yubico.yubikit.core.YubiKeyDevice; -import com.yubico.yubikit.testing.ScpParameters; +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; -import java.io.IOException; - -import javax.annotation.Nullable; - public class OpenPgpTestState extends TestState { private static final char[] COMPLEX_USER_PIN = "112345678".toCharArray(); From 85362e93d090e94bd00b71826c5335cfc3ec350a Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 23 Jul 2024 17:17:24 +0200 Subject: [PATCH 31/46] fix enterprise attestation tests --- .../yubico/yubikit/testing/DeviceTests.java | 2 +- ...nterpriseAttestationInstrumentedTests.java | 2 +- .../testing/fido/Ctap2BioEnrollmentTests.java | 2 - .../fido/EnterpriseAttestationTests.java | 76 ++++++++++++------- 4 files changed, 50 insertions(+), 32 deletions(-) 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 421c435a..ba626add 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 @@ -26,7 +26,7 @@ import org.junit.runners.Suite; /** - * All integration tests for PIV, OpenPGP and OATH. + * All integration tests for PIV, OpenPGP, OATH, FIDO2 and MPE. *

    * The YubiKey applications will be reset several times. *

    diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java index 6977789a..a302fc53 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationInstrumentedTests.java @@ -45,7 +45,7 @@ public void testUnsupportedPlatformManagedEA() throws Throwable { @Test public void testCreateOptionsAttestationPreference() throws Throwable { - withCtap2Session(EnterpriseAttestationTests::testCreateOptionsAttestationPreference); + withDevice(EnterpriseAttestationTests::testCreateOptionsAttestationPreference); } @Test diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java index 3fb6acc2..935597f0 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java @@ -101,8 +101,6 @@ private static byte[] enrollFingerprint(FingerprintBioEnrollment bioEnrollment) private static FingerprintBioEnrollment fpBioEnrollment(Ctap2Session session, FidoTestState state) throws Throwable { - // ensureDefaultPinSet(session); - final ClientPin pin = new ClientPin(session, state.getPinUvAuthProtocol()); final byte[] pinToken = pin.getPinToken( TestData.PIN, diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java index a5e0d872..f1708f4d 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java @@ -16,6 +16,7 @@ package com.yubico.yubikit.testing.fido; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -80,7 +81,7 @@ public static void testSupportedPlatformManagedEA(Ctap2Session session, FidoTest public static void testUnsupportedPlatformManagedEA(Ctap2Session session, FidoTestState state) throws Throwable { assumeTrue("Enterprise attestation not supported", session.getCachedInfo().getOptions().containsKey("ep")); - // Ctap2ClientPinTests.ensureDefaultPinSet(session); + enableEp(session, state); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); @@ -95,7 +96,7 @@ public static void testUnsupportedPlatformManagedEA(Ctap2Session session, FidoTe public static void testVendorFacilitatedEA(Ctap2Session session, FidoTestState state) throws Throwable { assumeTrue("Enterprise attestation not supported", session.getCachedInfo().getOptions().containsKey("ep")); - // Ctap2ClientPinTests.ensureDefaultPinSet(session); + enableEp(session, state); BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); webauthn.getUserAgentConfiguration().setEpSupportedRpIds(Collections.singletonList(TestData.RP_ID)); @@ -104,52 +105,71 @@ public static void testVendorFacilitatedEA(Ctap2Session session, FidoTestState s final Map attestationObject = getAttestationObject(credential.getResponse()); assertNotNull(attestationObject); - assertTrue((Boolean) attestationObject.get("epAtt")); + assertEquals(Boolean.TRUE, attestationObject.get("epAtt")); } // test with different PublicKeyCredentialCreationOptions AttestationConveyancePreference // values - public static void testCreateOptionsAttestationPreference(Ctap2Session session, FidoTestState state) throws Throwable { - assumeTrue("Enterprise attestation not supported", - session.getCachedInfo().getOptions().containsKey("ep")); - // Ctap2ClientPinTests.ensureDefaultPinSet(session); - enableEp(session, state); + public static void testCreateOptionsAttestationPreference(FidoTestState state) throws Throwable { - // setup - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - webauthn.getUserAgentConfiguration().setEpSupportedRpIds(Collections.singletonList( - TestData.RP_ID - )); + state.withCtap2(session -> { + assumeTrue("Enterprise attestation not supported", + session.getCachedInfo().getOptions().containsKey("ep")); + enableEp(session, state); + }); // attestation = null - PublicKeyCredential credential = makeCredential(webauthn, null, 2); + state.withCtap2(session -> { + final BasicWebAuthnClient webauthn = setupClient(session); + PublicKeyCredential credential = makeCredential(webauthn, null, 2); - Map attestationObject = getAttestationObject(credential.getResponse()); - assertNull(attestationObject.get("epAtt")); + Map attestationObject = getAttestationObject(credential.getResponse()); + assertNull(attestationObject.get("epAtt")); + }); // attestation = DIRECT - credential = makeCredential(webauthn, AttestationConveyancePreference.DIRECT, 2); + state.withCtap2(session -> { + final BasicWebAuthnClient webauthn = setupClient(session); + PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.DIRECT, 2); + Map attestationObject = getAttestationObject(credential.getResponse()); + assertNull(attestationObject.get("epAtt")); + }); - attestationObject = getAttestationObject(credential.getResponse()); - assertNull(attestationObject.get("epAtt")); // attestation = INDIRECT - credential = makeCredential(webauthn, AttestationConveyancePreference.DIRECT, 2); + state.withCtap2(session -> { + final BasicWebAuthnClient webauthn = setupClient(session); + PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.DIRECT, 2); - attestationObject = getAttestationObject(credential.getResponse()); - assertNull(attestationObject.get("epAtt")); + Map attestationObject = getAttestationObject(credential.getResponse()); + assertNull(attestationObject.get("epAtt")); + }); // attestation = ENTERPRISE but null enterpriseAttestation - credential = makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, null); + state.withCtap2(session -> { + final BasicWebAuthnClient webauthn = setupClient(session); + PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, null); - attestationObject = getAttestationObject(credential.getResponse()); - assertNull(attestationObject.get("epAtt")); + Map attestationObject = getAttestationObject(credential.getResponse()); + assertNull(attestationObject.get("epAtt")); + }); // attestation = ENTERPRISE - credential = makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 2); + state.withCtap2(session -> { + final BasicWebAuthnClient webauthn = setupClient(session); + PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 2); - attestationObject = getAttestationObject(credential.getResponse()); - assertTrue((Boolean) attestationObject.get("epAtt")); + Map attestationObject = getAttestationObject(credential.getResponse()); + assertEquals(Boolean.TRUE, attestationObject.get("epAtt")); + }); + } + + private static BasicWebAuthnClient setupClient(Ctap2Session session) throws IOException, CommandException { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + webauthn.getUserAgentConfiguration().setEpSupportedRpIds(Collections.singletonList( + TestData.RP_ID + )); + return webauthn; } /** From 5cae0f7787b100dd82abead294d68704ed049034 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 23 Jul 2024 17:35:47 +0200 Subject: [PATCH 32/46] self review --- .../yubikit/testing/oath/OathDeviceTests.java | 1 - .../yubikit/testing/oath/OathTestState.java | 8 --- .../testing/openpgp/OpenPgpDeviceTests.java | 62 +++++++++---------- .../yubikit/testing/piv/PivTestState.java | 10 ++- 4 files changed, 34 insertions(+), 47 deletions(-) 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 49a8c562..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 @@ -22,7 +22,6 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.core.smartcard.SW; import com.yubico.yubikit.oath.Credential; diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java index a4bce027..8e2972b9 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java @@ -19,21 +19,13 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; -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.CommandException; -import com.yubico.yubikit.core.fido.FidoConnection; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.management.Capability; import com.yubico.yubikit.oath.OathSession; -import com.yubico.yubikit.testing.ScpParameters; import com.yubico.yubikit.testing.TestState; -import java.io.IOException; - -import javax.annotation.Nullable; - public class OathTestState extends TestState { public boolean isFipsApproved; public char[] password; 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 b8a25f3e..28257a1f 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 @@ -17,8 +17,6 @@ package com.yubico.yubikit.testing.openpgp; 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; @@ -89,7 +87,7 @@ public static void testGenerateRequiresAdmin(OpenPgpSession openpgp, OpenPgpTest try { openpgp.generateEcKey(KeyRef.DEC, OpenPgpCurve.BrainpoolP256R1); - fail(); + Assert.fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -97,7 +95,7 @@ public static void testGenerateRequiresAdmin(OpenPgpSession openpgp, OpenPgpTest if (!state.isFipsApproved) { try { openpgp.generateRsaKey(KeyRef.DEC, 2048); - fail(); + Assert.fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -120,7 +118,7 @@ public static void testResetPin(OpenPgpSession openpgp, OpenPgpTestState state) for (int i = remaining; i > 0; i--) { try { openpgp.verifyUserPin(CHANGED_PIN, false); - fail(); + Assert.fail(); } catch (InvalidPinException e) { Assert.assertEquals(e.getAttemptsRemaining(), i - 1); } @@ -129,7 +127,7 @@ public static void testResetPin(OpenPgpSession openpgp, OpenPgpTestState state) try { openpgp.resetPin(state.defaultUserPin, null); - fail(); + Assert.fail(); } catch (ApduException e) { Assert.assertEquals(e.getSw(), SW.SECURITY_CONDITION_NOT_SATISFIED); } @@ -142,7 +140,7 @@ public static void testResetPin(OpenPgpSession openpgp, OpenPgpTestState state) for (int i = remaining; i > 0; i--) { try { openpgp.verifyUserPin(CHANGED_PIN, false); - fail(); + Assert.fail(); } catch (InvalidPinException e) { Assert.assertEquals(e.getAttemptsRemaining(), i - 1); } @@ -157,7 +155,7 @@ public static void testResetPin(OpenPgpSession openpgp, OpenPgpTestState state) } public static void testSetPinAttempts(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_PIN_ATTEMPTS)); + Assume.assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_PIN_ATTEMPTS)); openpgp.verifyAdminPin(state.defaultAdminPin); openpgp.setPinAttempts(6, 3, 3); @@ -175,7 +173,7 @@ public static void testSetPinAttempts(OpenPgpSession openpgp, OpenPgpTestState s } public static void testGenerateRsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_RSA_GENERATION)); + Assume.assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_RSA_GENERATION)); openpgp.verifyAdminPin(state.defaultAdminPin); @@ -204,7 +202,7 @@ public static void testGenerateRsaKeys(OpenPgpSession openpgp, OpenPgpTestState } public static void testGenerateEcKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); @@ -243,7 +241,7 @@ public static void testGenerateEcKeys(OpenPgpSession openpgp, OpenPgpTestState s } public static void testGenerateEd25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); @@ -261,7 +259,7 @@ public static void testGenerateEd25519(OpenPgpSession openpgp, OpenPgpTestState } public static void testGenerateX25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", state.isFipsApproved); @@ -320,7 +318,7 @@ public static void testImportRsaKeys(OpenPgpSession openpgp, OpenPgpTestState st } public static void testImportEcDsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); @@ -368,7 +366,7 @@ public static void testImportEcDsaKeys(OpenPgpSession openpgp, OpenPgpTestState } public static void testImportEd25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); @@ -393,7 +391,7 @@ public static void testImportEd25519(OpenPgpSession openpgp, OpenPgpTestState st } public static void testImportX25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", state.isFipsApproved); Security.removeProvider("BC"); @@ -419,7 +417,7 @@ public static void testImportX25519(OpenPgpSession openpgp, OpenPgpTestState sta } public static void testAttestation(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("Attestation support", openpgp.supports(OpenPgpSession.FEATURE_ATTESTATION)); + Assume.assumeTrue("Attestation support", openpgp.supports(OpenPgpSession.FEATURE_ATTESTATION)); openpgp.verifyAdminPin(state.defaultAdminPin); @@ -443,7 +441,7 @@ public static void testSigPinPolicy(OpenPgpSession openpgp, OpenPgpTestState sta try { openpgp.sign(message); - fail(); + Assert.fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -454,7 +452,7 @@ public static void testSigPinPolicy(OpenPgpSession openpgp, OpenPgpTestState sta Assert.assertEquals(1, openpgp.getSignatureCounter()); try { openpgp.sign(message); - fail(); + Assert.fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -469,12 +467,12 @@ public static void testSigPinPolicy(OpenPgpSession openpgp, OpenPgpTestState sta } public static void testKdf(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("KDF Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.KDF)); + Assume.assumeTrue("KDF Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.KDF)); // Test setting KDF without admin PIN verification try { openpgp.setKdf(new Kdf.None()); - fail(); + Assert.fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -503,7 +501,7 @@ public static void testKdf(OpenPgpSession openpgp, OpenPgpTestState state) throw } public static void testUnverifyPin(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("Unverify PIN Support", openpgp.supports(OpenPgpSession.FEATURE_UNVERIFY_PIN)); + Assume.assumeTrue("Unverify PIN Support", openpgp.supports(OpenPgpSession.FEATURE_UNVERIFY_PIN)); KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); @@ -517,7 +515,7 @@ public static void testUnverifyPin(OpenPgpSession openpgp, OpenPgpTestState stat // Test import key after unverify try { openpgp.putKey(KeyRef.AUT, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - fail(); + Assert.fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -530,7 +528,7 @@ public static void testUnverifyPin(OpenPgpSession openpgp, OpenPgpTestState stat // Test sign after unverify try { openpgp.sign(message); - fail(); + Assert.fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -551,7 +549,7 @@ public static void testDeleteKey(OpenPgpSession openpgp, OpenPgpTestState state) openpgp.deleteKey(KeyRef.SIG); try { openpgp.sign(message); - fail(); + Assert.fail(); } catch (ApduException e) { Assert.assertEquals(CONDITIONS_NOT_SATISFIED, e.getSw()); } @@ -590,7 +588,7 @@ public static void testCertificateManagement(OpenPgpSession openpgp, OpenPgpTest } public static void testGetChallenge(OpenPgpSession openpgp, OpenPgpTestState ignored) throws Exception { - assumeTrue("Get Challenge Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.GET_CHALLENGE)); + Assume.assumeTrue("Get Challenge Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.GET_CHALLENGE)); byte[] challenge = openpgp.getChallenge(1); Assert.assertEquals(1, challenge.length); @@ -607,11 +605,11 @@ public static void testGetChallenge(OpenPgpSession openpgp, OpenPgpTestState ign } public static void testSetUif(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - assumeTrue("UIF Support", openpgp.supports(OpenPgpSession.FEATURE_UIF)); + Assume.assumeTrue("UIF Support", openpgp.supports(OpenPgpSession.FEATURE_UIF)); try { openpgp.setUif(KeyRef.SIG, Uif.ON); - fail(); + Assert.fail(); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } @@ -640,8 +638,8 @@ public static void testSetUif(OpenPgpSession openpgp, OpenPgpTestState state) th 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()); + Assume.assumeTrue("Device does not support PIN complexity", deviceInfo != null); + Assume.assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); openpgp.reset(); @@ -654,10 +652,10 @@ public static void testPinComplexity(OpenPgpSession openpgp, OpenPgpTestState st openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, weakPin); } catch (ApduException apduException) { if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { - fail("Unexpected exception"); + Assert.fail("Unexpected exception"); } } catch (Exception e) { - fail("Unexpected exception"); + Assert.fail("Unexpected exception"); } // set complex pin @@ -665,7 +663,7 @@ public static void testPinComplexity(OpenPgpSession openpgp, OpenPgpTestState st try { openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, complexPin); } catch (Exception e) { - fail("Unexpected exception"); + Assert.fail("Unexpected exception"); } // change the user pin to value stated in the state as it is correct for PIN complexity 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 3f0909e1..db0c2719 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 @@ -29,13 +29,8 @@ import com.yubico.yubikit.piv.KeyType; import com.yubico.yubikit.piv.ManagementKeyType; import com.yubico.yubikit.piv.PivSession; -import com.yubico.yubikit.testing.ScpParameters; import com.yubico.yubikit.testing.TestState; -import java.io.IOException; - -import javax.annotation.Nullable; - public class PivTestState extends TestState { static final char[] DEFAULT_PIN = "123456".toCharArray(); @@ -87,6 +82,9 @@ protected PivTestState(Builder builder) throws Throwable { assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", isUsbTransport()); } + // skip MPE devices + assumeFalse("Ignoring MPE device", isMpe(deviceInfo)); + if (scpParameters.getKid() != null) { // skip the test if the connected key does not provide matching SCP keys assumeTrue( @@ -95,7 +93,7 @@ protected PivTestState(Builder builder) throws Throwable { ); } - try (SmartCardConnection connection = currentDevice.openConnection(SmartCardConnection.class)) { + try (SmartCardConnection connection = openSmartCardConnection()) { PivSession pivSession = getPivSession(connection, scpParameters); assumeTrue("PIV not available", pivSession != null); From 0a954a174a6d3aaa0c9bd919023a2a0d612e7c48 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 23 Jul 2024 17:38:25 +0200 Subject: [PATCH 33/46] self review --- .../java/com/yubico/yubikit/testing/piv/PivTestState.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 db0c2719..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 @@ -75,6 +75,10 @@ protected PivTestState(Builder builder) throws Throwable { assumeTrue("No SmartCard support", currentDevice.supportsConnection(SmartCardConnection.class)); DeviceInfo deviceInfo = getDeviceInfo(); + + // skip MPE devices + assumeFalse("Ignoring MPE device", isMpe(deviceInfo)); + boolean isPivFipsCapable = isFipsCapable(deviceInfo, Capability.PIV); boolean hasPinComplexity = deviceInfo != null && deviceInfo.getPinComplexity(); @@ -82,9 +86,6 @@ protected PivTestState(Builder builder) throws Throwable { assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", isUsbTransport()); } - // skip MPE devices - assumeFalse("Ignoring MPE device", isMpe(deviceInfo)); - if (scpParameters.getKid() != null) { // skip the test if the connected key does not provide matching SCP keys assumeTrue( @@ -100,7 +101,6 @@ protected PivTestState(Builder builder) throws Throwable { try { pivSession.reset(); } catch (Exception ignored) { - } if (hasPinComplexity) { From 48fd760e6cf9e48589716fcbdc708c4efc1116dc Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 23 Jul 2024 17:43:23 +0200 Subject: [PATCH 34/46] self review --- .../java/com/yubico/yubikit/testing/DeviceTests.java | 2 -- .../com/yubico/yubikit/testing/openpgp/OpenPgpDeviceTests.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) 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 ba626add..b09d7383 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 @@ -30,8 +30,6 @@ *

    * The YubiKey applications will be reset several times. *

    - * FIDO integration tests cannot be ran in this suite and can be found in - * {@link com.yubico.yubikit.testing.fido.FidoTests} */ @RunWith(Suite.class) @Suite.SuiteClasses({ 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 28257a1f..5193c67b 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Yubico. + * Copyright (C) 2023-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From e9a6058316c2836fc0dac948893e3e353b2af72f Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 24 Jul 2024 15:16:44 +0200 Subject: [PATCH 35/46] add first test for SCP03 --- .../smartcard/scp/SecurityDomainSession.java | 13 +++- .../yubico/yubikit/testing/sd/Scp03Tests.java | 28 ++++++++ .../SecurityDomainInstrumentedTests.java | 46 +++++++++++++ .../com/yubico/yubikit/testing/TestState.java | 33 +++++++++- .../yubikit/testing/oath/OathTestState.java | 7 +- .../yubikit/testing/sd/Scp03DeviceTests.java | 64 +++++++++++++++++++ .../testing/sd/SecurityDomainTestState.java | 56 ++++++++++++++++ 7 files changed, 238 insertions(+), 9 deletions(-) create mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp03Tests.java create mode 100644 testing-android/src/main/java/com/yubico/yubikit/testing/framework/SecurityDomainInstrumentedTests.java create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java index 3f29bfc5..b529a08d 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java @@ -89,10 +89,21 @@ public class SecurityDomainSession extends ApplicationSession callback) throws Throwable { + final SecurityDomainTestState state = new SecurityDomainTestState.Builder(device) + .reconnectDeviceCallback(this::reconnectDevice) + .build(); + + state.withDeviceCallback(callback); + } + + protected void withSecurityDomainSession(TestState.StatefulSessionCallback callback) throws Throwable { + final SecurityDomainTestState state = new SecurityDomainTestState.Builder(device).scpKid(getScpKid()) + .build(); + state.withSecurityDomain(callback); + } + + protected void withScp11Session(TestState.StatefulSessionCallback callback) throws Throwable { + final SecurityDomainTestState state = new SecurityDomainTestState.Builder(device).scpKid(ScpKid.SCP11b) + .build(); + state.withSecurityDomain(callback); + } + +} \ No newline at end of file diff --git a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java index aaaca475..557ca9a9 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -28,6 +28,7 @@ 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.core.smartcard.scp.SecurityDomainSession; import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.management.Capability; import com.yubico.yubikit.management.DeviceInfo; @@ -226,7 +227,7 @@ public void withOath(StatefulSessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getSecurityDomainSession(connection)); + } + reconnect(); + } + + public void withSecurityDomain(StatefulSessionCallback callback) + throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + //noinspection unchecked + callback.invoke(getSecurityDomainSession(connection), (T) this); + } + reconnect(); + } + + @Nullable + protected SecurityDomainSession getSecurityDomainSession(SmartCardConnection connection) + throws IOException { + try { + return new SecurityDomainSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + // no OATH support + } + return null; + } + // OpenPGP helpers public void withOpenPgp(StatefulSessionCallback callback) throws Throwable { @@ -286,7 +315,7 @@ protected YubiKeyConnection openConnection() throws IOException { public DeviceInfo getDeviceInfo() { DeviceInfo deviceInfo = null; try (YubiKeyConnection connection = openConnection()) { - ManagementSession managementSession = getManagementSession(connection, null); + ManagementSession managementSession = getManagementSession(connection, scpParameters); deviceInfo = managementSession.getDeviceInfo(); } catch (IOException | CommandException ignored) { diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java index 8e2972b9..2f467f79 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java @@ -61,12 +61,7 @@ protected OathTestState(OathTestState.Builder builder) throws Throwable { } try (SmartCardConnection connection = openSmartCardConnection()) { - OathSession oath = null; - try { - oath = new OathSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - - } + OathSession oath = getOathSession(connection, scpParameters); assumeTrue("OATH not available", oath != null); oath.reset(); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java new file mode 100644 index 00000000..63044b19 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 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.sd; + +import com.yubico.yubikit.core.application.BadResponseException; +import com.yubico.yubikit.core.smartcard.ApduException; +import com.yubico.yubikit.core.smartcard.scp.KeyRef; +import com.yubico.yubikit.core.smartcard.scp.Scp03KeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpKid; +import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; +import com.yubico.yubikit.core.smartcard.scp.StaticKeys; + +import java.io.IOException; +import java.util.Map; + +public class Scp03DeviceTests { + + public static void testImportScp03(SecurityDomainTestState state) throws Throwable { + + final byte[] sk = new byte[]{ + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47 + }; + final StaticKeys staticKeys = new StaticKeys(sk, sk, sk); + + // reset security domain to get default keys + state.withSecurityDomain(SecurityDomainSession::reset); + state.withSecurityDomain(sd -> { + // this session is initialized + KeyRef keyRef = getKeyRef(sd, ScpKid.SCP03); + ScpKeyParams keyParams = new Scp03KeyParams(keyRef, StaticKeys.getDefaultKeys()); + sd.authenticate(keyParams); + sd.putKey(keyRef, staticKeys, 0); + }); + } + + private static KeyRef getKeyRef(SecurityDomainSession scp, byte kid) + throws ApduException, IOException, BadResponseException { + Map> keyInformation = scp.getKeyInformation(); + KeyRef keyRef = null; + for (KeyRef info : keyInformation.keySet()) { + if (info.getKid() == kid) { + keyRef = info; + break; + } + } + return keyRef; + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java new file mode 100644 index 00000000..29133566 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java @@ -0,0 +1,56 @@ +/* + * 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.sd; + +import static org.junit.Assert.assertNull; +import static org.junit.Assume.assumeTrue; + +import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.application.ApplicationNotAvailableException; +import com.yubico.yubikit.core.smartcard.SmartCardConnection; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; +import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; +import com.yubico.yubikit.testing.TestState; + +import java.io.IOException; + +import javax.annotation.Nullable; + +public class SecurityDomainTestState extends TestState { + + public static class Builder extends TestState.Builder { + + public Builder(YubiKeyDevice device) { + super(device); + } + + public SecurityDomainTestState build() throws Throwable { + return new SecurityDomainTestState(this); + } + } + + protected SecurityDomainTestState(Builder builder) throws Throwable { + super(builder); + + try (SmartCardConnection connection = openSmartCardConnection()) { + SecurityDomainSession sd = getSecurityDomainSession(connection); + assumeTrue("Security domain not supported", sd != null); + assertNull("These tests expect kid to be null", scpParameters.getKid()); + } + + } +} From 5bc026684de33d2bbf004234b22fb84fc0d038e8 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 31 Jul 2024 09:48:00 +0200 Subject: [PATCH 36/46] SCP03 tests --- .../yubico/yubikit/testing/DeviceTests.java | 4 +- .../yubico/yubikit/testing/sd/Scp03Tests.java | 17 +- .../yubikit/testing/sd/Scp03DeviceTests.java | 186 +++++++++++++++--- 3 files changed, 181 insertions(+), 26 deletions(-) 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 b09d7383..c691e11d 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 @@ -21,18 +21,20 @@ import com.yubico.yubikit.testing.oath.OathTests; import com.yubico.yubikit.testing.openpgp.OpenPgpTests; import com.yubico.yubikit.testing.piv.PivTests; +import com.yubico.yubikit.testing.sd.SecurityDomainTests; import org.junit.runner.RunWith; import org.junit.runners.Suite; /** - * All integration tests for PIV, OpenPGP, OATH, FIDO2 and MPE. + * All integration tests for Security domain, PIV, OpenPGP, OATH, FIDO2 and MPE. *

    * The YubiKey applications will be reset several times. *

    */ @RunWith(Suite.class) @Suite.SuiteClasses({ + SecurityDomainTests.class, PivTests.class, OpenPgpTests.class, OathTests.class, diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp03Tests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp03Tests.java index 9d55cf47..467b51b7 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp03Tests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp03Tests.java @@ -16,13 +16,26 @@ package com.yubico.yubikit.testing.sd; +import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.SecurityDomainInstrumentedTests; import org.junit.Test; +import org.junit.experimental.categories.Category; public class Scp03Tests extends SecurityDomainInstrumentedTests { @Test - public void testImportScp03() throws Throwable { - withDevice(Scp03DeviceTests::testImportScp03); + public void testImportKey() throws Throwable { + withDevice(Scp03DeviceTests::testImportKey); + } + + @Test + public void testDeleteKey() throws Throwable { + withDevice(Scp03DeviceTests::testDeleteKey); + } + + @Test + @Category(SmokeTest.class) + public void testReplaceKey() throws Throwable { + withDevice(Scp03DeviceTests::testReplaceKey); } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java index 63044b19..22830380 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java @@ -16,49 +16,189 @@ package com.yubico.yubikit.testing.sd; -import com.yubico.yubikit.core.application.BadResponseException; -import com.yubico.yubikit.core.smartcard.ApduException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assume.assumeFalse; + import com.yubico.yubikit.core.smartcard.scp.KeyRef; import com.yubico.yubikit.core.smartcard.scp.Scp03KeyParams; -import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; -import com.yubico.yubikit.core.smartcard.scp.ScpKid; import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; import com.yubico.yubikit.core.smartcard.scp.StaticKeys; -import java.io.IOException; import java.util.Map; public class Scp03DeviceTests { - public static void testImportScp03(SecurityDomainTestState state) throws Throwable { + static final KeyRef DEFAULT_KEY = new KeyRef((byte) 0x01, (byte) 0xff); + + public static void testImportKey(SecurityDomainTestState state) throws Throwable { final byte[] sk = new byte[]{ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, - 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47 + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, }; final StaticKeys staticKeys = new StaticKeys(sk, sk, sk); + final KeyRef newKey = new KeyRef((byte) 0x01, (byte) 0x01); + + assumeFalse("SCP03 not supported over NFC on FIPS capable devices", + state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); - // reset security domain to get default keys state.withSecurityDomain(SecurityDomainSession::reset); state.withSecurityDomain(sd -> { - // this session is initialized - KeyRef keyRef = getKeyRef(sd, ScpKid.SCP03); - ScpKeyParams keyParams = new Scp03KeyParams(keyRef, StaticKeys.getDefaultKeys()); - sd.authenticate(keyParams); - sd.putKey(keyRef, staticKeys, 0); + sd.authenticate(new Scp03KeyParams(DEFAULT_KEY, StaticKeys.getDefaultKeys())); + sd.putKey(newKey, staticKeys, 0); + }); + + state.withSecurityDomain(sd -> { + Map> keyInformation = sd.getKeyInformation(); + // verify there are three SCP03 keys with kvn 0x01 + assertNotNull(keyInformation.get(scp03EncOf(newKey))); + assertNotNull(keyInformation.get(scp03MacOf(newKey))); + assertNotNull(keyInformation.get(scp03DekOf(newKey))); + + // the default keys are gone + assertNull(keyInformation.get(scp03EncOf(DEFAULT_KEY))); + assertNull(keyInformation.get(scp03MacOf(DEFAULT_KEY))); + assertNull(keyInformation.get(scp03DekOf(DEFAULT_KEY))); }); } - private static KeyRef getKeyRef(SecurityDomainSession scp, byte kid) - throws ApduException, IOException, BadResponseException { - Map> keyInformation = scp.getKeyInformation(); - KeyRef keyRef = null; - for (KeyRef info : keyInformation.keySet()) { - if (info.getKid() == kid) { - keyRef = info; - break; + public static void testDeleteKey(SecurityDomainTestState state) throws Throwable { + + final byte[] bytes1 = new byte[]{ + 0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x67, + 0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x67, + }; + final StaticKeys staticKeys1 = new StaticKeys(bytes1, bytes1, bytes1); + + final byte[] bytes2 = new byte[]{ + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + }; + final StaticKeys staticKeys2 = new StaticKeys(bytes2, bytes2, bytes2); + + final KeyRef keyRef1 = new KeyRef((byte) 0x01, (byte) 0x10); + final KeyRef keyRef2 = new KeyRef((byte) 0x01, (byte) 0x55); + + assumeFalse("SCP03 not supported over NFC on FIPS capable devices", + state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + + state.withSecurityDomain(SecurityDomainSession::reset); + state.withSecurityDomain(sd -> { + sd.authenticate(new Scp03KeyParams(DEFAULT_KEY, StaticKeys.getDefaultKeys())); + sd.putKey(keyRef1, staticKeys1, 0); + }); + + // authenticate with the new key and put the second + state.withSecurityDomain(sd -> { + sd.authenticate(new Scp03KeyParams(keyRef1, staticKeys1)); + sd.putKey(keyRef2, staticKeys2, 0); + + // there are 2 SCP03 keys + Map> keyInformation = sd.getKeyInformation(); + // verify there are three SCP03 keys with kvn 0x01 + assertNotNull(keyInformation.get(scp03EncOf(keyRef1))); + assertNotNull(keyInformation.get(scp03MacOf(keyRef1))); + assertNotNull(keyInformation.get(scp03DekOf(keyRef1))); + + assertNotNull(keyInformation.get(scp03EncOf(keyRef2))); + assertNotNull(keyInformation.get(scp03MacOf(keyRef2))); + assertNotNull(keyInformation.get(scp03DekOf(keyRef2))); + }); + + // delete first key + state.withSecurityDomain(sd -> { + // authenticate with the second key + sd.authenticate(new Scp03KeyParams(keyRef2, staticKeys2)); + sd.deleteKey(keyRef1, false); + + // verify there are three the first key is deleted + final Map> keyInformation = sd.getKeyInformation(); + assertNull(keyInformation.get(scp03EncOf(keyRef1))); + assertNull(keyInformation.get(scp03MacOf(keyRef1))); + assertNull(keyInformation.get(scp03DekOf(keyRef1))); + + assertNotNull(keyInformation.get(scp03EncOf(keyRef2))); + assertNotNull(keyInformation.get(scp03MacOf(keyRef2))); + assertNotNull(keyInformation.get(scp03DekOf(keyRef2))); + }); + + // delete the second key + state.withSecurityDomain(sd -> { + // authenticate with the second key + sd.authenticate(new Scp03KeyParams(keyRef2, staticKeys2)); + sd.deleteKey(keyRef2, true); // the last key + + // verify that the second key is gone + final Map> keyInformation = sd.getKeyInformation(); + assertNull(keyInformation.get(scp03EncOf(keyRef2))); + assertNull(keyInformation.get(scp03MacOf(keyRef2))); + assertNull(keyInformation.get(scp03DekOf(keyRef2))); + + // there now should be a default SCP03 key + // it might be missing at all if the YubiKey supports other SCP versions + for (KeyRef keyRef : keyInformation.keySet()) { + final byte kid = keyRef.getKid(); + if (kid == (byte) 0x01 || kid == (byte) 0x02 || kid == (byte) 0x03) { + assertEquals(DEFAULT_KEY.getKvn(), keyRef.getKvn()); + } } - } - return keyRef; + }); + } + + public static void testReplaceKey(SecurityDomainTestState state) throws Throwable { + + final byte[] bytes1 = new byte[]{ + 0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x67, + 0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x67, + }; + final StaticKeys staticKeys1 = new StaticKeys(bytes1, bytes1, bytes1); + + final byte[] bytes2 = new byte[]{ + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + }; + final StaticKeys staticKeys2 = new StaticKeys(bytes2, bytes2, bytes2); + + final KeyRef keyRef1 = new KeyRef((byte) 0x01, (byte) 0x10); + final KeyRef keyRef2 = new KeyRef((byte) 0x01, (byte) 0x55); + + assumeFalse("SCP03 not supported over NFC on FIPS capable devices", + state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + + state.withSecurityDomain(SecurityDomainSession::reset); + state.withSecurityDomain(sd -> { + sd.authenticate(new Scp03KeyParams(DEFAULT_KEY, StaticKeys.getDefaultKeys())); + sd.putKey(keyRef1, staticKeys1, 0); + }); + + // authenticate with the new key and replace it with the second + state.withSecurityDomain(sd -> { + sd.authenticate(new Scp03KeyParams(keyRef1, staticKeys1)); + sd.putKey(keyRef2, staticKeys2, keyRef1.getKvn()); + + // there is only one SCP03 key + Map> keyInformation = sd.getKeyInformation(); + assertNull(keyInformation.get(scp03EncOf(keyRef1))); + assertNull(keyInformation.get(scp03MacOf(keyRef1))); + assertNull(keyInformation.get(scp03DekOf(keyRef1))); + + assertNotNull(keyInformation.get(scp03EncOf(keyRef2))); + assertNotNull(keyInformation.get(scp03MacOf(keyRef2))); + assertNotNull(keyInformation.get(scp03DekOf(keyRef2))); + }); + } + + private static KeyRef scp03EncOf(KeyRef key) { + return new KeyRef((byte) 0x01, key.getKvn()); + } + + private static KeyRef scp03MacOf(KeyRef key) { + return new KeyRef((byte) 0x02, key.getKvn()); + } + + private static KeyRef scp03DekOf(KeyRef key) { + return new KeyRef((byte) 0x03, key.getKvn()); } } From 07fa73ec07bbcdbfe269d89ed3cba5b5fc007180 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 6 Aug 2024 10:49:02 +0200 Subject: [PATCH 37/46] add SCP11a import key tests --- .../yubikit/core/smartcard/scp/ScpState.java | 3 + .../smartcard/scp/SecurityDomainSession.java | 8 +- .../yubikit/testing/sd/Scp11aTests.java | 29 ++ .../testing/sd/SecurityDomainTests.java | 28 ++ .../com/yubico/yubikit/testing/TestState.java | 37 ++ .../yubikit/testing/sd/Scp11aDeviceTests.java | 354 ++++++++++++++++++ .../yubikit/testing/sd/ScpCertificates.java | 102 +++++ .../testing/sd/SecurityDomainTestState.java | 14 +- 8 files changed, 567 insertions(+), 8 deletions(-) create mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11aTests.java create mode 100644 testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/SecurityDomainTests.java create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java index 0bdf242d..62f2f978 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java @@ -26,6 +26,7 @@ import com.yubico.yubikit.core.smartcard.SW; import com.yubico.yubikit.core.util.Pair; import com.yubico.yubikit.core.util.RandomUtils; +import com.yubico.yubikit.core.util.StringUtils; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; @@ -85,6 +86,7 @@ public ScpState(SessionKeys keys, byte[] macChain) { public byte[] encrypt(byte[] data) { // Pad the data + logger.trace("Plain data: {}", StringUtils.bytesToHex(data)); int padLen = 16 - (data.length % 16); byte[] padded = Arrays.copyOf(data, data.length + padLen); padded[data.length] = (byte) 0x80; @@ -123,6 +125,7 @@ public byte[] decrypt(byte[] encrypted) throws BadResponseException { decrypted = cipher.doFinal(encrypted); for (int i = decrypted.length - 1; i > 0; i--) { if (decrypted[i] == (byte) 0x80) { + logger.trace("Plaintext resp: {}", StringUtils.bytesToHex(decrypted)); return Arrays.copyOf(decrypted, i); } else if (decrypted[i] != 0x00) { break; diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java index b529a08d..b1337c05 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java @@ -77,10 +77,10 @@ public class SecurityDomainSession extends ApplicationSession callback) reconnect(); } + public R withSecurityDomain(SessionCallbackT callback) throws Throwable { + R result; + try (SmartCardConnection connection = openSmartCardConnection()) { + result = callback.invoke(getSecurityDomainSession(connection)); + } + reconnect(); + return result; + } + + public void withSecurityDomain(ScpKeyParams scpKeyParams, SessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getSecurityDomainSession(scpKeyParams, connection)); + } + reconnect(); + } + public void withSecurityDomain(StatefulSessionCallback callback) throws Throwable { try (SmartCardConnection connection = openSmartCardConnection()) { @@ -254,6 +271,15 @@ public void withSecurityDomain(StatefulSessionCallback R withSecurityDomain(ScpKeyParams scpKeyParams, SessionCallbackT callback) throws Throwable { + R result; + try (SmartCardConnection connection = openSmartCardConnection()) { + result = callback.invoke(getSecurityDomainSession(scpKeyParams, connection)); + } + reconnect(); + return result; + } + @Nullable protected SecurityDomainSession getSecurityDomainSession(SmartCardConnection connection) throws IOException { @@ -265,6 +291,17 @@ protected SecurityDomainSession getSecurityDomainSession(SmartCardConnection con return null; } + @Nullable + protected SecurityDomainSession getSecurityDomainSession(ScpKeyParams scpKeyParams, SmartCardConnection connection) + throws IOException { + try { + return new SecurityDomainSession(connection, scpKeyParams); + } catch (ApplicationNotAvailableException ignored) { + // no OATH support + } + return null; + } + // OpenPGP helpers public void withOpenPgp(StatefulSessionCallback callback) throws Throwable { diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java new file mode 100644 index 00000000..256c0b76 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2023 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.sd; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import com.yubico.yubikit.core.internal.codec.Base64; +import com.yubico.yubikit.core.keys.PublicKeyValues; +import com.yubico.yubikit.core.smartcard.scp.KeyRef; +import com.yubico.yubikit.core.smartcard.scp.Scp03KeyParams; +import com.yubico.yubikit.core.smartcard.scp.Scp11KeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpKid; +import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; +import com.yubico.yubikit.core.smartcard.scp.StaticKeys; +import com.yubico.yubikit.core.util.Tlv; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +public class Scp11aDeviceTests { + + static final KeyRef DEFAULT_SCP03_KEY = new KeyRef((byte) 0x01, (byte) 0xff); + static final KeyRef AUTH_SCP03_KEY = new KeyRef((byte) 0x01, (byte) 0x01); + static final KeyRef AUTH_SCP11A_KEY = new KeyRef(ScpKid.SCP11a, (byte) 2); + static final KeyRef CA_KLOC_KEY_REF = new KeyRef((byte) 0x10, (byte) 2); + + @SuppressWarnings("SpellCheckingInspection") + static final byte[] OCE_CERTS_V1 = ("-----BEGIN CERTIFICATE-----\n" + + "MIIB8DCCAZegAwIBAgIUf0lxsK1R+EydqZKLLV/vXhaykgowCgYIKoZIzj0EAwIw\n" + + "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + + "NDA1MjgwOTIyMDlaFw0yNDA4MjYwOTIyMDlaMC8xLTArBgNVBAMMJEV4YW1wbGUg\n" + + "T0NFIEludGVybWVkaWF0ZSBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49\n" + + "AwEHA0IABMXbjb+Y33+GP8qUznrdZSJX9b2qC0VUS1WDhuTlQUfg/RBNFXb2/qWt\n" + + "h/a+Ag406fV7wZW2e4PPH+Le7EwS1nyjgZUwgZIwHQYDVR0OBBYEFJzdQCINVBES\n" + + "R4yZBN2l5CXyzlWsMB8GA1UdIwQYMBaAFDGqVWafYGfoHzPc/QT+3nPlcZ89MBIG\n" + + "A1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgIEMCwGA1UdIAEB/wQiMCAw\n" + + "DgYMKoZIhvxrZAAKAgEoMA4GDCqGSIb8a2QACgIBADAKBggqhkjOPQQDAgNHADBE\n" + + "AiBE5SpNEKDW3OehDhvTKT9g1cuuIyPdaXGLZ3iX0x0VcwIgdnIirhlKocOKGXf9\n" + + "ijkE8e+9dTazSPLf24lSIf0IGC8=\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIIB2zCCAYGgAwIBAgIUSf59wIpCKOrNGNc5FMPTD9zDGVAwCgYIKoZIzj0EAwIw\n" + + "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + + "NDA1MjgwOTIyMDlaFw0yNDA2MjcwOTIyMDlaMCoxKDAmBgNVBAMMH0V4YW1wbGUg\n" + + "T0NFIFJvb3QgQ0EgQ2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n" + + "AASPrxfpSB/AvuvLKaCz1YTx68Xbtx8S9xAMfRGwzp5cXMdF8c7AWpUfeM3BQ26M\n" + + "h0WPvyBJKhCdeK8iVCaHyr5Jo4GEMIGBMB0GA1UdDgQWBBQxqlVmn2Bn6B8z3P0E\n" + + "/t5z5XGfPTASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBBjA8BgNV\n" + + "HSABAf8EMjAwMA4GDCqGSIb8a2QACgIBFDAOBgwqhkiG/GtkAAoCASgwDgYMKoZI\n" + + "hvxrZAAKAgEAMAoGCCqGSM49BAMCA0gAMEUCIHv8cgOzxq2n1uZktL9gCXSR85mk\n" + + "TieYeSoKZn6MM4rOAiEA1S/+7ez/gxDl01ztKeoHiUiW4FbEG4JUCzIITaGxVvM=\n" + + "-----END CERTIFICATE-----").getBytes(StandardCharsets.UTF_8); + + // PKCS12 certificate with a private key and full certificate chain + @SuppressWarnings("SpellCheckingInspection") + public static byte[] OCE_V1 = Base64.fromUrlSafeString("MIIIfAIBAzCCCDIGCSqGSIb3DQEHAaCCCCME" + + "gggfMIIIGzCCBtIGCSqGSIb3DQEHBqCCBsMwgga_AgEAMIIGuAYJKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTB" + + "KMCkGCSqGSIb3DQEFDDAcBAg8IcJO44iSgAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAllIH" + + "doQx_USA3jmRMeciiAggZQAHCPJ5lzPV0Z5tnssXZZ1AWm8AcKEq28gWUTVqVxc-0EcbKQHig1Jx7rqC3q4" + + "G4sboIRw1vDH6q5O8eGsbkeNuYBim8fZ08JrsjeJABJoEiJrPqplMWA7H6a7athg3YSu1v4OR3UKN5Gyzn3" + + "s0Yx5yMm_xzw204TEK5_1LpK8AMcUliFSq7jw3Xl1RY0zjMSWyQjX0KmB9IdubqQCfhy8zkKluAQADtHsEY" + + "An0F3LoMETQytyUSkIvGMZoFemkCWV7zZ5n5IPhXL7gvnTu0WS8UxEnz_-FYdF43cjmwGfSb3OpaxOND4PB" + + "CpwzbFfVCLa6mUBlwq1KQWRm1-PFm4LnL-3s2mxfjJAsVYP4U722_FHpW8rdTsyvdift9lsQjas2jIjCu8P" + + "FClFZJLQldu5FxOhKzx2gsjYS_aeTdefwjlRiGtEFSrE1snKBbnBeRYFocBjhTD_sy3Vj0i5sbWwTx7iq67" + + "joWydWAMp_lGSZ6akWRsyku_282jlwYsc3pR05qCHkbV0TzJcZofhXBwRgH5NKfulnJ1gH-i3e3RT3TauAK" + + "lqCeAfvDvA3-jxEDy_puPncod7WH0m9P4OmXjZ0s5EI4U-v6bKPgL7LlTCEI6yj15P7kxmruoxZlDAmhixV" + + "mlwJ8ZbVxD6Q-AOhXYPg-il3AYaRAS-VyJla0K-ac6hpYVAnbZCPzgHVkKC6iq4a_azf2b4uq9ks109jjnr" + + "yAChdBsGdmStpZaPW4koMSAIJf12vGRp5jNjSaxaIL5QxTn0WCO8FHi1oqTmlTSWvR8wwZLiBmqQtnNTpew" + + "iLL7C22lerUT7pYvKLCq_nnPYtb5UrSTHrmTNOUzEGVOSAGUWV293S4yiPGIwxT3dPE5_UaU_yKq1RonMRa" + + "PhOZEESZEwLKVCqyDVEbAt7Hdahp-Ex0FVrC5JQhpVQ0Wn6uCptF2Jup70u-P2kVWjxrGBuRrlgEkKuHcoh" + + "WoO9EMX_bLK9KcY4s1ofnfgSNagsAyX7N51Bmahgz1MCFOEcuFa375QYQhqkyLO2ZkNTpFQtjHjX0izZWO5" + + "5LN3rNpcD9-fZt6ldoZCpg-t6y5xqHy-7soH0BpxF1oGIHAUkYSuXpLY0M7Pt3qqvsJ4_ycmFUEyoGv8Ib_" + + "ieUBbebPz0Uhn-jaTpjgtKCyym7nBxVCuUv39vZ31nhNr4WaFsjdB_FOJh1s4KI6kQgzCSObrIVXBcLCTXP" + + "fZ3jWxspKIREHn-zNuW7jIkbugSRiNFfVArcc7cmU4av9JPSmFiZzeyA0gkrkESTg8DVPT16u7W5HREX4Cw" + + "mKu-12R6iYQ_po9Hcy6NJ8ShLdAzU0-q_BzgH7Cb8qimjgfGBA3Mesc-P98FlCzAjB2EgucRuXuehM_Femm" + + "ZyNl0qI1Mj9qOgx_HeYaJaYD-yXwojApmetFGtDtMJsDxwL0zK7eGXeHHa7pd7OybKdSjDq25CCTOZvfR0D" + + "D55FDIGCy0FsJTcferzPFlkz_Q45vEwuGfEBnXXS9IhH4ySvJmDmyfLMGiHW6t-9gjyEEg-dwSOq9yXYScf" + + "CsefRl7-o_9nDoNQ8s_XS7LKlJ72ZEBaKeAxcm6q4wVwUWITNNl1R3EYAsFBWzYt4Ka9Ob3igVaNfeG9K4p" + + "fQqMWcPpqVp4FuIsEpDWZYuv71s-WMYCs1JMfHbHDUczdRet1Ir2vLDGeWwvci70AzeKvvQ9OwBVESRec6c" + + "Vrgt3EJWLey5sXY01WpMm526fwtLolSMpCf-dNePT97nXemQCcr3QXimagHTSGPngG3577FPrSQJl-lCJDY" + + "xBFFtnd6hq4OcVr5HiNAbLnSjBWbzqxhHMmgoojy4rwtHmrfyVYKXyl-98r-Lobitv2tpnBqmjL6dMPRBOJ" + + "vQl8-Wp4MGBsi1gvTgW_-pLlMXT--1iYyxBeK9_AN5hfjtrivewE3JY531jwkrl3rUl50MKwBJMMAtQQIYr" + + "Dg7DAg_-QcOi-2mgo9zJPzR2jIXF0wP-9FA4-MITa2v78QVXcesh63agcFJCayGAL1StnbSBvvDqK5vEei3" + + "uGZbeJEpU1hikQx57w3UzS9O7OSQMFvRBOrFBQsYC4JzfF0soIweGNpJxpm-UNYz-hB9vCb8-3OHA069M0C" + + "AlJVOTF9uEpLVRzK-1kwggFBBgkqhkiG9w0BBwGgggEyBIIBLjCCASowggEmBgsqhkiG9w0BDAoBAqCB7zC" + + "B7DBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIexxrwNlHM34CAggAMAwGCCqGSIb3DQIJBQAwHQ" + + "YJYIZIAWUDBAEqBBAkK96h6gHJglyJl1_yEylvBIGQh62z7u5RoQ9y5wIXbE3_oMQTKVfCSrtqGUmj38sxD" + + "Y7yIoTVQq7sw0MPNeYHROgGUAzawU0DlXMGuOWrbgzYeURZs0_HZ2Cqk8qhVnD8TgpB2n0U0NB7aJRHlkzT" + + "l5MLFAwn3NE49CSzb891lGwfLYXYCfNfqltD7xZ7uvz6JAo_y6UtY8892wrRv4UdejyfMSUwIwYJKoZIhvc" + + "NAQkVMRYEFJBU0s1_6SLbIRbyeq65gLWqClWNMEEwMTANBglghkgBZQMEAgEFAAQgqkOJRTcBlnx5yn57k2" + + "3PH-qUXUGPEuYkrGy-DzEQiikECB0BXjHOZZhuAgIIAA=="); + public static char[] OCE_V1_PASSWORD = "password".toCharArray(); + + @SuppressWarnings("SpellCheckingInspection") + static final byte[] OCE_CERTS_V2 = ("-----BEGIN CERTIFICATE-----\n" + + "MIICRTCCAeugAwIBAgICEAAwCgYIKoZIzj0EAwIwaDELMAkGA1UEBhMCU1YxCzAJ\n" + + "BgNVBAgMAlNWMRIwEAYDVQQHDAlTdG9ja2hvbG0xEDAOBgNVBAoMB1Rlc3RPcmcx\n" + + "FDASBgNVBAsMC1Rlc3RPcmdVbml0MRAwDgYDVQQDDAdDQS1LTE9DMCAXDTI0MDgw\n" + + "MjA3NTI0MVoYDzIxMjQwNzA5MDc1MjQxWjBTMQswCQYDVQQGEwJTVjELMAkGA1UE\n" + + "CAwCU1YxEDAOBgNVBAoMB1Rlc3RPcmcxFDASBgNVBAsMC1Rlc3RPcmdVbml0MQ8w\n" + + "DQYDVQQDDAZDQS1PQ0UwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARQ8mUZsuiR\n" + + "njsqLh8WMPz2CB6rhF7FZhLNqBQCEBT9yP2TPSqtSizYj0IBnYVFpNEo+e70dU7F\n" + + "3VmjkbuEXMTAo4GXMIGUMB8GA1UdIwQYMBaAFHMPyjll59Oc2tkOYx70pZ0u6JcJ\n" + + "MAsGA1UdDwQEAwIDCDAZBgNVHSAEEjAQMA4GDCqGSIb8a2QACgIBADAUBgwqhkiG\n" + + "/GtkAAoCAAEEBAQCvyAwFAYMKoZIhvxrZAAKAgACBAQEAl8gMB0GA1UdDgQWBBTv\n" + + "xZlvtJr2cz2ZOX+fjOyJKKIUzDAKBggqhkjOPQQDAgNIADBFAiBlnbco6Ciwf+3Y\n" + + "EyK8ZU07zeQPp47+XUEfgPF1qL9TgwIhANSqEpSjoMJTpFO8gzOey+Q83mXOiCRp\n" + + "c6go7DG5ObQh\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIICLjCCAdSgAwIBAgIUOhaGJ8+QFxtHCXhvLRGhJY/wq7wwCgYIKoZIzj0EAwIw\n" + + "aDELMAkGA1UEBhMCU1YxCzAJBgNVBAgMAlNWMRIwEAYDVQQHDAlTdG9ja2hvbG0x\n" + + "EDAOBgNVBAoMB1Rlc3RPcmcxFDASBgNVBAsMC1Rlc3RPcmdVbml0MRAwDgYDVQQD\n" + + "DAdDQS1LTE9DMCAXDTI0MDgwMjA3NTE1MFoYDzIyMjQwNjE1MDc1MTUwWjBoMQsw\n" + + "CQYDVQQGEwJTVjELMAkGA1UECAwCU1YxEjAQBgNVBAcMCVN0b2NraG9sbTEQMA4G\n" + + "A1UECgwHVGVzdE9yZzEUMBIGA1UECwwLVGVzdE9yZ1VuaXQxEDAOBgNVBAMMB0NB\n" + + "LUtMT0MwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATRC0DYFzG0Lm0TwekgXYPz\n" + + "2ScKOARoft2t/E4WUyFiX0Snsy6S2y+hYP+bscnjUEKZplxE91MlsBNVhG09ZMx3\n" + + "o1owWDAdBgNVHQ4EFgQUcw/KOWXn05za2Q5jHvSlnS7olwkwCwYDVR0PBAQDAgEG\n" + + "MA8GA1UdEwEB/wQFMAMBAf8wGQYDVR0gBBIwEDAOBgwqhkiG/GtkAAoCARQwCgYI\n" + + "KoZIzj0EAwIDSAAwRQIhAPhg4A/O3RNNiBniSAzenpE1ftkaKSk21oMd95XKwdqb\n" + + "AiBzSpZzDimTeD/24luQ27oAOAmPI808aX8YMctqlo0LEg==\n" + + "-----END CERTIFICATE-----\n").getBytes(StandardCharsets.UTF_8); + + @SuppressWarnings("SpellCheckingInspection") + public static byte[] OCE_V2 = Base64.fromUrlSafeString("MIIE7AIBAzCCBKIGCSqGSIb3DQEHAaCCBJME" + + "ggSPMIIEizCCAzoGCSqGSIb3DQEHBqCCAyswggMnAgEAMIIDIAYJKoZIhvcNAQcBMF8GCSqGSIb3DQEFDTB" + + "SMDEGCSqGSIb3DQEFDDAkBBAn11qHSzbFWry2uT9sm1TkAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAw" + + "QBKgQQyd4c6OQQ_iirEEQOSE9XoYCCArDY1YqqfjynV3J09obO-_0WDGjJ-isRyYXbpRwOrw3r8-m7TmN1a" + + "nC7t0Gwb-Do2y3gbX54pko8lXY0rUyT0GTC1fr820Ghi72ovO6rr9JwSv2ndjOe-A4e65aisjMlHPZKJi1y" + + "YWsV9dJzO6OO75nnauX6SCeYCMTWzRKMZj7NHPaGFsl_jyaEqxAteY5tXYudzGmOYLIcSPMj_r0QbfL_Fo8" + + "DtkI6DcXOSVQnu-fHQl8SZAN-6qZ2eQr4TdsS3njn4gscgqsXnKw0FIhv3Js7ab89WHpLrjPHW31RJPSPWc" + + "vTFgMRb4O_2OmEm7aiwj4YnvBdnxtYmeCi5lbthZxW3MFu72DEAZQsnXvflGaV29rafUhKgwWWbiUvX0f4r" + + "XIUrfbQ1udf1I5l7LNC7RMwHYsObSOwoX6dDzj1kGroz1B_MeiIznT0RAd-YvLZ1SQM99MB8_gktWyJUO1e" + + "HjF4faBHgGAGCA0svdryKgy7D7TzvS2TjNOTWaJvRlaxzFmerXTmZytnDEbkU9eVyiiPR37KcpD2_KiLJzn" + + "vWKSo3wImP9-MPnxwNrkEcKf4qHZaE3ky_WLbnC2R8K5odIt7_01A0Nm-f-1NbuxtZBLy6tKnYUf3ir_PPm" + + "pjphS-D3coHn4Tlfa_chGNsMnP-TWDkxCgCyxHDvotpIynHxAibS9GHGHDF6nJekS9cFkGLJ9axJg2Pygic" + + "g78ls-CcDeAllFT46sU1d8Pz9A9MhJYmKc-AWBM00r9P32JC7kAANbOWErgUDwsRMxxHqVDo1U7QTL8LkBI" + + "CmVkpqzF7ObxwU04PB7E0k22sMp1zg95foVst3rZHriJ85CPq7Ht2ZsyLKmJNgqTfkgEJ_kVMuRRRcs4Pod" + + "4aHZL40qy6qrMru-qPTnVD0_2kV59W3xbd_ybMIIBSQYJKoZIhvcNAQcBoIIBOgSCATYwggEyMIIBLgYLKo" + + "ZIhvcNAQwKAQKggfcwgfQwXwYJKoZIhvcNAQUNMFIwMQYJKoZIhvcNAQUMMCQEEI2wPOcHSnElmbOyeO0dj" + + "bACAggAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBDH2f8UV8_hw3QAA7KO6WCYBIGQF6lkbV6ZPhC-" + + "s0WB-25i_Q6uPFieZsmqBsLHoVzXE8US_Kz4YitiVkVPY8JAcJSMI6DcQVkGPJW7suipx2evtne9kJI8TYh" + + "VsX0g7pv3HyTg2roDlHCbedoRHZcoC8lqy078LSdJicJmRLN0aj417oxRbMm972_pCHajk60hhA2A_fDoLA" + + "bEPJoO1qdFcjCjMSUwIwYJKoZIhvcNAQkVMRYEFOgid7IToNeY7_auRfGdSPhz0f-yMEEwMTANBglghkgBZ" + + "QMEAgEFAAQggxlm83v4OwO2nvegRpOK2G4HvCi0iO31XksWx0Pf5DkECAqUVhzeEdSPAgIIAA=="); + public static char[] OCE_V2_PASSWORD = "password".toCharArray(); + + public static void testImportKey(SecurityDomainTestState state) throws Throwable { + testImportKey(state, OCE_CERTS_V1, OCE_V1, OCE_V1_PASSWORD); + } + + public static void testImportKeyAlt(SecurityDomainTestState state) throws Throwable { + testImportKey(state, OCE_CERTS_V2, OCE_V2, OCE_V2_PASSWORD); + } + + public static void testImportKey( + SecurityDomainTestState state, + byte[] oceCerts, + byte[] oce, + char[] password) throws Throwable { + + assumeTrue("Device does not support SCP11a", state.getDeviceInfo().getVersion() + .isAtLeast(5, 7, 2)); + + state.withSecurityDomain(SecurityDomainSession::reset); + + // replace default SCP03 keys so that we can authenticate later + ScpKeyParams scp03KeyParams = importNewScp03Key(state); + + PublicKeyValues pk = state.withSecurityDomain(scp03KeyParams, sd -> { + return setupScp11a(sd, oceCerts); + }); + + // direct auth + state.withSecurityDomain( + getScp11aKeyParams(oce, password, pk.toPublicKey()), + sd -> { + Map> keyInformation = sd.getKeyInformation(); + assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); + }); + + // read public key and auth + state.withSecurityDomain(sd -> { + List certs = sd.getCertificateBundle(AUTH_SCP11A_KEY); + PublicKey publicKey = certs.get(certs.size() - 1).getPublicKey(); + ScpKeyParams params = getScp11aKeyParams(oce, password, publicKey); + sd.authenticate(params); + // use authenticated session to make a request + Map> keyInformation = sd.getKeyInformation(); + assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); + }); + + // read public key and then auth + PublicKey publicKey = state.withSecurityDomain(sd -> { + List certs = sd.getCertificateBundle(AUTH_SCP11A_KEY); + return certs.get(certs.size() - 1).getPublicKey(); + }); + + state.withSecurityDomain( + getScp11aKeyParams(oce, password, publicKey), + sd -> { + // use authenticated session to make a request + Map> keyInformation = sd.getKeyInformation(); + assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); + }); + } + + private static ScpKeyParams importNewScp03Key(SecurityDomainTestState state) throws Throwable { + final byte[] sk = new byte[]{ + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + }; + final StaticKeys staticKeys = new StaticKeys(sk, sk, sk); + + assumeFalse("SCP03 not supported over NFC on FIPS capable devices", + state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + + state.withSecurityDomain(sd -> { + sd.authenticate(new Scp03KeyParams(DEFAULT_SCP03_KEY, StaticKeys.getDefaultKeys())); + sd.putKey(AUTH_SCP03_KEY, staticKeys, 0); + }); + + return new Scp03KeyParams(AUTH_SCP03_KEY, staticKeys); + } + + private static Scp11KeyParams getScp11aKeyParams(byte[] pkcs12, char[] password, PublicKey pk) + throws Throwable { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + + try (InputStream is = new ByteArrayInputStream(pkcs12)) { + keyStore.load(is, password); + + final Enumeration aliases = keyStore.aliases(); + assertTrue(aliases.hasMoreElements()); + String alias = keyStore.aliases().nextElement(); + assertTrue(keyStore.isKeyEntry(alias)); + + Key sk = keyStore.getKey(keyStore.aliases().nextElement(), password); + assertTrue("No private key in pkcs12", sk instanceof PrivateKey); + + ScpCertificates certs = ScpCertificates.from(getCertificateChain(keyStore, alias)); + + List certChain = new ArrayList<>(certs.bundle); + if (certs.leaf != null) { + certChain.add(certs.leaf); + } + + return new Scp11KeyParams( + AUTH_SCP11A_KEY, + pk, + CA_KLOC_KEY_REF, + (PrivateKey) sk, + certChain + ); + } + } + + @SuppressWarnings("unchecked") + private static ScpCertificates getOceCertificates(byte[] pem) + throws CertificateException, IOException { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + try (InputStream is = new ByteArrayInputStream(pem)) { + return ScpCertificates.from((List) certificateFactory.generateCertificates(is)); + } + } + + private static byte[] getSki(X509Certificate certificate) { + byte[] skiExtensionValue = certificate.getExtensionValue("2.5.29.14"); + if (skiExtensionValue == null) { + return null; + } + assertNotNull("Missing Subject Key Identifier", skiExtensionValue); + Tlv tlv = Tlv.parse(skiExtensionValue); + assertEquals("Invalid extension value", 0x04, tlv.getTag()); + Tlv digest = Tlv.parse(tlv.getValue()); + assertEquals("Invalid Subject Key Identifier", 0x04, digest.getTag()); + return digest.getValue(); + } + + private static List getCertificateChain(KeyStore keyStore, String alias) + throws KeyStoreException { + Certificate[] chain = keyStore.getCertificateChain(alias); + final List certificateChain = new ArrayList<>(); + for (Certificate cert : chain) { + if (cert instanceof X509Certificate) { + certificateChain.add((X509Certificate) cert); + } + } + return certificateChain; + } + + private static PublicKeyValues setupScp11a(SecurityDomainSession sd, byte[] pem) + throws Throwable { + // generate new SCP11a key + PublicKeyValues generatedPk = sd.generateEcKey(AUTH_SCP11A_KEY, 0); + + // delete default SCP11b key + sd.deleteKey(new KeyRef(ScpKid.SCP11b, (byte) 1), false); + + // import OCE CA-KLOC certificate + ScpCertificates certs = getOceCertificates(pem); + + if (certs.ca == null) { + fail("Input does not contain valid CA-KLOC certificate"); + } + + sd.putKey(CA_KLOC_KEY_REF, PublicKeyValues.fromPublicKey(certs.ca.getPublicKey()), 0); + + byte[] ski = getSki(certs.ca); + assertNotNull("CA certificate missing Subject Key Identifier", ski); + sd.storeCaIssuer(CA_KLOC_KEY_REF, ski); + + // delete our SCP03 keys + sd.deleteKey(AUTH_SCP03_KEY, false); + + return generatedPk; + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java new file mode 100644 index 00000000..132e558a --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java @@ -0,0 +1,102 @@ +/* + * 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.sd; + +import static org.junit.Assert.fail; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nullable; + +public class ScpCertificates { + @Nullable final X509Certificate ca; + final List bundle; + @Nullable final X509Certificate leaf; + + ScpCertificates(@Nullable X509Certificate ca, List bundle, @Nullable X509Certificate leaf) { + this.ca = ca; + this.bundle = bundle; + this.leaf = leaf; + } + + static ScpCertificates from(@Nullable List certificates) { + if (certificates == null) { + return new ScpCertificates(null, Collections.emptyList(), null); + } + + X509Certificate ca = null; + + // order certificates with the Root CA on top + List ordered = new ArrayList<>(); + ordered.add(certificates.get(0)); + certificates.remove(0); + + while (!certificates.isEmpty()) { + X509Certificate head = ordered.get(0); + X509Certificate tail = ordered.get(ordered.size() - 1); + X509Certificate cert = certificates.get(0); + certificates.remove(0); + + if (isIssuedBy(cert, cert)) { + ordered.add(0, cert); + ca = ordered.get(0); + continue; + } + + if (isIssuedBy(cert, tail)) { + ordered.add(cert); + continue; + } + + if (isIssuedBy(head, cert)) { + ordered.add(0, cert); + continue; + } + + fail("Cannot decide the order of " + cert + " in " + ordered); + } + + // find ca and leaf + if (ca != null) { + ordered.remove(0); + } + + X509Certificate leaf = null; + if (!ordered.isEmpty()) { + X509Certificate lastCert = ordered.get(ordered.size() - 1); + final boolean[] keyUsage = lastCert.getKeyUsage(); + if (keyUsage != null && keyUsage[4]) { + leaf = lastCert; + ordered.remove(leaf); + } + } + + return new ScpCertificates(ca, ordered, leaf); + } + + /** + * @param subjectCert the certificate which we test if it is issued by the issuerCert + * @param issuerCert the certificate which should issue the subjectCertificate + * @return true if the subject certificate is issued by the issuer certificate + */ + private static boolean isIssuedBy(X509Certificate subjectCert, X509Certificate issuerCert) { + return subjectCert.getIssuerX500Principal().equals(issuerCert.getSubjectX500Principal()); + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java index 29133566..33a561c3 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java @@ -20,15 +20,13 @@ import static org.junit.Assume.assumeTrue; import com.yubico.yubikit.core.YubiKeyDevice; -import com.yubico.yubikit.core.application.ApplicationNotAvailableException; import com.yubico.yubikit.core.smartcard.SmartCardConnection; -import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; import com.yubico.yubikit.testing.TestState; -import java.io.IOException; +import org.bouncycastle.jce.provider.BouncyCastleProvider; -import javax.annotation.Nullable; +import java.security.Security; public class SecurityDomainTestState extends TestState { @@ -46,11 +44,19 @@ public SecurityDomainTestState build() throws Throwable { protected SecurityDomainTestState(Builder builder) throws Throwable { super(builder); + setupJca(); + try (SmartCardConnection connection = openSmartCardConnection()) { + assumeTrue("Key does not support smart card connection", connection != null); SecurityDomainSession sd = getSecurityDomainSession(connection); assumeTrue("Security domain not supported", sd != null); assertNull("These tests expect kid to be null", scpParameters.getKid()); } } + + public static void setupJca() { + Security.removeProvider("BC"); + Security.addProvider(new BouncyCastleProvider()); + } } From 117cc29158686c3ebc3ac2708a9f4c003df796c0 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 6 Aug 2024 15:45:26 +0200 Subject: [PATCH 38/46] add additional tests --- .../core/smartcard/scp/Scp11KeyParams.java | 9 + .../smartcard/scp/SecurityDomainSession.java | 18 +- .../yubico/yubikit/testing/sd/Scp03Tests.java | 17 +- .../yubikit/testing/sd/Scp11aTests.java | 46 +++- .../SecurityDomainInstrumentedTests.java | 17 +- .../yubikit/testing/sd/Scp03DeviceTests.java | 185 +++++++-------- .../yubikit/testing/sd/Scp11aDeviceTests.java | 222 +++++++++++++++++- 7 files changed, 377 insertions(+), 137 deletions(-) diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java index d79dcee2..ec23197c 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java @@ -70,4 +70,13 @@ public Scp11KeyParams(KeyRef keyRef, PublicKey pkSdEcka) { public KeyRef getKeyRef() { return keyRef; } + + public List getCertificates() { + return certificates; + } + + @Nullable + public KeyRef getOceKeyRef() { + return oceKeyRef; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java index b1337c05..f5abdc11 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java @@ -97,7 +97,7 @@ public SecurityDomainSession(SmartCardConnection connection, @Nullable ScpKeyPar protocol.select(AppId.SECURITYDOMAIN); // We don't know the version, but we know it's at least 5.3.0 protocol.configure(new Version(5, 3, 0)); - if(scpKeyParams != null) { + if (scpKeyParams != null) { try { protocol.initScp(scpKeyParams); } catch (BadResponseException | ApduException e) { @@ -240,7 +240,7 @@ public void storeAllowlist(KeyRef keyRef, List serials) throws ApduE Logger.debug(logger, "Storing serial allowlist for {}", keyRef); ByteArrayOutputStream data = new ByteArrayOutputStream(); for (BigInteger serial : serials) { - data.write(serial.toByteArray()); + data.write(new Tlv(0x93, serial.toByteArray()).getBytes()); } storeData(Tlvs.encodeList(Arrays.asList( new Tlv(0xA6, new Tlv(0x83, keyRef.getBytes()).getBytes()), @@ -320,7 +320,11 @@ public PublicKeyValues.Ec generateEcKey(KeyRef keyRef, int replaceKvn) throws Ap (replaceKvn == 0 ? "" : String.format(Locale.ROOT, ", replacing KVN=0x%02x", replaceKvn)), keyRef); byte[] params = new Tlv(KEY_TYPE_ECC_KEY_PARAMS, new byte[]{0}).getBytes(); - byte[] data = ByteBuffer.allocate(params.length + 1).put(keyRef.getKvn()).put(params).array(); + byte[] data = ByteBuffer + .allocate(params.length + 1) + .put(keyRef.getKvn()) + .put(params) + .array(); byte[] resp = protocol.sendAndReceive(new Apdu(0x80, INS_GENERATE_KEY, replaceKvn, keyRef.getKid(), data)); byte[] encodedPoint = Tlvs.unpackValue(KEY_TYPE_ECC_PUBLIC_KEY, resp); return PublicKeyValues.Ec.fromEncodedPoint(EllipticCurveValues.SECP256R1, encodedPoint); @@ -378,7 +382,9 @@ public void putKey(KeyRef keyRef, StaticKeys keys, int replaceKvn) throws ApduEx */ public void putKey(KeyRef keyRef, PrivateKeyValues secretKey, int replaceKvn) throws ApduException, IOException, BadResponseException { Logger.debug(logger, "Importing SCP11 private key into {}", keyRef); - if (!(secretKey instanceof PrivateKeyValues.Ec) || !((PrivateKeyValues.Ec) secretKey).getCurveParams().equals(EllipticCurveValues.SECP256R1)) { + if (!(secretKey instanceof PrivateKeyValues.Ec) || + !((PrivateKeyValues.Ec) secretKey).getCurveParams() + .equals(EllipticCurveValues.SECP256R1)) { throw new IllegalArgumentException("Private key must be of type SECP256R1"); } if (dataEncryptor == null) { @@ -415,7 +421,9 @@ public void putKey(KeyRef keyRef, PrivateKeyValues secretKey, int replaceKvn) th */ public void putKey(KeyRef keyRef, PublicKeyValues publicKey, int replaceKvn) throws ApduException, IOException, BadResponseException { Logger.debug(logger, "Importing SCP11 public key into {}", keyRef); - if (!(publicKey instanceof PublicKeyValues.Ec) || !((PublicKeyValues.Ec) publicKey).getCurveParams().equals(EllipticCurveValues.SECP256R1)) { + if (!(publicKey instanceof PublicKeyValues.Ec) || + !((PublicKeyValues.Ec) publicKey).getCurveParams() + .equals(EllipticCurveValues.SECP256R1)) { throw new IllegalArgumentException("Public key must be of type SECP256R1"); } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp03Tests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp03Tests.java index 467b51b7..42515ec8 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp03Tests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp03Tests.java @@ -19,23 +19,34 @@ import com.yubico.yubikit.testing.SmokeTest; import com.yubico.yubikit.testing.framework.SecurityDomainInstrumentedTests; +import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; public class Scp03Tests extends SecurityDomainInstrumentedTests { + + @Before + public void before() throws Throwable { + withState(Scp03DeviceTests::before); + } @Test public void testImportKey() throws Throwable { - withDevice(Scp03DeviceTests::testImportKey); + withState(Scp03DeviceTests::testImportKey); } @Test public void testDeleteKey() throws Throwable { - withDevice(Scp03DeviceTests::testDeleteKey); + withState(Scp03DeviceTests::testDeleteKey); } @Test @Category(SmokeTest.class) public void testReplaceKey() throws Throwable { - withDevice(Scp03DeviceTests::testReplaceKey); + withState(Scp03DeviceTests::testReplaceKey); + } + + @Test + public void testWrongKey() throws Throwable { + withState(Scp03DeviceTests::testWrongKey); } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11aTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11aTests.java index 5a41d8cf..168deb83 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11aTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11aTests.java @@ -18,12 +18,54 @@ import com.yubico.yubikit.testing.framework.SecurityDomainInstrumentedTests; +import org.junit.Before; import org.junit.Test; public class Scp11aTests extends SecurityDomainInstrumentedTests { + + @Before + public void before() throws Throwable { + withState(Scp11aDeviceTests::before); + } + @Test public void testImportKey() throws Throwable { - withDevice(Scp11aDeviceTests::testImportKey); - withDevice(Scp11aDeviceTests::testImportKeyAlt); + withState(Scp11aDeviceTests::testImportKey); + withState(Scp11aDeviceTests::testImportKeyAlt); + } + + @Test + public void testAuthenticate() throws Throwable { + withState(Scp11aDeviceTests::testAuthenticate); + } + + @Test + public void testAllowlist() throws Throwable { + withState(Scp11aDeviceTests::testAllowList); + } + + @Test + public void testAllowlistBlocked() throws Throwable { + withState(Scp11aDeviceTests::testAllowListBlocked); + } + + @Test + public void testScp11cAuthenticate() throws Throwable { + withState(Scp11aDeviceTests::testScp11cAuthenticate); + } + + @Test + public void testScp11bAuthenticate() throws Throwable { + withState(Scp11aDeviceTests::testScp11bAuthenticate); + } + + @Test + public void testScp11bWrongPubKey() throws Throwable { + withState(Scp11aDeviceTests::testScp11bWrongPubKey); + } + + @Test + public void testScp11bImport() throws Throwable { + withState(Scp11aDeviceTests::testScp11bImport); } } diff --git a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/SecurityDomainInstrumentedTests.java b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/SecurityDomainInstrumentedTests.java index 71de4a28..88596f8a 100644 --- a/testing-android/src/main/java/com/yubico/yubikit/testing/framework/SecurityDomainInstrumentedTests.java +++ b/testing-android/src/main/java/com/yubico/yubikit/testing/framework/SecurityDomainInstrumentedTests.java @@ -16,31 +16,16 @@ package com.yubico.yubikit.testing.framework; -import com.yubico.yubikit.core.smartcard.scp.ScpKid; -import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; import com.yubico.yubikit.testing.TestState; import com.yubico.yubikit.testing.sd.SecurityDomainTestState; public class SecurityDomainInstrumentedTests extends YKInstrumentedTests { - protected void withDevice(TestState.StatefulDeviceCallback callback) throws Throwable { + protected void withState(TestState.StatefulDeviceCallback callback) throws Throwable { final SecurityDomainTestState state = new SecurityDomainTestState.Builder(device) .reconnectDeviceCallback(this::reconnectDevice) .build(); state.withDeviceCallback(callback); } - - protected void withSecurityDomainSession(TestState.StatefulSessionCallback callback) throws Throwable { - final SecurityDomainTestState state = new SecurityDomainTestState.Builder(device).scpKid(getScpKid()) - .build(); - state.withSecurityDomain(callback); - } - - protected void withScp11Session(TestState.StatefulSessionCallback callback) throws Throwable { - final SecurityDomainTestState state = new SecurityDomainTestState.Builder(device).scpKid(ScpKid.SCP11b) - .build(); - state.withSecurityDomain(callback); - } - } \ No newline at end of file diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java index 22830380..0d8f0f84 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java @@ -16,21 +16,31 @@ package com.yubico.yubikit.testing.sd; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assume.assumeFalse; +import com.yubico.yubikit.core.application.BadResponseException; +import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.core.smartcard.scp.KeyRef; import com.yubico.yubikit.core.smartcard.scp.Scp03KeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpKid; import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; import com.yubico.yubikit.core.smartcard.scp.StaticKeys; +import com.yubico.yubikit.core.util.RandomUtils; -import java.util.Map; +import java.io.IOException; public class Scp03DeviceTests { static final KeyRef DEFAULT_KEY = new KeyRef((byte) 0x01, (byte) 0xff); + static final ScpKeyParams defaultRef = new Scp03KeyParams(DEFAULT_KEY, StaticKeys.getDefaultKeys()); + + public static void before(SecurityDomainTestState state) throws Throwable { + assumeFalse("SCP03 not supported over NFC on FIPS capable devices", + state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + state.withSecurityDomain(SecurityDomainSession::reset); + } public static void testImportKey(SecurityDomainTestState state) throws Throwable { @@ -39,166 +49,137 @@ public static void testImportKey(SecurityDomainTestState state) throws Throwable 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, }; final StaticKeys staticKeys = new StaticKeys(sk, sk, sk); - final KeyRef newKey = new KeyRef((byte) 0x01, (byte) 0x01); + final KeyRef ref = new KeyRef((byte) 0x01, (byte) 0x01); + final ScpKeyParams params = new Scp03KeyParams(ref, staticKeys); assumeFalse("SCP03 not supported over NFC on FIPS capable devices", state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); - state.withSecurityDomain(SecurityDomainSession::reset); state.withSecurityDomain(sd -> { - sd.authenticate(new Scp03KeyParams(DEFAULT_KEY, StaticKeys.getDefaultKeys())); - sd.putKey(newKey, staticKeys, 0); + sd.authenticate(defaultRef); + sd.putKey(ref, staticKeys, 0); }); state.withSecurityDomain(sd -> { - Map> keyInformation = sd.getKeyInformation(); - // verify there are three SCP03 keys with kvn 0x01 - assertNotNull(keyInformation.get(scp03EncOf(newKey))); - assertNotNull(keyInformation.get(scp03MacOf(newKey))); - assertNotNull(keyInformation.get(scp03DekOf(newKey))); + sd.authenticate(params); + }); - // the default keys are gone - assertNull(keyInformation.get(scp03EncOf(DEFAULT_KEY))); - assertNull(keyInformation.get(scp03MacOf(DEFAULT_KEY))); - assertNull(keyInformation.get(scp03DekOf(DEFAULT_KEY))); + state.withSecurityDomain(sd -> { + // cannot use default key to authenticate + assertThrows(ApduException.class, () -> sd.authenticate(defaultRef)); }); + } public static void testDeleteKey(SecurityDomainTestState state) throws Throwable { - - final byte[] bytes1 = new byte[]{ - 0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x67, - 0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x67, - }; - final StaticKeys staticKeys1 = new StaticKeys(bytes1, bytes1, bytes1); - - final byte[] bytes2 = new byte[]{ - 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, - 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, - }; - final StaticKeys staticKeys2 = new StaticKeys(bytes2, bytes2, bytes2); - + final StaticKeys staticKeys1 = randomStaticKeys(); + final StaticKeys staticKeys2 = randomStaticKeys(); final KeyRef keyRef1 = new KeyRef((byte) 0x01, (byte) 0x10); final KeyRef keyRef2 = new KeyRef((byte) 0x01, (byte) 0x55); + final Scp03KeyParams ref1 = new Scp03KeyParams(keyRef1, staticKeys1); + final ScpKeyParams ref2 = new Scp03KeyParams(keyRef2, staticKeys2); - assumeFalse("SCP03 not supported over NFC on FIPS capable devices", - state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); - - state.withSecurityDomain(SecurityDomainSession::reset); state.withSecurityDomain(sd -> { - sd.authenticate(new Scp03KeyParams(DEFAULT_KEY, StaticKeys.getDefaultKeys())); + sd.authenticate(defaultRef); sd.putKey(keyRef1, staticKeys1, 0); }); // authenticate with the new key and put the second state.withSecurityDomain(sd -> { - sd.authenticate(new Scp03KeyParams(keyRef1, staticKeys1)); + sd.authenticate(ref1); sd.putKey(keyRef2, staticKeys2, 0); + }); - // there are 2 SCP03 keys - Map> keyInformation = sd.getKeyInformation(); - // verify there are three SCP03 keys with kvn 0x01 - assertNotNull(keyInformation.get(scp03EncOf(keyRef1))); - assertNotNull(keyInformation.get(scp03MacOf(keyRef1))); - assertNotNull(keyInformation.get(scp03DekOf(keyRef1))); + state.withSecurityDomain(sd -> { + sd.authenticate(ref1); + }); - assertNotNull(keyInformation.get(scp03EncOf(keyRef2))); - assertNotNull(keyInformation.get(scp03MacOf(keyRef2))); - assertNotNull(keyInformation.get(scp03DekOf(keyRef2))); + state.withSecurityDomain(sd -> { + sd.authenticate(ref2); }); // delete first key state.withSecurityDomain(sd -> { - // authenticate with the second key - sd.authenticate(new Scp03KeyParams(keyRef2, staticKeys2)); + sd.authenticate(ref2); sd.deleteKey(keyRef1, false); + }); - // verify there are three the first key is deleted - final Map> keyInformation = sd.getKeyInformation(); - assertNull(keyInformation.get(scp03EncOf(keyRef1))); - assertNull(keyInformation.get(scp03MacOf(keyRef1))); - assertNull(keyInformation.get(scp03DekOf(keyRef1))); + state.withSecurityDomain(sd -> { + assertThrows(ApduException.class, () -> sd.authenticate(ref1)); + }); - assertNotNull(keyInformation.get(scp03EncOf(keyRef2))); - assertNotNull(keyInformation.get(scp03MacOf(keyRef2))); - assertNotNull(keyInformation.get(scp03DekOf(keyRef2))); + state.withSecurityDomain(sd -> { + sd.authenticate(ref2); }); // delete the second key state.withSecurityDomain(sd -> { - // authenticate with the second key - sd.authenticate(new Scp03KeyParams(keyRef2, staticKeys2)); + sd.authenticate(ref2); sd.deleteKey(keyRef2, true); // the last key + }); - // verify that the second key is gone - final Map> keyInformation = sd.getKeyInformation(); - assertNull(keyInformation.get(scp03EncOf(keyRef2))); - assertNull(keyInformation.get(scp03MacOf(keyRef2))); - assertNull(keyInformation.get(scp03DekOf(keyRef2))); - - // there now should be a default SCP03 key - // it might be missing at all if the YubiKey supports other SCP versions - for (KeyRef keyRef : keyInformation.keySet()) { - final byte kid = keyRef.getKid(); - if (kid == (byte) 0x01 || kid == (byte) 0x02 || kid == (byte) 0x03) { - assertEquals(DEFAULT_KEY.getKvn(), keyRef.getKvn()); - } - } + state.withSecurityDomain(sd -> { + assertThrows(ApduException.class, () -> sd.authenticate(ref2)); }); } public static void testReplaceKey(SecurityDomainTestState state) throws Throwable { - - final byte[] bytes1 = new byte[]{ - 0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x67, - 0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x67, - }; - final StaticKeys staticKeys1 = new StaticKeys(bytes1, bytes1, bytes1); - - final byte[] bytes2 = new byte[]{ - 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, - 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, - }; - final StaticKeys staticKeys2 = new StaticKeys(bytes2, bytes2, bytes2); + final StaticKeys staticKeys1 = randomStaticKeys(); + final StaticKeys staticKeys2 = randomStaticKeys(); final KeyRef keyRef1 = new KeyRef((byte) 0x01, (byte) 0x10); final KeyRef keyRef2 = new KeyRef((byte) 0x01, (byte) 0x55); - assumeFalse("SCP03 not supported over NFC on FIPS capable devices", - state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + final ScpKeyParams ref1 = new Scp03KeyParams(keyRef1, staticKeys1); + final ScpKeyParams ref2 = new Scp03KeyParams(keyRef2, staticKeys2); - state.withSecurityDomain(SecurityDomainSession::reset); state.withSecurityDomain(sd -> { - sd.authenticate(new Scp03KeyParams(DEFAULT_KEY, StaticKeys.getDefaultKeys())); + sd.authenticate(defaultRef); sd.putKey(keyRef1, staticKeys1, 0); }); // authenticate with the new key and replace it with the second state.withSecurityDomain(sd -> { - sd.authenticate(new Scp03KeyParams(keyRef1, staticKeys1)); + sd.authenticate(ref1); sd.putKey(keyRef2, staticKeys2, keyRef1.getKvn()); + }); - // there is only one SCP03 key - Map> keyInformation = sd.getKeyInformation(); - assertNull(keyInformation.get(scp03EncOf(keyRef1))); - assertNull(keyInformation.get(scp03MacOf(keyRef1))); - assertNull(keyInformation.get(scp03DekOf(keyRef1))); + state.withSecurityDomain(sd -> { + assertThrows(ApduException.class, () -> sd.authenticate(ref1)); + }); - assertNotNull(keyInformation.get(scp03EncOf(keyRef2))); - assertNotNull(keyInformation.get(scp03MacOf(keyRef2))); - assertNotNull(keyInformation.get(scp03DekOf(keyRef2))); + state.withSecurityDomain(sd -> { + sd.authenticate(ref2); }); } - private static KeyRef scp03EncOf(KeyRef key) { - return new KeyRef((byte) 0x01, key.getKvn()); + public static void testWrongKey(SecurityDomainTestState state) throws Throwable { + final StaticKeys staticKeys = randomStaticKeys(); + final KeyRef ref = new KeyRef((byte) 0x01, (byte) 0x01); + final ScpKeyParams params = new Scp03KeyParams(ref, staticKeys); + + state.withSecurityDomain(sd -> { + assertThrows(ApduException.class, () -> sd.authenticate(params)); + assertThrows(ApduException.class, () -> verifyAuth(sd)); + }); + + state.withSecurityDomain(sd -> { + sd.authenticate(defaultRef); + }); } - private static KeyRef scp03MacOf(KeyRef key) { - return new KeyRef((byte) 0x02, key.getKvn()); + private static StaticKeys randomStaticKeys() { + return new StaticKeys( + RandomUtils.getRandomBytes(16), + RandomUtils.getRandomBytes(16), + RandomUtils.getRandomBytes(16) + ); } - private static KeyRef scp03DekOf(KeyRef key) { - return new KeyRef((byte) 0x03, key.getKvn()); + private static void verifyAuth(SecurityDomainSession session) + throws BadResponseException, ApduException, IOException { + KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x7f); + session.generateEcKey(ref, 0); + session.deleteKey(ref, false); } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java index 256c0b76..ba06d99f 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java @@ -18,13 +18,17 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; +import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.internal.codec.Base64; +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.scp.KeyRef; import com.yubico.yubikit.core.smartcard.scp.Scp03KeyParams; import com.yubico.yubikit.core.smartcard.scp.Scp11KeyParams; @@ -32,13 +36,17 @@ import com.yubico.yubikit.core.smartcard.scp.ScpKid; import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; import com.yubico.yubikit.core.smartcard.scp.StaticKeys; +import com.yubico.yubikit.core.util.RandomUtils; import com.yubico.yubikit.core.util.Tlv; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.PrivateKey; @@ -47,7 +55,9 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; import java.util.ArrayList; +import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.Map; @@ -181,6 +191,10 @@ public class Scp11aDeviceTests { "QMEAgEFAAQggxlm83v4OwO2nvegRpOK2G4HvCi0iO31XksWx0Pf5DkECAqUVhzeEdSPAgIIAA=="); public static char[] OCE_V2_PASSWORD = "password".toCharArray(); + public static void before(SecurityDomainTestState state) throws Throwable { + state.withSecurityDomain(SecurityDomainSession::reset); + } + public static void testImportKey(SecurityDomainTestState state) throws Throwable { testImportKey(state, OCE_CERTS_V1, OCE_V1, OCE_V1_PASSWORD); } @@ -189,7 +203,7 @@ public static void testImportKeyAlt(SecurityDomainTestState state) throws Throwa testImportKey(state, OCE_CERTS_V2, OCE_V2, OCE_V2_PASSWORD); } - public static void testImportKey( + private static void testImportKey( SecurityDomainTestState state, byte[] oceCerts, byte[] oce, @@ -201,7 +215,7 @@ public static void testImportKey( state.withSecurityDomain(SecurityDomainSession::reset); // replace default SCP03 keys so that we can authenticate later - ScpKeyParams scp03KeyParams = importNewScp03Key(state); + ScpKeyParams scp03KeyParams = replaceDefaultScp03Key(state); PublicKeyValues pk = state.withSecurityDomain(scp03KeyParams, sd -> { return setupScp11a(sd, oceCerts); @@ -241,18 +255,163 @@ public static void testImportKey( }); } - private static ScpKeyParams importNewScp03Key(SecurityDomainTestState state) throws Throwable { - final byte[] sk = new byte[]{ - 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, - 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, - }; - final StaticKeys staticKeys = new StaticKeys(sk, sk, sk); + private static final ScpKeyParams defaultScp03KeyParams = + new Scp03KeyParams(DEFAULT_SCP03_KEY, StaticKeys.getDefaultKeys()); + + public static void testAuthenticate(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x03; + + ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, sd -> { + return loadKeys(sd, ScpKid.SCP11a, kvn); + }); + + state.withSecurityDomain(keyParams, sd -> { + sd.deleteKey(new KeyRef(ScpKid.SCP11a, kvn), false); + }); + } + + public static void testAllowList(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x03; + + ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, sd -> { + Scp11KeyParams params = loadKeys(sd, ScpKid.SCP11a, kvn); + assertNotNull(params.getOceKeyRef()); + + List serials = new ArrayList<>(); + for (X509Certificate cert : params.getCertificates()) { + serials.add(cert.getSerialNumber()); + } + + sd.storeAllowlist(params.getOceKeyRef(), serials); + return params; + }); + + state.withSecurityDomain(keyParams, sd -> { + sd.deleteKey(new KeyRef(ScpKid.SCP11a, kvn), false); + }); + } + + public static void testAllowListBlocked(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x03; + final KeyRef ref = new KeyRef(ScpKid.SCP03, (byte) 2); + + ScpKeyParams scp03KeyParams = replaceDefaultScp03Key(state); + + Scp11KeyParams keyParams = state.withSecurityDomain(scp03KeyParams, sd -> { + + sd.deleteKey(new KeyRef(ScpKid.SCP11b, (byte) 1), false); + + Scp11KeyParams scp11KeyParams = loadKeys(sd, ScpKid.SCP11a, kvn); + assertNotNull(scp11KeyParams.getOceKeyRef()); + + final List serials = Arrays.asList( + BigInteger.valueOf(1), BigInteger.valueOf(2), BigInteger.valueOf(3), + BigInteger.valueOf(4), BigInteger.valueOf(5)); + + sd.storeAllowlist(scp11KeyParams.getOceKeyRef(), serials); + + return scp11KeyParams; + }); + + // authenticate with scp11a will throw + state.withSecurityDomain(sd -> { + assertThrows(ApduException.class, () -> sd.authenticate(keyParams)); + }); + + // reset the allow list + state.withSecurityDomain(scp03KeyParams, sd -> { + assertNotNull(keyParams.getOceKeyRef()); + sd.storeAllowlist(keyParams.getOceKeyRef(), new ArrayList<>()); + }); + + // authenticate with scp11a will not throw + state.withSecurityDomain(sd -> { + sd.authenticate(keyParams); + }); + } + + public static void testScp11cAuthenticate(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x03; + + ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, sd -> { + return loadKeys(sd, ScpKid.SCP11c, kvn); + }); + + state.withSecurityDomain(keyParams, sd -> { + assertThrows(ApduException.class, () -> + sd.deleteKey(new KeyRef(ScpKid.SCP11c, kvn), false)); + }); + } + + public static void testScp11bAuthenticate(SecurityDomainTestState state) throws Throwable { + final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); + + List chain = state.withSecurityDomain(defaultScp03KeyParams, sd -> { + return sd.getCertificateBundle(ref); + }); + + X509Certificate leaf = chain.get(chain.size() - 1); + Scp11KeyParams params = new Scp11KeyParams(ref, leaf.getPublicKey()); + + state.withSecurityDomain(params, sd -> { + assertThrows(ApduException.class, () -> verifyScp11bAuth(sd)); + }); + } + + public static void testScp11bWrongPubKey(SecurityDomainTestState state) throws Throwable { + final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); + + List chain = state.withSecurityDomain(defaultScp03KeyParams, sd -> { + return sd.getCertificateBundle(ref); + }); + + X509Certificate cert = chain.get(0); + Scp11KeyParams params = new Scp11KeyParams(ref, cert.getPublicKey()); + + state.withSecurityDomain(sd -> { + BadResponseException e = + assertThrows(BadResponseException.class, () -> sd.authenticate(params)); + assertEquals("Receipt does not match", e.getMessage()); + }); + } + + public static void testScp11bImport(SecurityDomainTestState state) throws Throwable { + final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x2); + + ScpKeyParams keyParams = state.withSecurityDomain(sd -> { + sd.authenticate(defaultScp03KeyParams); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + ECGenParameterSpec ecParams = new ECGenParameterSpec("secp256r1"); + kpg.initialize(ecParams); + KeyPair keyPair = kpg.generateKeyPair(); + sd.putKey(ref, PrivateKeyValues.fromPrivateKey(keyPair.getPrivate()), 0); + return new Scp11KeyParams(ref, keyPair.getPublic()); + }); + + state.withSecurityDomain(sd -> { + sd.authenticate(keyParams); + }); + } + + private static void verifyScp11bAuth(SecurityDomainSession session) + throws BadResponseException, ApduException, IOException { + KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x7f); + session.generateEcKey(ref, 0); + session.deleteKey(ref, false); + } + + private static ScpKeyParams replaceDefaultScp03Key(SecurityDomainTestState state) throws Throwable { + final StaticKeys staticKeys = new StaticKeys( + RandomUtils.getRandomBytes(16), + RandomUtils.getRandomBytes(16), + RandomUtils.getRandomBytes(16) + ); assumeFalse("SCP03 not supported over NFC on FIPS capable devices", state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); state.withSecurityDomain(sd -> { - sd.authenticate(new Scp03KeyParams(DEFAULT_SCP03_KEY, StaticKeys.getDefaultKeys())); + sd.authenticate(defaultScp03KeyParams); sd.putKey(AUTH_SCP03_KEY, staticKeys, 0); }); @@ -351,4 +510,49 @@ private static PublicKeyValues setupScp11a(SecurityDomainSession sd, byte[] pem) return generatedPk; } + + private static Scp11KeyParams loadKeys(SecurityDomainSession sd, byte kid, byte kvn) + throws Throwable { + KeyRef sdRef = new KeyRef(kid, kvn); + KeyRef oceRef = new KeyRef((byte) 0x10, kvn); + + PublicKeyValues publicKeyValues = sd.generateEcKey(sdRef, 0); + + ScpCertificates oceCerts = getOceCertificates(OCE_CERTS_V1); + assertNotNull("Missing CA", oceCerts.ca); + sd.putKey(oceRef, PublicKeyValues.fromPublicKey(oceCerts.ca.getPublicKey()), 0); + + byte[] ski = getSki(oceCerts.ca); + assertNotNull("CA certificate missing Subject Key Identifier", ski); + sd.storeCaIssuer(oceRef, ski); + + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + + try (InputStream is = new ByteArrayInputStream(OCE_V1)) { + keyStore.load(is, OCE_V1_PASSWORD); + + final Enumeration aliases = keyStore.aliases(); + assertTrue(aliases.hasMoreElements()); + String alias = keyStore.aliases().nextElement(); + assertTrue(keyStore.isKeyEntry(alias)); + + Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_V1_PASSWORD); + assertTrue("No private key in pkcs12", sk instanceof PrivateKey); + + ScpCertificates certs = ScpCertificates.from(getCertificateChain(keyStore, alias)); + + List certChain = new ArrayList<>(certs.bundle); + if (certs.leaf != null) { + certChain.add(certs.leaf); + } + + return new Scp11KeyParams( + sdRef, + publicKeyValues.toPublicKey(), + oceRef, + (PrivateKey) sk, + certChain + ); + } + } } From e8660218c2cd4dd633cbdac3ce3b5d26d8aa9559 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 6 Aug 2024 16:11:46 +0200 Subject: [PATCH 39/46] refactor --- .../sd/{Scp11aTests.java => Scp11Tests.java} | 38 +- .../testing/sd/SecurityDomainTests.java | 2 +- .../yubikit/testing/sd/Scp03DeviceTests.java | 88 +-- .../yubikit/testing/sd/Scp11DeviceTests.java | 435 ++++++++++++++ .../yubikit/testing/sd/Scp11TestData.java | 145 +++++ .../yubikit/testing/sd/Scp11aDeviceTests.java | 558 ------------------ .../yubikit/testing/sd/ScpCertificates.java | 11 +- 7 files changed, 654 insertions(+), 623 deletions(-) rename testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/{Scp11aTests.java => Scp11Tests.java} (57%) create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java create mode 100644 testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java delete mode 100644 testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11aTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11Tests.java similarity index 57% rename from testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11aTests.java rename to testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11Tests.java index 168deb83..5d833fef 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11aTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11Tests.java @@ -21,51 +21,51 @@ import org.junit.Before; import org.junit.Test; -public class Scp11aTests extends SecurityDomainInstrumentedTests { +public class Scp11Tests extends SecurityDomainInstrumentedTests { @Before public void before() throws Throwable { - withState(Scp11aDeviceTests::before); + withState(Scp11DeviceTests::before); } @Test - public void testImportKey() throws Throwable { - withState(Scp11aDeviceTests::testImportKey); - withState(Scp11aDeviceTests::testImportKeyAlt); + public void testScp11aImportKey() throws Throwable { + withState(Scp11DeviceTests::testScp11aImportKey); + withState(Scp11DeviceTests::testScp11aImportKeyAlt); } @Test - public void testAuthenticate() throws Throwable { - withState(Scp11aDeviceTests::testAuthenticate); + public void testScp11aAuthenticate() throws Throwable { + withState(Scp11DeviceTests::testScp11aAuthenticate); } @Test - public void testAllowlist() throws Throwable { - withState(Scp11aDeviceTests::testAllowList); + public void testScp11aAllowlist() throws Throwable { + withState(Scp11DeviceTests::testScp11aAllowList); } @Test - public void testAllowlistBlocked() throws Throwable { - withState(Scp11aDeviceTests::testAllowListBlocked); - } - - @Test - public void testScp11cAuthenticate() throws Throwable { - withState(Scp11aDeviceTests::testScp11cAuthenticate); + public void testScp11aAllowlistBlocked() throws Throwable { + withState(Scp11DeviceTests::testScp11aAllowListBlocked); } @Test public void testScp11bAuthenticate() throws Throwable { - withState(Scp11aDeviceTests::testScp11bAuthenticate); + withState(Scp11DeviceTests::testScp11bAuthenticate); } @Test public void testScp11bWrongPubKey() throws Throwable { - withState(Scp11aDeviceTests::testScp11bWrongPubKey); + withState(Scp11DeviceTests::testScp11bWrongPubKey); } @Test public void testScp11bImport() throws Throwable { - withState(Scp11aDeviceTests::testScp11bImport); + withState(Scp11DeviceTests::testScp11bImport); + } + + @Test + public void testScp11cAuthenticate() throws Throwable { + withState(Scp11DeviceTests::testScp11cAuthenticate); } } diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/SecurityDomainTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/SecurityDomainTests.java index 73844b3d..4fbbc10f 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/SecurityDomainTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/SecurityDomainTests.java @@ -22,7 +22,7 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ Scp03Tests.class, - Scp11aTests.class + Scp11Tests.class }) public class SecurityDomainTests { } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java index 0d8f0f84..06c7c0c8 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java @@ -55,18 +55,18 @@ public static void testImportKey(SecurityDomainTestState state) throws Throwable assumeFalse("SCP03 not supported over NFC on FIPS capable devices", state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); - state.withSecurityDomain(sd -> { - sd.authenticate(defaultRef); - sd.putKey(ref, staticKeys, 0); + state.withSecurityDomain(session -> { + session.authenticate(defaultRef); + session.putKey(ref, staticKeys, 0); }); - state.withSecurityDomain(sd -> { - sd.authenticate(params); + state.withSecurityDomain(session -> { + session.authenticate(params); }); - state.withSecurityDomain(sd -> { + state.withSecurityDomain(session -> { // cannot use default key to authenticate - assertThrows(ApduException.class, () -> sd.authenticate(defaultRef)); + assertThrows(ApduException.class, () -> session.authenticate(defaultRef)); }); } @@ -79,47 +79,47 @@ public static void testDeleteKey(SecurityDomainTestState state) throws Throwable final Scp03KeyParams ref1 = new Scp03KeyParams(keyRef1, staticKeys1); final ScpKeyParams ref2 = new Scp03KeyParams(keyRef2, staticKeys2); - state.withSecurityDomain(sd -> { - sd.authenticate(defaultRef); - sd.putKey(keyRef1, staticKeys1, 0); + state.withSecurityDomain(session -> { + session.authenticate(defaultRef); + session.putKey(keyRef1, staticKeys1, 0); }); // authenticate with the new key and put the second - state.withSecurityDomain(sd -> { - sd.authenticate(ref1); - sd.putKey(keyRef2, staticKeys2, 0); + state.withSecurityDomain(session -> { + session.authenticate(ref1); + session.putKey(keyRef2, staticKeys2, 0); }); - state.withSecurityDomain(sd -> { - sd.authenticate(ref1); + state.withSecurityDomain(session -> { + session.authenticate(ref1); }); - state.withSecurityDomain(sd -> { - sd.authenticate(ref2); + state.withSecurityDomain(session -> { + session.authenticate(ref2); }); // delete first key - state.withSecurityDomain(sd -> { - sd.authenticate(ref2); - sd.deleteKey(keyRef1, false); + state.withSecurityDomain(session -> { + session.authenticate(ref2); + session.deleteKey(keyRef1, false); }); - state.withSecurityDomain(sd -> { - assertThrows(ApduException.class, () -> sd.authenticate(ref1)); + state.withSecurityDomain(session -> { + assertThrows(ApduException.class, () -> session.authenticate(ref1)); }); - state.withSecurityDomain(sd -> { - sd.authenticate(ref2); + state.withSecurityDomain(session -> { + session.authenticate(ref2); }); // delete the second key - state.withSecurityDomain(sd -> { - sd.authenticate(ref2); - sd.deleteKey(keyRef2, true); // the last key + state.withSecurityDomain(session -> { + session.authenticate(ref2); + session.deleteKey(keyRef2, true); // the last key }); - state.withSecurityDomain(sd -> { - assertThrows(ApduException.class, () -> sd.authenticate(ref2)); + state.withSecurityDomain(session -> { + assertThrows(ApduException.class, () -> session.authenticate(ref2)); }); } @@ -133,23 +133,23 @@ public static void testReplaceKey(SecurityDomainTestState state) throws Throwabl final ScpKeyParams ref1 = new Scp03KeyParams(keyRef1, staticKeys1); final ScpKeyParams ref2 = new Scp03KeyParams(keyRef2, staticKeys2); - state.withSecurityDomain(sd -> { - sd.authenticate(defaultRef); - sd.putKey(keyRef1, staticKeys1, 0); + state.withSecurityDomain(session -> { + session.authenticate(defaultRef); + session.putKey(keyRef1, staticKeys1, 0); }); // authenticate with the new key and replace it with the second - state.withSecurityDomain(sd -> { - sd.authenticate(ref1); - sd.putKey(keyRef2, staticKeys2, keyRef1.getKvn()); + state.withSecurityDomain(session -> { + session.authenticate(ref1); + session.putKey(keyRef2, staticKeys2, keyRef1.getKvn()); }); - state.withSecurityDomain(sd -> { - assertThrows(ApduException.class, () -> sd.authenticate(ref1)); + state.withSecurityDomain(session -> { + assertThrows(ApduException.class, () -> session.authenticate(ref1)); }); - state.withSecurityDomain(sd -> { - sd.authenticate(ref2); + state.withSecurityDomain(session -> { + session.authenticate(ref2); }); } @@ -158,13 +158,13 @@ public static void testWrongKey(SecurityDomainTestState state) throws Throwable final KeyRef ref = new KeyRef((byte) 0x01, (byte) 0x01); final ScpKeyParams params = new Scp03KeyParams(ref, staticKeys); - state.withSecurityDomain(sd -> { - assertThrows(ApduException.class, () -> sd.authenticate(params)); - assertThrows(ApduException.class, () -> verifyAuth(sd)); + state.withSecurityDomain(session -> { + assertThrows(ApduException.class, () -> session.authenticate(params)); + assertThrows(ApduException.class, () -> verifyAuth(session)); }); - state.withSecurityDomain(sd -> { - sd.authenticate(defaultRef); + state.withSecurityDomain(session -> { + session.authenticate(defaultRef); }); } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java new file mode 100644 index 00000000..9d8b92c5 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2023 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.sd; + +import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_CERTS_V1; +import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_CERTS_V2; +import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_V1; +import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_V1_PASSWORD; +import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_V2; +import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_V2_PASSWORD; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import com.yubico.yubikit.core.application.BadResponseException; +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.scp.KeyRef; +import com.yubico.yubikit.core.smartcard.scp.Scp03KeyParams; +import com.yubico.yubikit.core.smartcard.scp.Scp11KeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; +import com.yubico.yubikit.core.smartcard.scp.ScpKid; +import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; +import com.yubico.yubikit.core.smartcard.scp.StaticKeys; +import com.yubico.yubikit.core.util.RandomUtils; +import com.yubico.yubikit.core.util.Tlv; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +public class Scp11DeviceTests { + + static final KeyRef defaultRef = new KeyRef((byte) 0x01, (byte) 0xff); + static final KeyRef AUTH_SCP03_KEY = new KeyRef((byte) 0x01, (byte) 0x01); + static final KeyRef AUTH_SCP11A_KEY = new KeyRef(ScpKid.SCP11a, (byte) 2); + static final KeyRef CA_KLOC_KEY_REF = new KeyRef((byte) 0x10, (byte) 2); + + public static void before(SecurityDomainTestState state) throws Throwable { + assumeTrue("Device does not support SCP11a", + state.getDeviceInfo().getVersion().isAtLeast(5, 7, 2)); + state.withSecurityDomain(SecurityDomainSession::reset); + } + + public static void testScp11aImportKey(SecurityDomainTestState state) throws Throwable { + testScp11aImportKey(state, OCE_CERTS_V1, OCE_V1, OCE_V1_PASSWORD); + } + + public static void testScp11aImportKeyAlt(SecurityDomainTestState state) throws Throwable { + testScp11aImportKey(state, OCE_CERTS_V2, OCE_V2, OCE_V2_PASSWORD); + } + + private static void testScp11aImportKey( + SecurityDomainTestState state, + byte[] oceCerts, + byte[] oce, + char[] password) throws Throwable { + + state.withSecurityDomain(SecurityDomainSession::reset); + + // replace default SCP03 keys so that we can authenticate later + ScpKeyParams scp03KeyParams = replaceDefaultScp03Key(state); + + PublicKeyValues pk = state.withSecurityDomain(scp03KeyParams, session -> { + return setupScp11a(session, oceCerts); + }); + + // direct auth + state.withSecurityDomain( + getScp11aKeyParams(oce, password, pk.toPublicKey()), + session -> { + Map> keyInformation = session.getKeyInformation(); + assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); + }); + + // read public key and auth + state.withSecurityDomain(session -> { + List certs = session.getCertificateBundle(AUTH_SCP11A_KEY); + PublicKey publicKey = certs.get(certs.size() - 1).getPublicKey(); + ScpKeyParams params = getScp11aKeyParams(oce, password, publicKey); + session.authenticate(params); + Map> keyInformation = session.getKeyInformation(); + assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); + }); + + // read public key and then auth + PublicKey publicKey = state.withSecurityDomain(session -> { + List certs = session.getCertificateBundle(AUTH_SCP11A_KEY); + return certs.get(certs.size() - 1).getPublicKey(); + }); + + state.withSecurityDomain( + getScp11aKeyParams(oce, password, publicKey), + session -> { + Map> keyInformation = session.getKeyInformation(); + assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); + }); + } + + private static final ScpKeyParams defaultScp03KeyParams = + new Scp03KeyParams(defaultRef, StaticKeys.getDefaultKeys()); + + public static void testScp11aAuthenticate(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x03; + + ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, session -> { + return loadKeys(session, ScpKid.SCP11a, kvn); + }); + + state.withSecurityDomain(keyParams, session -> { + session.deleteKey(new KeyRef(ScpKid.SCP11a, kvn), false); + }); + } + + public static void testScp11aAllowList(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x05; + + ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, session -> { + Scp11KeyParams params = loadKeys(session, ScpKid.SCP11a, kvn); + assertNotNull(params.getOceKeyRef()); + + List serials = new ArrayList<>(); + for (X509Certificate cert : params.getCertificates()) { + serials.add(cert.getSerialNumber()); + } + + session.storeAllowlist(params.getOceKeyRef(), serials); + return params; + }); + + state.withSecurityDomain(keyParams, session -> { + session.deleteKey(new KeyRef(ScpKid.SCP11a, kvn), false); + }); + } + + public static void testScp11aAllowListBlocked(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x03; + + ScpKeyParams scp03KeyParams = replaceDefaultScp03Key(state); + + Scp11KeyParams keyParams = state.withSecurityDomain(scp03KeyParams, session -> { + session.deleteKey(new KeyRef(ScpKid.SCP11b, (byte) 1), false); + + Scp11KeyParams scp11KeyParams = loadKeys(session, ScpKid.SCP11a, kvn); + assertNotNull(scp11KeyParams.getOceKeyRef()); + + final List serials = Arrays.asList( + BigInteger.valueOf(1), BigInteger.valueOf(2), BigInteger.valueOf(3), + BigInteger.valueOf(4), BigInteger.valueOf(5)); + + session.storeAllowlist(scp11KeyParams.getOceKeyRef(), serials); + + return scp11KeyParams; + }); + + // authenticate with scp11a will throw + state.withSecurityDomain(session -> { + assertThrows(ApduException.class, () -> session.authenticate(keyParams)); + }); + + // reset the allow list + state.withSecurityDomain(scp03KeyParams, session -> { + assertNotNull(keyParams.getOceKeyRef()); + session.storeAllowlist(keyParams.getOceKeyRef(), new ArrayList<>()); + }); + + // authenticate with scp11a will not throw + state.withSecurityDomain(session -> { + session.authenticate(keyParams); + }); + } + + public static void testScp11bAuthenticate(SecurityDomainTestState state) throws Throwable { + final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); + + List chain = state.withSecurityDomain(defaultScp03KeyParams, session -> { + return session.getCertificateBundle(ref); + }); + + X509Certificate leaf = chain.get(chain.size() - 1); + Scp11KeyParams params = new Scp11KeyParams(ref, leaf.getPublicKey()); + + state.withSecurityDomain(params, session -> { + assertThrows(ApduException.class, () -> verifyScp11bAuth(session)); + }); + } + + public static void testScp11bWrongPubKey(SecurityDomainTestState state) throws Throwable { + final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); + + List chain = state.withSecurityDomain(defaultScp03KeyParams, session -> { + return session.getCertificateBundle(ref); + }); + + X509Certificate cert = chain.get(0); + Scp11KeyParams params = new Scp11KeyParams(ref, cert.getPublicKey()); + + state.withSecurityDomain(session -> { + BadResponseException e = + assertThrows(BadResponseException.class, () -> session.authenticate(params)); + assertEquals("Receipt does not match", e.getMessage()); + }); + } + + public static void testScp11bImport(SecurityDomainTestState state) throws Throwable { + final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x2); + + ScpKeyParams keyParams = state.withSecurityDomain(session -> { + session.authenticate(defaultScp03KeyParams); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + ECGenParameterSpec ecParams = new ECGenParameterSpec("secp256r1"); + kpg.initialize(ecParams); + KeyPair keyPair = kpg.generateKeyPair(); + session.putKey(ref, PrivateKeyValues.fromPrivateKey(keyPair.getPrivate()), 0); + return new Scp11KeyParams(ref, keyPair.getPublic()); + }); + + state.withSecurityDomain(session -> { + session.authenticate(keyParams); + }); + } + + private static void verifyScp11bAuth(SecurityDomainSession session) + throws BadResponseException, ApduException, IOException { + KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x7f); + session.generateEcKey(ref, 0); + session.deleteKey(ref, false); + } + + public static void testScp11cAuthenticate(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x03; + + ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, session -> { + return loadKeys(session, ScpKid.SCP11c, kvn); + }); + + state.withSecurityDomain(keyParams, session -> { + assertThrows(ApduException.class, () -> + session.deleteKey(new KeyRef(ScpKid.SCP11c, kvn), false)); + }); + } + + private static ScpKeyParams replaceDefaultScp03Key(SecurityDomainTestState state) throws Throwable { + assumeFalse("SCP03 management not supported over NFC on FIPS capable devices", + state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + + final StaticKeys staticKeys = new StaticKeys( + RandomUtils.getRandomBytes(16), + RandomUtils.getRandomBytes(16), + RandomUtils.getRandomBytes(16) + ); + + state.withSecurityDomain(session -> { + session.authenticate(defaultScp03KeyParams); + session.putKey(AUTH_SCP03_KEY, staticKeys, 0); + }); + + return new Scp03KeyParams(AUTH_SCP03_KEY, staticKeys); + } + + private static Scp11KeyParams getScp11aKeyParams(byte[] pkcs12, char[] password, PublicKey pk) + throws Throwable { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + + try (InputStream is = new ByteArrayInputStream(pkcs12)) { + keyStore.load(is, password); + + final Enumeration aliases = keyStore.aliases(); + assertTrue(aliases.hasMoreElements()); + String alias = keyStore.aliases().nextElement(); + assertTrue(keyStore.isKeyEntry(alias)); + + Key sk = keyStore.getKey(keyStore.aliases().nextElement(), password); + assertTrue("No private key in pkcs12", sk instanceof PrivateKey); + + ScpCertificates certs = ScpCertificates.from(getCertificateChain(keyStore, alias)); + + List certChain = new ArrayList<>(certs.bundle); + if (certs.leaf != null) { + certChain.add(certs.leaf); + } + + return new Scp11KeyParams( + AUTH_SCP11A_KEY, + pk, + CA_KLOC_KEY_REF, + (PrivateKey) sk, + certChain + ); + } + } + + @SuppressWarnings("unchecked") + private static ScpCertificates getOceCertificates(byte[] pem) + throws CertificateException, IOException { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + try (InputStream is = new ByteArrayInputStream(pem)) { + return ScpCertificates.from((List) certificateFactory.generateCertificates(is)); + } + } + + private static byte[] getSki(X509Certificate certificate) { + byte[] skiExtensionValue = certificate.getExtensionValue("2.5.29.14"); + if (skiExtensionValue == null) { + return null; + } + assertNotNull("Missing Subject Key Identifier", skiExtensionValue); + Tlv tlv = Tlv.parse(skiExtensionValue); + assertEquals("Invalid extension value", 0x04, tlv.getTag()); + Tlv digest = Tlv.parse(tlv.getValue()); + assertEquals("Invalid Subject Key Identifier", 0x04, digest.getTag()); + return digest.getValue(); + } + + private static List getCertificateChain(KeyStore keyStore, String alias) + throws KeyStoreException { + Certificate[] chain = keyStore.getCertificateChain(alias); + final List certificateChain = new ArrayList<>(); + for (Certificate cert : chain) { + if (cert instanceof X509Certificate) { + certificateChain.add((X509Certificate) cert); + } + } + return certificateChain; + } + + private static PublicKeyValues setupScp11a(SecurityDomainSession session, byte[] pem) + throws Throwable { + // generate new SCP11a key + PublicKeyValues generatedPk = session.generateEcKey(AUTH_SCP11A_KEY, 0); + + // delete default SCP11b key + session.deleteKey(new KeyRef(ScpKid.SCP11b, (byte) 1), false); + + // import OCE CA-KLOC certificate + ScpCertificates certs = getOceCertificates(pem); + + if (certs.ca == null) { + fail("Input does not contain valid CA-KLOC certificate"); + } + + session.putKey(CA_KLOC_KEY_REF, PublicKeyValues.fromPublicKey(certs.ca.getPublicKey()), 0); + + byte[] ski = getSki(certs.ca); + assertNotNull("CA certificate missing Subject Key Identifier", ski); + session.storeCaIssuer(CA_KLOC_KEY_REF, ski); + + // delete our SCP03 keys + session.deleteKey(AUTH_SCP03_KEY, false); + + return generatedPk; + } + + private static Scp11KeyParams loadKeys(SecurityDomainSession session, byte kid, byte kvn) + throws Throwable { + KeyRef sessionRef = new KeyRef(kid, kvn); + KeyRef oceRef = new KeyRef((byte) 0x10, kvn); + + PublicKeyValues publicKeyValues = session.generateEcKey(sessionRef, 0); + + ScpCertificates oceCerts = getOceCertificates(OCE_CERTS_V1); + assertNotNull("Missing CA", oceCerts.ca); + session.putKey(oceRef, PublicKeyValues.fromPublicKey(oceCerts.ca.getPublicKey()), 0); + + byte[] ski = getSki(oceCerts.ca); + assertNotNull("CA certificate missing Subject Key Identifier", ski); + session.storeCaIssuer(oceRef, ski); + + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + + try (InputStream is = new ByteArrayInputStream(OCE_V1)) { + keyStore.load(is, OCE_V1_PASSWORD); + + final Enumeration aliases = keyStore.aliases(); + assertTrue(aliases.hasMoreElements()); + String alias = keyStore.aliases().nextElement(); + assertTrue(keyStore.isKeyEntry(alias)); + + Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_V1_PASSWORD); + assertTrue("No private key in pkcs12", sk instanceof PrivateKey); + + ScpCertificates certs = ScpCertificates.from(getCertificateChain(keyStore, alias)); + + List certChain = new ArrayList<>(certs.bundle); + if (certs.leaf != null) { + certChain.add(certs.leaf); + } + + return new Scp11KeyParams( + sessionRef, + publicKeyValues.toPublicKey(), + oceRef, + (PrivateKey) sk, + certChain + ); + } + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java new file mode 100644 index 00000000..dc0c3a41 --- /dev/null +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java @@ -0,0 +1,145 @@ +/* + * 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.sd; + +import com.yubico.yubikit.core.internal.codec.Base64; + +import java.nio.charset.StandardCharsets; + +class Scp11TestData { + @SuppressWarnings("SpellCheckingInspection") + static final byte[] OCE_CERTS_V1 = ("-----BEGIN CERTIFICATE-----\n" + + "MIIB8DCCAZegAwIBAgIUf0lxsK1R+EydqZKLLV/vXhaykgowCgYIKoZIzj0EAwIw\n" + + "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + + "NDA1MjgwOTIyMDlaFw0yNDA4MjYwOTIyMDlaMC8xLTArBgNVBAMMJEV4YW1wbGUg\n" + + "T0NFIEludGVybWVkaWF0ZSBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49\n" + + "AwEHA0IABMXbjb+Y33+GP8qUznrdZSJX9b2qC0VUS1WDhuTlQUfg/RBNFXb2/qWt\n" + + "h/a+Ag406fV7wZW2e4PPH+Le7EwS1nyjgZUwgZIwHQYDVR0OBBYEFJzdQCINVBES\n" + + "R4yZBN2l5CXyzlWsMB8GA1UdIwQYMBaAFDGqVWafYGfoHzPc/QT+3nPlcZ89MBIG\n" + + "A1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgIEMCwGA1UdIAEB/wQiMCAw\n" + + "DgYMKoZIhvxrZAAKAgEoMA4GDCqGSIb8a2QACgIBADAKBggqhkjOPQQDAgNHADBE\n" + + "AiBE5SpNEKDW3OehDhvTKT9g1cuuIyPdaXGLZ3iX0x0VcwIgdnIirhlKocOKGXf9\n" + + "ijkE8e+9dTazSPLf24lSIf0IGC8=\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIIB2zCCAYGgAwIBAgIUSf59wIpCKOrNGNc5FMPTD9zDGVAwCgYIKoZIzj0EAwIw\n" + + "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + + "NDA1MjgwOTIyMDlaFw0yNDA2MjcwOTIyMDlaMCoxKDAmBgNVBAMMH0V4YW1wbGUg\n" + + "T0NFIFJvb3QgQ0EgQ2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n" + + "AASPrxfpSB/AvuvLKaCz1YTx68Xbtx8S9xAMfRGwzp5cXMdF8c7AWpUfeM3BQ26M\n" + + "h0WPvyBJKhCdeK8iVCaHyr5Jo4GEMIGBMB0GA1UdDgQWBBQxqlVmn2Bn6B8z3P0E\n" + + "/t5z5XGfPTASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBBjA8BgNV\n" + + "HSABAf8EMjAwMA4GDCqGSIb8a2QACgIBFDAOBgwqhkiG/GtkAAoCASgwDgYMKoZI\n" + + "hvxrZAAKAgEAMAoGCCqGSM49BAMCA0gAMEUCIHv8cgOzxq2n1uZktL9gCXSR85mk\n" + + "TieYeSoKZn6MM4rOAiEA1S/+7ez/gxDl01ztKeoHiUiW4FbEG4JUCzIITaGxVvM=\n" + + "-----END CERTIFICATE-----").getBytes(StandardCharsets.UTF_8); + + // PKCS12 certificate with a private key and full certificate chain + @SuppressWarnings("SpellCheckingInspection") + public static byte[] OCE_V1 = Base64.fromUrlSafeString("MIIIfAIBAzCCCDIGCSqGSIb3DQEHAaCCCCME" + + "gggfMIIIGzCCBtIGCSqGSIb3DQEHBqCCBsMwgga_AgEAMIIGuAYJKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTB" + + "KMCkGCSqGSIb3DQEFDDAcBAg8IcJO44iSgAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAllIH" + + "doQx_USA3jmRMeciiAggZQAHCPJ5lzPV0Z5tnssXZZ1AWm8AcKEq28gWUTVqVxc-0EcbKQHig1Jx7rqC3q4" + + "G4sboIRw1vDH6q5O8eGsbkeNuYBim8fZ08JrsjeJABJoEiJrPqplMWA7H6a7athg3YSu1v4OR3UKN5Gyzn3" + + "s0Yx5yMm_xzw204TEK5_1LpK8AMcUliFSq7jw3Xl1RY0zjMSWyQjX0KmB9IdubqQCfhy8zkKluAQADtHsEY" + + "An0F3LoMETQytyUSkIvGMZoFemkCWV7zZ5n5IPhXL7gvnTu0WS8UxEnz_-FYdF43cjmwGfSb3OpaxOND4PB" + + "CpwzbFfVCLa6mUBlwq1KQWRm1-PFm4LnL-3s2mxfjJAsVYP4U722_FHpW8rdTsyvdift9lsQjas2jIjCu8P" + + "FClFZJLQldu5FxOhKzx2gsjYS_aeTdefwjlRiGtEFSrE1snKBbnBeRYFocBjhTD_sy3Vj0i5sbWwTx7iq67" + + "joWydWAMp_lGSZ6akWRsyku_282jlwYsc3pR05qCHkbV0TzJcZofhXBwRgH5NKfulnJ1gH-i3e3RT3TauAK" + + "lqCeAfvDvA3-jxEDy_puPncod7WH0m9P4OmXjZ0s5EI4U-v6bKPgL7LlTCEI6yj15P7kxmruoxZlDAmhixV" + + "mlwJ8ZbVxD6Q-AOhXYPg-il3AYaRAS-VyJla0K-ac6hpYVAnbZCPzgHVkKC6iq4a_azf2b4uq9ks109jjnr" + + "yAChdBsGdmStpZaPW4koMSAIJf12vGRp5jNjSaxaIL5QxTn0WCO8FHi1oqTmlTSWvR8wwZLiBmqQtnNTpew" + + "iLL7C22lerUT7pYvKLCq_nnPYtb5UrSTHrmTNOUzEGVOSAGUWV293S4yiPGIwxT3dPE5_UaU_yKq1RonMRa" + + "PhOZEESZEwLKVCqyDVEbAt7Hdahp-Ex0FVrC5JQhpVQ0Wn6uCptF2Jup70u-P2kVWjxrGBuRrlgEkKuHcoh" + + "WoO9EMX_bLK9KcY4s1ofnfgSNagsAyX7N51Bmahgz1MCFOEcuFa375QYQhqkyLO2ZkNTpFQtjHjX0izZWO5" + + "5LN3rNpcD9-fZt6ldoZCpg-t6y5xqHy-7soH0BpxF1oGIHAUkYSuXpLY0M7Pt3qqvsJ4_ycmFUEyoGv8Ib_" + + "ieUBbebPz0Uhn-jaTpjgtKCyym7nBxVCuUv39vZ31nhNr4WaFsjdB_FOJh1s4KI6kQgzCSObrIVXBcLCTXP" + + "fZ3jWxspKIREHn-zNuW7jIkbugSRiNFfVArcc7cmU4av9JPSmFiZzeyA0gkrkESTg8DVPT16u7W5HREX4Cw" + + "mKu-12R6iYQ_po9Hcy6NJ8ShLdAzU0-q_BzgH7Cb8qimjgfGBA3Mesc-P98FlCzAjB2EgucRuXuehM_Femm" + + "ZyNl0qI1Mj9qOgx_HeYaJaYD-yXwojApmetFGtDtMJsDxwL0zK7eGXeHHa7pd7OybKdSjDq25CCTOZvfR0D" + + "D55FDIGCy0FsJTcferzPFlkz_Q45vEwuGfEBnXXS9IhH4ySvJmDmyfLMGiHW6t-9gjyEEg-dwSOq9yXYScf" + + "CsefRl7-o_9nDoNQ8s_XS7LKlJ72ZEBaKeAxcm6q4wVwUWITNNl1R3EYAsFBWzYt4Ka9Ob3igVaNfeG9K4p" + + "fQqMWcPpqVp4FuIsEpDWZYuv71s-WMYCs1JMfHbHDUczdRet1Ir2vLDGeWwvci70AzeKvvQ9OwBVESRec6c" + + "Vrgt3EJWLey5sXY01WpMm526fwtLolSMpCf-dNePT97nXemQCcr3QXimagHTSGPngG3577FPrSQJl-lCJDY" + + "xBFFtnd6hq4OcVr5HiNAbLnSjBWbzqxhHMmgoojy4rwtHmrfyVYKXyl-98r-Lobitv2tpnBqmjL6dMPRBOJ" + + "vQl8-Wp4MGBsi1gvTgW_-pLlMXT--1iYyxBeK9_AN5hfjtrivewE3JY531jwkrl3rUl50MKwBJMMAtQQIYr" + + "Dg7DAg_-QcOi-2mgo9zJPzR2jIXF0wP-9FA4-MITa2v78QVXcesh63agcFJCayGAL1StnbSBvvDqK5vEei3" + + "uGZbeJEpU1hikQx57w3UzS9O7OSQMFvRBOrFBQsYC4JzfF0soIweGNpJxpm-UNYz-hB9vCb8-3OHA069M0C" + + "AlJVOTF9uEpLVRzK-1kwggFBBgkqhkiG9w0BBwGgggEyBIIBLjCCASowggEmBgsqhkiG9w0BDAoBAqCB7zC" + + "B7DBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIexxrwNlHM34CAggAMAwGCCqGSIb3DQIJBQAwHQ" + + "YJYIZIAWUDBAEqBBAkK96h6gHJglyJl1_yEylvBIGQh62z7u5RoQ9y5wIXbE3_oMQTKVfCSrtqGUmj38sxD" + + "Y7yIoTVQq7sw0MPNeYHROgGUAzawU0DlXMGuOWrbgzYeURZs0_HZ2Cqk8qhVnD8TgpB2n0U0NB7aJRHlkzT" + + "l5MLFAwn3NE49CSzb891lGwfLYXYCfNfqltD7xZ7uvz6JAo_y6UtY8892wrRv4UdejyfMSUwIwYJKoZIhvc" + + "NAQkVMRYEFJBU0s1_6SLbIRbyeq65gLWqClWNMEEwMTANBglghkgBZQMEAgEFAAQgqkOJRTcBlnx5yn57k2" + + "3PH-qUXUGPEuYkrGy-DzEQiikECB0BXjHOZZhuAgIIAA=="); + public static char[] OCE_V1_PASSWORD = "password".toCharArray(); + + @SuppressWarnings("SpellCheckingInspection") + static final byte[] OCE_CERTS_V2 = ("-----BEGIN CERTIFICATE-----\n" + + "MIICRTCCAeugAwIBAgICEAAwCgYIKoZIzj0EAwIwaDELMAkGA1UEBhMCU1YxCzAJ\n" + + "BgNVBAgMAlNWMRIwEAYDVQQHDAlTdG9ja2hvbG0xEDAOBgNVBAoMB1Rlc3RPcmcx\n" + + "FDASBgNVBAsMC1Rlc3RPcmdVbml0MRAwDgYDVQQDDAdDQS1LTE9DMCAXDTI0MDgw\n" + + "MjA3NTI0MVoYDzIxMjQwNzA5MDc1MjQxWjBTMQswCQYDVQQGEwJTVjELMAkGA1UE\n" + + "CAwCU1YxEDAOBgNVBAoMB1Rlc3RPcmcxFDASBgNVBAsMC1Rlc3RPcmdVbml0MQ8w\n" + + "DQYDVQQDDAZDQS1PQ0UwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARQ8mUZsuiR\n" + + "njsqLh8WMPz2CB6rhF7FZhLNqBQCEBT9yP2TPSqtSizYj0IBnYVFpNEo+e70dU7F\n" + + "3VmjkbuEXMTAo4GXMIGUMB8GA1UdIwQYMBaAFHMPyjll59Oc2tkOYx70pZ0u6JcJ\n" + + "MAsGA1UdDwQEAwIDCDAZBgNVHSAEEjAQMA4GDCqGSIb8a2QACgIBADAUBgwqhkiG\n" + + "/GtkAAoCAAEEBAQCvyAwFAYMKoZIhvxrZAAKAgACBAQEAl8gMB0GA1UdDgQWBBTv\n" + + "xZlvtJr2cz2ZOX+fjOyJKKIUzDAKBggqhkjOPQQDAgNIADBFAiBlnbco6Ciwf+3Y\n" + + "EyK8ZU07zeQPp47+XUEfgPF1qL9TgwIhANSqEpSjoMJTpFO8gzOey+Q83mXOiCRp\n" + + "c6go7DG5ObQh\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIICLjCCAdSgAwIBAgIUOhaGJ8+QFxtHCXhvLRGhJY/wq7wwCgYIKoZIzj0EAwIw\n" + + "aDELMAkGA1UEBhMCU1YxCzAJBgNVBAgMAlNWMRIwEAYDVQQHDAlTdG9ja2hvbG0x\n" + + "EDAOBgNVBAoMB1Rlc3RPcmcxFDASBgNVBAsMC1Rlc3RPcmdVbml0MRAwDgYDVQQD\n" + + "DAdDQS1LTE9DMCAXDTI0MDgwMjA3NTE1MFoYDzIyMjQwNjE1MDc1MTUwWjBoMQsw\n" + + "CQYDVQQGEwJTVjELMAkGA1UECAwCU1YxEjAQBgNVBAcMCVN0b2NraG9sbTEQMA4G\n" + + "A1UECgwHVGVzdE9yZzEUMBIGA1UECwwLVGVzdE9yZ1VuaXQxEDAOBgNVBAMMB0NB\n" + + "LUtMT0MwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATRC0DYFzG0Lm0TwekgXYPz\n" + + "2ScKOARoft2t/E4WUyFiX0Snsy6S2y+hYP+bscnjUEKZplxE91MlsBNVhG09ZMx3\n" + + "o1owWDAdBgNVHQ4EFgQUcw/KOWXn05za2Q5jHvSlnS7olwkwCwYDVR0PBAQDAgEG\n" + + "MA8GA1UdEwEB/wQFMAMBAf8wGQYDVR0gBBIwEDAOBgwqhkiG/GtkAAoCARQwCgYI\n" + + "KoZIzj0EAwIDSAAwRQIhAPhg4A/O3RNNiBniSAzenpE1ftkaKSk21oMd95XKwdqb\n" + + "AiBzSpZzDimTeD/24luQ27oAOAmPI808aX8YMctqlo0LEg==\n" + + "-----END CERTIFICATE-----\n").getBytes(StandardCharsets.UTF_8); + + @SuppressWarnings("SpellCheckingInspection") + public static byte[] OCE_V2 = Base64.fromUrlSafeString("MIIE7AIBAzCCBKIGCSqGSIb3DQEHAaCCBJME" + + "ggSPMIIEizCCAzoGCSqGSIb3DQEHBqCCAyswggMnAgEAMIIDIAYJKoZIhvcNAQcBMF8GCSqGSIb3DQEFDTB" + + "SMDEGCSqGSIb3DQEFDDAkBBAn11qHSzbFWry2uT9sm1TkAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAw" + + "QBKgQQyd4c6OQQ_iirEEQOSE9XoYCCArDY1YqqfjynV3J09obO-_0WDGjJ-isRyYXbpRwOrw3r8-m7TmN1a" + + "nC7t0Gwb-Do2y3gbX54pko8lXY0rUyT0GTC1fr820Ghi72ovO6rr9JwSv2ndjOe-A4e65aisjMlHPZKJi1y" + + "YWsV9dJzO6OO75nnauX6SCeYCMTWzRKMZj7NHPaGFsl_jyaEqxAteY5tXYudzGmOYLIcSPMj_r0QbfL_Fo8" + + "DtkI6DcXOSVQnu-fHQl8SZAN-6qZ2eQr4TdsS3njn4gscgqsXnKw0FIhv3Js7ab89WHpLrjPHW31RJPSPWc" + + "vTFgMRb4O_2OmEm7aiwj4YnvBdnxtYmeCi5lbthZxW3MFu72DEAZQsnXvflGaV29rafUhKgwWWbiUvX0f4r" + + "XIUrfbQ1udf1I5l7LNC7RMwHYsObSOwoX6dDzj1kGroz1B_MeiIznT0RAd-YvLZ1SQM99MB8_gktWyJUO1e" + + "HjF4faBHgGAGCA0svdryKgy7D7TzvS2TjNOTWaJvRlaxzFmerXTmZytnDEbkU9eVyiiPR37KcpD2_KiLJzn" + + "vWKSo3wImP9-MPnxwNrkEcKf4qHZaE3ky_WLbnC2R8K5odIt7_01A0Nm-f-1NbuxtZBLy6tKnYUf3ir_PPm" + + "pjphS-D3coHn4Tlfa_chGNsMnP-TWDkxCgCyxHDvotpIynHxAibS9GHGHDF6nJekS9cFkGLJ9axJg2Pygic" + + "g78ls-CcDeAllFT46sU1d8Pz9A9MhJYmKc-AWBM00r9P32JC7kAANbOWErgUDwsRMxxHqVDo1U7QTL8LkBI" + + "CmVkpqzF7ObxwU04PB7E0k22sMp1zg95foVst3rZHriJ85CPq7Ht2ZsyLKmJNgqTfkgEJ_kVMuRRRcs4Pod" + + "4aHZL40qy6qrMru-qPTnVD0_2kV59W3xbd_ybMIIBSQYJKoZIhvcNAQcBoIIBOgSCATYwggEyMIIBLgYLKo" + + "ZIhvcNAQwKAQKggfcwgfQwXwYJKoZIhvcNAQUNMFIwMQYJKoZIhvcNAQUMMCQEEI2wPOcHSnElmbOyeO0dj" + + "bACAggAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBDH2f8UV8_hw3QAA7KO6WCYBIGQF6lkbV6ZPhC-" + + "s0WB-25i_Q6uPFieZsmqBsLHoVzXE8US_Kz4YitiVkVPY8JAcJSMI6DcQVkGPJW7suipx2evtne9kJI8TYh" + + "VsX0g7pv3HyTg2roDlHCbedoRHZcoC8lqy078LSdJicJmRLN0aj417oxRbMm972_pCHajk60hhA2A_fDoLA" + + "bEPJoO1qdFcjCjMSUwIwYJKoZIhvcNAQkVMRYEFOgid7IToNeY7_auRfGdSPhz0f-yMEEwMTANBglghkgBZ" + + "QMEAgEFAAQggxlm83v4OwO2nvegRpOK2G4HvCi0iO31XksWx0Pf5DkECAqUVhzeEdSPAgIIAA=="); + public static char[] OCE_V2_PASSWORD = "password".toCharArray(); +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java deleted file mode 100644 index ba06d99f..00000000 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11aDeviceTests.java +++ /dev/null @@ -1,558 +0,0 @@ -/* - * Copyright (C) 2023 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.sd; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeFalse; -import static org.junit.Assume.assumeTrue; - -import com.yubico.yubikit.core.application.BadResponseException; -import com.yubico.yubikit.core.internal.codec.Base64; -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.scp.KeyRef; -import com.yubico.yubikit.core.smartcard.scp.Scp03KeyParams; -import com.yubico.yubikit.core.smartcard.scp.Scp11KeyParams; -import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; -import com.yubico.yubikit.core.smartcard.scp.ScpKid; -import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; -import com.yubico.yubikit.core.smartcard.scp.StaticKeys; -import com.yubico.yubikit.core.util.RandomUtils; -import com.yubico.yubikit.core.util.Tlv; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.security.spec.ECGenParameterSpec; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.List; -import java.util.Map; - -public class Scp11aDeviceTests { - - static final KeyRef DEFAULT_SCP03_KEY = new KeyRef((byte) 0x01, (byte) 0xff); - static final KeyRef AUTH_SCP03_KEY = new KeyRef((byte) 0x01, (byte) 0x01); - static final KeyRef AUTH_SCP11A_KEY = new KeyRef(ScpKid.SCP11a, (byte) 2); - static final KeyRef CA_KLOC_KEY_REF = new KeyRef((byte) 0x10, (byte) 2); - - @SuppressWarnings("SpellCheckingInspection") - static final byte[] OCE_CERTS_V1 = ("-----BEGIN CERTIFICATE-----\n" + - "MIIB8DCCAZegAwIBAgIUf0lxsK1R+EydqZKLLV/vXhaykgowCgYIKoZIzj0EAwIw\n" + - "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + - "NDA1MjgwOTIyMDlaFw0yNDA4MjYwOTIyMDlaMC8xLTArBgNVBAMMJEV4YW1wbGUg\n" + - "T0NFIEludGVybWVkaWF0ZSBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49\n" + - "AwEHA0IABMXbjb+Y33+GP8qUznrdZSJX9b2qC0VUS1WDhuTlQUfg/RBNFXb2/qWt\n" + - "h/a+Ag406fV7wZW2e4PPH+Le7EwS1nyjgZUwgZIwHQYDVR0OBBYEFJzdQCINVBES\n" + - "R4yZBN2l5CXyzlWsMB8GA1UdIwQYMBaAFDGqVWafYGfoHzPc/QT+3nPlcZ89MBIG\n" + - "A1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgIEMCwGA1UdIAEB/wQiMCAw\n" + - "DgYMKoZIhvxrZAAKAgEoMA4GDCqGSIb8a2QACgIBADAKBggqhkjOPQQDAgNHADBE\n" + - "AiBE5SpNEKDW3OehDhvTKT9g1cuuIyPdaXGLZ3iX0x0VcwIgdnIirhlKocOKGXf9\n" + - "ijkE8e+9dTazSPLf24lSIf0IGC8=\n" + - "-----END CERTIFICATE-----\n" + - "-----BEGIN CERTIFICATE-----\n" + - "MIIB2zCCAYGgAwIBAgIUSf59wIpCKOrNGNc5FMPTD9zDGVAwCgYIKoZIzj0EAwIw\n" + - "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + - "NDA1MjgwOTIyMDlaFw0yNDA2MjcwOTIyMDlaMCoxKDAmBgNVBAMMH0V4YW1wbGUg\n" + - "T0NFIFJvb3QgQ0EgQ2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n" + - "AASPrxfpSB/AvuvLKaCz1YTx68Xbtx8S9xAMfRGwzp5cXMdF8c7AWpUfeM3BQ26M\n" + - "h0WPvyBJKhCdeK8iVCaHyr5Jo4GEMIGBMB0GA1UdDgQWBBQxqlVmn2Bn6B8z3P0E\n" + - "/t5z5XGfPTASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBBjA8BgNV\n" + - "HSABAf8EMjAwMA4GDCqGSIb8a2QACgIBFDAOBgwqhkiG/GtkAAoCASgwDgYMKoZI\n" + - "hvxrZAAKAgEAMAoGCCqGSM49BAMCA0gAMEUCIHv8cgOzxq2n1uZktL9gCXSR85mk\n" + - "TieYeSoKZn6MM4rOAiEA1S/+7ez/gxDl01ztKeoHiUiW4FbEG4JUCzIITaGxVvM=\n" + - "-----END CERTIFICATE-----").getBytes(StandardCharsets.UTF_8); - - // PKCS12 certificate with a private key and full certificate chain - @SuppressWarnings("SpellCheckingInspection") - public static byte[] OCE_V1 = Base64.fromUrlSafeString("MIIIfAIBAzCCCDIGCSqGSIb3DQEHAaCCCCME" + - "gggfMIIIGzCCBtIGCSqGSIb3DQEHBqCCBsMwgga_AgEAMIIGuAYJKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTB" + - "KMCkGCSqGSIb3DQEFDDAcBAg8IcJO44iSgAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAllIH" + - "doQx_USA3jmRMeciiAggZQAHCPJ5lzPV0Z5tnssXZZ1AWm8AcKEq28gWUTVqVxc-0EcbKQHig1Jx7rqC3q4" + - "G4sboIRw1vDH6q5O8eGsbkeNuYBim8fZ08JrsjeJABJoEiJrPqplMWA7H6a7athg3YSu1v4OR3UKN5Gyzn3" + - "s0Yx5yMm_xzw204TEK5_1LpK8AMcUliFSq7jw3Xl1RY0zjMSWyQjX0KmB9IdubqQCfhy8zkKluAQADtHsEY" + - "An0F3LoMETQytyUSkIvGMZoFemkCWV7zZ5n5IPhXL7gvnTu0WS8UxEnz_-FYdF43cjmwGfSb3OpaxOND4PB" + - "CpwzbFfVCLa6mUBlwq1KQWRm1-PFm4LnL-3s2mxfjJAsVYP4U722_FHpW8rdTsyvdift9lsQjas2jIjCu8P" + - "FClFZJLQldu5FxOhKzx2gsjYS_aeTdefwjlRiGtEFSrE1snKBbnBeRYFocBjhTD_sy3Vj0i5sbWwTx7iq67" + - "joWydWAMp_lGSZ6akWRsyku_282jlwYsc3pR05qCHkbV0TzJcZofhXBwRgH5NKfulnJ1gH-i3e3RT3TauAK" + - "lqCeAfvDvA3-jxEDy_puPncod7WH0m9P4OmXjZ0s5EI4U-v6bKPgL7LlTCEI6yj15P7kxmruoxZlDAmhixV" + - "mlwJ8ZbVxD6Q-AOhXYPg-il3AYaRAS-VyJla0K-ac6hpYVAnbZCPzgHVkKC6iq4a_azf2b4uq9ks109jjnr" + - "yAChdBsGdmStpZaPW4koMSAIJf12vGRp5jNjSaxaIL5QxTn0WCO8FHi1oqTmlTSWvR8wwZLiBmqQtnNTpew" + - "iLL7C22lerUT7pYvKLCq_nnPYtb5UrSTHrmTNOUzEGVOSAGUWV293S4yiPGIwxT3dPE5_UaU_yKq1RonMRa" + - "PhOZEESZEwLKVCqyDVEbAt7Hdahp-Ex0FVrC5JQhpVQ0Wn6uCptF2Jup70u-P2kVWjxrGBuRrlgEkKuHcoh" + - "WoO9EMX_bLK9KcY4s1ofnfgSNagsAyX7N51Bmahgz1MCFOEcuFa375QYQhqkyLO2ZkNTpFQtjHjX0izZWO5" + - "5LN3rNpcD9-fZt6ldoZCpg-t6y5xqHy-7soH0BpxF1oGIHAUkYSuXpLY0M7Pt3qqvsJ4_ycmFUEyoGv8Ib_" + - "ieUBbebPz0Uhn-jaTpjgtKCyym7nBxVCuUv39vZ31nhNr4WaFsjdB_FOJh1s4KI6kQgzCSObrIVXBcLCTXP" + - "fZ3jWxspKIREHn-zNuW7jIkbugSRiNFfVArcc7cmU4av9JPSmFiZzeyA0gkrkESTg8DVPT16u7W5HREX4Cw" + - "mKu-12R6iYQ_po9Hcy6NJ8ShLdAzU0-q_BzgH7Cb8qimjgfGBA3Mesc-P98FlCzAjB2EgucRuXuehM_Femm" + - "ZyNl0qI1Mj9qOgx_HeYaJaYD-yXwojApmetFGtDtMJsDxwL0zK7eGXeHHa7pd7OybKdSjDq25CCTOZvfR0D" + - "D55FDIGCy0FsJTcferzPFlkz_Q45vEwuGfEBnXXS9IhH4ySvJmDmyfLMGiHW6t-9gjyEEg-dwSOq9yXYScf" + - "CsefRl7-o_9nDoNQ8s_XS7LKlJ72ZEBaKeAxcm6q4wVwUWITNNl1R3EYAsFBWzYt4Ka9Ob3igVaNfeG9K4p" + - "fQqMWcPpqVp4FuIsEpDWZYuv71s-WMYCs1JMfHbHDUczdRet1Ir2vLDGeWwvci70AzeKvvQ9OwBVESRec6c" + - "Vrgt3EJWLey5sXY01WpMm526fwtLolSMpCf-dNePT97nXemQCcr3QXimagHTSGPngG3577FPrSQJl-lCJDY" + - "xBFFtnd6hq4OcVr5HiNAbLnSjBWbzqxhHMmgoojy4rwtHmrfyVYKXyl-98r-Lobitv2tpnBqmjL6dMPRBOJ" + - "vQl8-Wp4MGBsi1gvTgW_-pLlMXT--1iYyxBeK9_AN5hfjtrivewE3JY531jwkrl3rUl50MKwBJMMAtQQIYr" + - "Dg7DAg_-QcOi-2mgo9zJPzR2jIXF0wP-9FA4-MITa2v78QVXcesh63agcFJCayGAL1StnbSBvvDqK5vEei3" + - "uGZbeJEpU1hikQx57w3UzS9O7OSQMFvRBOrFBQsYC4JzfF0soIweGNpJxpm-UNYz-hB9vCb8-3OHA069M0C" + - "AlJVOTF9uEpLVRzK-1kwggFBBgkqhkiG9w0BBwGgggEyBIIBLjCCASowggEmBgsqhkiG9w0BDAoBAqCB7zC" + - "B7DBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIexxrwNlHM34CAggAMAwGCCqGSIb3DQIJBQAwHQ" + - "YJYIZIAWUDBAEqBBAkK96h6gHJglyJl1_yEylvBIGQh62z7u5RoQ9y5wIXbE3_oMQTKVfCSrtqGUmj38sxD" + - "Y7yIoTVQq7sw0MPNeYHROgGUAzawU0DlXMGuOWrbgzYeURZs0_HZ2Cqk8qhVnD8TgpB2n0U0NB7aJRHlkzT" + - "l5MLFAwn3NE49CSzb891lGwfLYXYCfNfqltD7xZ7uvz6JAo_y6UtY8892wrRv4UdejyfMSUwIwYJKoZIhvc" + - "NAQkVMRYEFJBU0s1_6SLbIRbyeq65gLWqClWNMEEwMTANBglghkgBZQMEAgEFAAQgqkOJRTcBlnx5yn57k2" + - "3PH-qUXUGPEuYkrGy-DzEQiikECB0BXjHOZZhuAgIIAA=="); - public static char[] OCE_V1_PASSWORD = "password".toCharArray(); - - @SuppressWarnings("SpellCheckingInspection") - static final byte[] OCE_CERTS_V2 = ("-----BEGIN CERTIFICATE-----\n" + - "MIICRTCCAeugAwIBAgICEAAwCgYIKoZIzj0EAwIwaDELMAkGA1UEBhMCU1YxCzAJ\n" + - "BgNVBAgMAlNWMRIwEAYDVQQHDAlTdG9ja2hvbG0xEDAOBgNVBAoMB1Rlc3RPcmcx\n" + - "FDASBgNVBAsMC1Rlc3RPcmdVbml0MRAwDgYDVQQDDAdDQS1LTE9DMCAXDTI0MDgw\n" + - "MjA3NTI0MVoYDzIxMjQwNzA5MDc1MjQxWjBTMQswCQYDVQQGEwJTVjELMAkGA1UE\n" + - "CAwCU1YxEDAOBgNVBAoMB1Rlc3RPcmcxFDASBgNVBAsMC1Rlc3RPcmdVbml0MQ8w\n" + - "DQYDVQQDDAZDQS1PQ0UwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARQ8mUZsuiR\n" + - "njsqLh8WMPz2CB6rhF7FZhLNqBQCEBT9yP2TPSqtSizYj0IBnYVFpNEo+e70dU7F\n" + - "3VmjkbuEXMTAo4GXMIGUMB8GA1UdIwQYMBaAFHMPyjll59Oc2tkOYx70pZ0u6JcJ\n" + - "MAsGA1UdDwQEAwIDCDAZBgNVHSAEEjAQMA4GDCqGSIb8a2QACgIBADAUBgwqhkiG\n" + - "/GtkAAoCAAEEBAQCvyAwFAYMKoZIhvxrZAAKAgACBAQEAl8gMB0GA1UdDgQWBBTv\n" + - "xZlvtJr2cz2ZOX+fjOyJKKIUzDAKBggqhkjOPQQDAgNIADBFAiBlnbco6Ciwf+3Y\n" + - "EyK8ZU07zeQPp47+XUEfgPF1qL9TgwIhANSqEpSjoMJTpFO8gzOey+Q83mXOiCRp\n" + - "c6go7DG5ObQh\n" + - "-----END CERTIFICATE-----\n" - + "-----BEGIN CERTIFICATE-----\n" + - "MIICLjCCAdSgAwIBAgIUOhaGJ8+QFxtHCXhvLRGhJY/wq7wwCgYIKoZIzj0EAwIw\n" + - "aDELMAkGA1UEBhMCU1YxCzAJBgNVBAgMAlNWMRIwEAYDVQQHDAlTdG9ja2hvbG0x\n" + - "EDAOBgNVBAoMB1Rlc3RPcmcxFDASBgNVBAsMC1Rlc3RPcmdVbml0MRAwDgYDVQQD\n" + - "DAdDQS1LTE9DMCAXDTI0MDgwMjA3NTE1MFoYDzIyMjQwNjE1MDc1MTUwWjBoMQsw\n" + - "CQYDVQQGEwJTVjELMAkGA1UECAwCU1YxEjAQBgNVBAcMCVN0b2NraG9sbTEQMA4G\n" + - "A1UECgwHVGVzdE9yZzEUMBIGA1UECwwLVGVzdE9yZ1VuaXQxEDAOBgNVBAMMB0NB\n" + - "LUtMT0MwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATRC0DYFzG0Lm0TwekgXYPz\n" + - "2ScKOARoft2t/E4WUyFiX0Snsy6S2y+hYP+bscnjUEKZplxE91MlsBNVhG09ZMx3\n" + - "o1owWDAdBgNVHQ4EFgQUcw/KOWXn05za2Q5jHvSlnS7olwkwCwYDVR0PBAQDAgEG\n" + - "MA8GA1UdEwEB/wQFMAMBAf8wGQYDVR0gBBIwEDAOBgwqhkiG/GtkAAoCARQwCgYI\n" + - "KoZIzj0EAwIDSAAwRQIhAPhg4A/O3RNNiBniSAzenpE1ftkaKSk21oMd95XKwdqb\n" + - "AiBzSpZzDimTeD/24luQ27oAOAmPI808aX8YMctqlo0LEg==\n" + - "-----END CERTIFICATE-----\n").getBytes(StandardCharsets.UTF_8); - - @SuppressWarnings("SpellCheckingInspection") - public static byte[] OCE_V2 = Base64.fromUrlSafeString("MIIE7AIBAzCCBKIGCSqGSIb3DQEHAaCCBJME" + - "ggSPMIIEizCCAzoGCSqGSIb3DQEHBqCCAyswggMnAgEAMIIDIAYJKoZIhvcNAQcBMF8GCSqGSIb3DQEFDTB" + - "SMDEGCSqGSIb3DQEFDDAkBBAn11qHSzbFWry2uT9sm1TkAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAw" + - "QBKgQQyd4c6OQQ_iirEEQOSE9XoYCCArDY1YqqfjynV3J09obO-_0WDGjJ-isRyYXbpRwOrw3r8-m7TmN1a" + - "nC7t0Gwb-Do2y3gbX54pko8lXY0rUyT0GTC1fr820Ghi72ovO6rr9JwSv2ndjOe-A4e65aisjMlHPZKJi1y" + - "YWsV9dJzO6OO75nnauX6SCeYCMTWzRKMZj7NHPaGFsl_jyaEqxAteY5tXYudzGmOYLIcSPMj_r0QbfL_Fo8" + - "DtkI6DcXOSVQnu-fHQl8SZAN-6qZ2eQr4TdsS3njn4gscgqsXnKw0FIhv3Js7ab89WHpLrjPHW31RJPSPWc" + - "vTFgMRb4O_2OmEm7aiwj4YnvBdnxtYmeCi5lbthZxW3MFu72DEAZQsnXvflGaV29rafUhKgwWWbiUvX0f4r" + - "XIUrfbQ1udf1I5l7LNC7RMwHYsObSOwoX6dDzj1kGroz1B_MeiIznT0RAd-YvLZ1SQM99MB8_gktWyJUO1e" + - "HjF4faBHgGAGCA0svdryKgy7D7TzvS2TjNOTWaJvRlaxzFmerXTmZytnDEbkU9eVyiiPR37KcpD2_KiLJzn" + - "vWKSo3wImP9-MPnxwNrkEcKf4qHZaE3ky_WLbnC2R8K5odIt7_01A0Nm-f-1NbuxtZBLy6tKnYUf3ir_PPm" + - "pjphS-D3coHn4Tlfa_chGNsMnP-TWDkxCgCyxHDvotpIynHxAibS9GHGHDF6nJekS9cFkGLJ9axJg2Pygic" + - "g78ls-CcDeAllFT46sU1d8Pz9A9MhJYmKc-AWBM00r9P32JC7kAANbOWErgUDwsRMxxHqVDo1U7QTL8LkBI" + - "CmVkpqzF7ObxwU04PB7E0k22sMp1zg95foVst3rZHriJ85CPq7Ht2ZsyLKmJNgqTfkgEJ_kVMuRRRcs4Pod" + - "4aHZL40qy6qrMru-qPTnVD0_2kV59W3xbd_ybMIIBSQYJKoZIhvcNAQcBoIIBOgSCATYwggEyMIIBLgYLKo" + - "ZIhvcNAQwKAQKggfcwgfQwXwYJKoZIhvcNAQUNMFIwMQYJKoZIhvcNAQUMMCQEEI2wPOcHSnElmbOyeO0dj" + - "bACAggAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBDH2f8UV8_hw3QAA7KO6WCYBIGQF6lkbV6ZPhC-" + - "s0WB-25i_Q6uPFieZsmqBsLHoVzXE8US_Kz4YitiVkVPY8JAcJSMI6DcQVkGPJW7suipx2evtne9kJI8TYh" + - "VsX0g7pv3HyTg2roDlHCbedoRHZcoC8lqy078LSdJicJmRLN0aj417oxRbMm972_pCHajk60hhA2A_fDoLA" + - "bEPJoO1qdFcjCjMSUwIwYJKoZIhvcNAQkVMRYEFOgid7IToNeY7_auRfGdSPhz0f-yMEEwMTANBglghkgBZ" + - "QMEAgEFAAQggxlm83v4OwO2nvegRpOK2G4HvCi0iO31XksWx0Pf5DkECAqUVhzeEdSPAgIIAA=="); - public static char[] OCE_V2_PASSWORD = "password".toCharArray(); - - public static void before(SecurityDomainTestState state) throws Throwable { - state.withSecurityDomain(SecurityDomainSession::reset); - } - - public static void testImportKey(SecurityDomainTestState state) throws Throwable { - testImportKey(state, OCE_CERTS_V1, OCE_V1, OCE_V1_PASSWORD); - } - - public static void testImportKeyAlt(SecurityDomainTestState state) throws Throwable { - testImportKey(state, OCE_CERTS_V2, OCE_V2, OCE_V2_PASSWORD); - } - - private static void testImportKey( - SecurityDomainTestState state, - byte[] oceCerts, - byte[] oce, - char[] password) throws Throwable { - - assumeTrue("Device does not support SCP11a", state.getDeviceInfo().getVersion() - .isAtLeast(5, 7, 2)); - - state.withSecurityDomain(SecurityDomainSession::reset); - - // replace default SCP03 keys so that we can authenticate later - ScpKeyParams scp03KeyParams = replaceDefaultScp03Key(state); - - PublicKeyValues pk = state.withSecurityDomain(scp03KeyParams, sd -> { - return setupScp11a(sd, oceCerts); - }); - - // direct auth - state.withSecurityDomain( - getScp11aKeyParams(oce, password, pk.toPublicKey()), - sd -> { - Map> keyInformation = sd.getKeyInformation(); - assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); - }); - - // read public key and auth - state.withSecurityDomain(sd -> { - List certs = sd.getCertificateBundle(AUTH_SCP11A_KEY); - PublicKey publicKey = certs.get(certs.size() - 1).getPublicKey(); - ScpKeyParams params = getScp11aKeyParams(oce, password, publicKey); - sd.authenticate(params); - // use authenticated session to make a request - Map> keyInformation = sd.getKeyInformation(); - assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); - }); - - // read public key and then auth - PublicKey publicKey = state.withSecurityDomain(sd -> { - List certs = sd.getCertificateBundle(AUTH_SCP11A_KEY); - return certs.get(certs.size() - 1).getPublicKey(); - }); - - state.withSecurityDomain( - getScp11aKeyParams(oce, password, publicKey), - sd -> { - // use authenticated session to make a request - Map> keyInformation = sd.getKeyInformation(); - assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); - }); - } - - private static final ScpKeyParams defaultScp03KeyParams = - new Scp03KeyParams(DEFAULT_SCP03_KEY, StaticKeys.getDefaultKeys()); - - public static void testAuthenticate(SecurityDomainTestState state) throws Throwable { - final byte kvn = 0x03; - - ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, sd -> { - return loadKeys(sd, ScpKid.SCP11a, kvn); - }); - - state.withSecurityDomain(keyParams, sd -> { - sd.deleteKey(new KeyRef(ScpKid.SCP11a, kvn), false); - }); - } - - public static void testAllowList(SecurityDomainTestState state) throws Throwable { - final byte kvn = 0x03; - - ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, sd -> { - Scp11KeyParams params = loadKeys(sd, ScpKid.SCP11a, kvn); - assertNotNull(params.getOceKeyRef()); - - List serials = new ArrayList<>(); - for (X509Certificate cert : params.getCertificates()) { - serials.add(cert.getSerialNumber()); - } - - sd.storeAllowlist(params.getOceKeyRef(), serials); - return params; - }); - - state.withSecurityDomain(keyParams, sd -> { - sd.deleteKey(new KeyRef(ScpKid.SCP11a, kvn), false); - }); - } - - public static void testAllowListBlocked(SecurityDomainTestState state) throws Throwable { - final byte kvn = 0x03; - final KeyRef ref = new KeyRef(ScpKid.SCP03, (byte) 2); - - ScpKeyParams scp03KeyParams = replaceDefaultScp03Key(state); - - Scp11KeyParams keyParams = state.withSecurityDomain(scp03KeyParams, sd -> { - - sd.deleteKey(new KeyRef(ScpKid.SCP11b, (byte) 1), false); - - Scp11KeyParams scp11KeyParams = loadKeys(sd, ScpKid.SCP11a, kvn); - assertNotNull(scp11KeyParams.getOceKeyRef()); - - final List serials = Arrays.asList( - BigInteger.valueOf(1), BigInteger.valueOf(2), BigInteger.valueOf(3), - BigInteger.valueOf(4), BigInteger.valueOf(5)); - - sd.storeAllowlist(scp11KeyParams.getOceKeyRef(), serials); - - return scp11KeyParams; - }); - - // authenticate with scp11a will throw - state.withSecurityDomain(sd -> { - assertThrows(ApduException.class, () -> sd.authenticate(keyParams)); - }); - - // reset the allow list - state.withSecurityDomain(scp03KeyParams, sd -> { - assertNotNull(keyParams.getOceKeyRef()); - sd.storeAllowlist(keyParams.getOceKeyRef(), new ArrayList<>()); - }); - - // authenticate with scp11a will not throw - state.withSecurityDomain(sd -> { - sd.authenticate(keyParams); - }); - } - - public static void testScp11cAuthenticate(SecurityDomainTestState state) throws Throwable { - final byte kvn = 0x03; - - ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, sd -> { - return loadKeys(sd, ScpKid.SCP11c, kvn); - }); - - state.withSecurityDomain(keyParams, sd -> { - assertThrows(ApduException.class, () -> - sd.deleteKey(new KeyRef(ScpKid.SCP11c, kvn), false)); - }); - } - - public static void testScp11bAuthenticate(SecurityDomainTestState state) throws Throwable { - final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); - - List chain = state.withSecurityDomain(defaultScp03KeyParams, sd -> { - return sd.getCertificateBundle(ref); - }); - - X509Certificate leaf = chain.get(chain.size() - 1); - Scp11KeyParams params = new Scp11KeyParams(ref, leaf.getPublicKey()); - - state.withSecurityDomain(params, sd -> { - assertThrows(ApduException.class, () -> verifyScp11bAuth(sd)); - }); - } - - public static void testScp11bWrongPubKey(SecurityDomainTestState state) throws Throwable { - final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); - - List chain = state.withSecurityDomain(defaultScp03KeyParams, sd -> { - return sd.getCertificateBundle(ref); - }); - - X509Certificate cert = chain.get(0); - Scp11KeyParams params = new Scp11KeyParams(ref, cert.getPublicKey()); - - state.withSecurityDomain(sd -> { - BadResponseException e = - assertThrows(BadResponseException.class, () -> sd.authenticate(params)); - assertEquals("Receipt does not match", e.getMessage()); - }); - } - - public static void testScp11bImport(SecurityDomainTestState state) throws Throwable { - final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x2); - - ScpKeyParams keyParams = state.withSecurityDomain(sd -> { - sd.authenticate(defaultScp03KeyParams); - KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); - ECGenParameterSpec ecParams = new ECGenParameterSpec("secp256r1"); - kpg.initialize(ecParams); - KeyPair keyPair = kpg.generateKeyPair(); - sd.putKey(ref, PrivateKeyValues.fromPrivateKey(keyPair.getPrivate()), 0); - return new Scp11KeyParams(ref, keyPair.getPublic()); - }); - - state.withSecurityDomain(sd -> { - sd.authenticate(keyParams); - }); - } - - private static void verifyScp11bAuth(SecurityDomainSession session) - throws BadResponseException, ApduException, IOException { - KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x7f); - session.generateEcKey(ref, 0); - session.deleteKey(ref, false); - } - - private static ScpKeyParams replaceDefaultScp03Key(SecurityDomainTestState state) throws Throwable { - final StaticKeys staticKeys = new StaticKeys( - RandomUtils.getRandomBytes(16), - RandomUtils.getRandomBytes(16), - RandomUtils.getRandomBytes(16) - ); - - assumeFalse("SCP03 not supported over NFC on FIPS capable devices", - state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); - - state.withSecurityDomain(sd -> { - sd.authenticate(defaultScp03KeyParams); - sd.putKey(AUTH_SCP03_KEY, staticKeys, 0); - }); - - return new Scp03KeyParams(AUTH_SCP03_KEY, staticKeys); - } - - private static Scp11KeyParams getScp11aKeyParams(byte[] pkcs12, char[] password, PublicKey pk) - throws Throwable { - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - - try (InputStream is = new ByteArrayInputStream(pkcs12)) { - keyStore.load(is, password); - - final Enumeration aliases = keyStore.aliases(); - assertTrue(aliases.hasMoreElements()); - String alias = keyStore.aliases().nextElement(); - assertTrue(keyStore.isKeyEntry(alias)); - - Key sk = keyStore.getKey(keyStore.aliases().nextElement(), password); - assertTrue("No private key in pkcs12", sk instanceof PrivateKey); - - ScpCertificates certs = ScpCertificates.from(getCertificateChain(keyStore, alias)); - - List certChain = new ArrayList<>(certs.bundle); - if (certs.leaf != null) { - certChain.add(certs.leaf); - } - - return new Scp11KeyParams( - AUTH_SCP11A_KEY, - pk, - CA_KLOC_KEY_REF, - (PrivateKey) sk, - certChain - ); - } - } - - @SuppressWarnings("unchecked") - private static ScpCertificates getOceCertificates(byte[] pem) - throws CertificateException, IOException { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - try (InputStream is = new ByteArrayInputStream(pem)) { - return ScpCertificates.from((List) certificateFactory.generateCertificates(is)); - } - } - - private static byte[] getSki(X509Certificate certificate) { - byte[] skiExtensionValue = certificate.getExtensionValue("2.5.29.14"); - if (skiExtensionValue == null) { - return null; - } - assertNotNull("Missing Subject Key Identifier", skiExtensionValue); - Tlv tlv = Tlv.parse(skiExtensionValue); - assertEquals("Invalid extension value", 0x04, tlv.getTag()); - Tlv digest = Tlv.parse(tlv.getValue()); - assertEquals("Invalid Subject Key Identifier", 0x04, digest.getTag()); - return digest.getValue(); - } - - private static List getCertificateChain(KeyStore keyStore, String alias) - throws KeyStoreException { - Certificate[] chain = keyStore.getCertificateChain(alias); - final List certificateChain = new ArrayList<>(); - for (Certificate cert : chain) { - if (cert instanceof X509Certificate) { - certificateChain.add((X509Certificate) cert); - } - } - return certificateChain; - } - - private static PublicKeyValues setupScp11a(SecurityDomainSession sd, byte[] pem) - throws Throwable { - // generate new SCP11a key - PublicKeyValues generatedPk = sd.generateEcKey(AUTH_SCP11A_KEY, 0); - - // delete default SCP11b key - sd.deleteKey(new KeyRef(ScpKid.SCP11b, (byte) 1), false); - - // import OCE CA-KLOC certificate - ScpCertificates certs = getOceCertificates(pem); - - if (certs.ca == null) { - fail("Input does not contain valid CA-KLOC certificate"); - } - - sd.putKey(CA_KLOC_KEY_REF, PublicKeyValues.fromPublicKey(certs.ca.getPublicKey()), 0); - - byte[] ski = getSki(certs.ca); - assertNotNull("CA certificate missing Subject Key Identifier", ski); - sd.storeCaIssuer(CA_KLOC_KEY_REF, ski); - - // delete our SCP03 keys - sd.deleteKey(AUTH_SCP03_KEY, false); - - return generatedPk; - } - - private static Scp11KeyParams loadKeys(SecurityDomainSession sd, byte kid, byte kvn) - throws Throwable { - KeyRef sdRef = new KeyRef(kid, kvn); - KeyRef oceRef = new KeyRef((byte) 0x10, kvn); - - PublicKeyValues publicKeyValues = sd.generateEcKey(sdRef, 0); - - ScpCertificates oceCerts = getOceCertificates(OCE_CERTS_V1); - assertNotNull("Missing CA", oceCerts.ca); - sd.putKey(oceRef, PublicKeyValues.fromPublicKey(oceCerts.ca.getPublicKey()), 0); - - byte[] ski = getSki(oceCerts.ca); - assertNotNull("CA certificate missing Subject Key Identifier", ski); - sd.storeCaIssuer(oceRef, ski); - - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - - try (InputStream is = new ByteArrayInputStream(OCE_V1)) { - keyStore.load(is, OCE_V1_PASSWORD); - - final Enumeration aliases = keyStore.aliases(); - assertTrue(aliases.hasMoreElements()); - String alias = keyStore.aliases().nextElement(); - assertTrue(keyStore.isKeyEntry(alias)); - - Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_V1_PASSWORD); - assertTrue("No private key in pkcs12", sk instanceof PrivateKey); - - ScpCertificates certs = ScpCertificates.from(getCertificateChain(keyStore, alias)); - - List certChain = new ArrayList<>(certs.bundle); - if (certs.leaf != null) { - certChain.add(certs.leaf); - } - - return new Scp11KeyParams( - sdRef, - publicKeyValues.toPublicKey(), - oceRef, - (PrivateKey) sk, - certChain - ); - } - } -} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java index 132e558a..4e277807 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java @@ -18,6 +18,7 @@ import static org.junit.Assert.fail; +import java.math.BigInteger; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; @@ -42,6 +43,7 @@ static ScpCertificates from(@Nullable List certificates) { } X509Certificate ca = null; + BigInteger seenSerial = null; // order certificates with the Root CA on top List ordered = new ArrayList<>(); @@ -70,7 +72,14 @@ static ScpCertificates from(@Nullable List certificates) { continue; } - fail("Cannot decide the order of " + cert + " in " + ordered); + if (seenSerial != null && cert.getSerialNumber().equals(seenSerial)) { + fail("Cannot decide the order of " + cert + " in " + ordered); + } + + // this cert could not be ordered, try to process rest of certificates + // but if you see this cert again fail because the cert chain is not complete + certificates.add(cert); + seenSerial = cert.getSerialNumber(); } // find ca and leaf From 41efd9b139141b6d70536eed70c5c6a6ff5401a5 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 6 Aug 2024 16:33:53 +0200 Subject: [PATCH 40/46] refactor and update style --- .../yubico/yubikit/testing/sd/Scp11Tests.java | 6 - .../yubikit/testing/sd/Scp11DeviceTests.java | 173 +++--------------- .../yubikit/testing/sd/Scp11TestData.java | 131 ++++--------- 3 files changed, 64 insertions(+), 246 deletions(-) diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11Tests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11Tests.java index 5d833fef..dc368681 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11Tests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/sd/Scp11Tests.java @@ -28,12 +28,6 @@ public void before() throws Throwable { withState(Scp11DeviceTests::before); } - @Test - public void testScp11aImportKey() throws Throwable { - withState(Scp11DeviceTests::testScp11aImportKey); - withState(Scp11DeviceTests::testScp11aImportKeyAlt); - } - @Test public void testScp11aAuthenticate() throws Throwable { withState(Scp11DeviceTests::testScp11aAuthenticate); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java index 9d8b92c5..ffab766c 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java @@ -16,17 +16,13 @@ package com.yubico.yubikit.testing.sd; -import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_CERTS_V1; -import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_CERTS_V2; -import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_V1; -import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_V1_PASSWORD; -import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_V2; -import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_V2_PASSWORD; +import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE; +import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_CERTS; +import static com.yubico.yubikit.testing.sd.Scp11TestData.OCE_PASSWORD; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; @@ -54,7 +50,6 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.PrivateKey; -import java.security.PublicKey; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -64,83 +59,23 @@ import java.util.Arrays; import java.util.Enumeration; import java.util.List; -import java.util.Map; public class Scp11DeviceTests { - - static final KeyRef defaultRef = new KeyRef((byte) 0x01, (byte) 0xff); - static final KeyRef AUTH_SCP03_KEY = new KeyRef((byte) 0x01, (byte) 0x01); - static final KeyRef AUTH_SCP11A_KEY = new KeyRef(ScpKid.SCP11a, (byte) 2); - static final KeyRef CA_KLOC_KEY_REF = new KeyRef((byte) 0x10, (byte) 2); + private static final ScpKeyParams defaultKeyParams = + new Scp03KeyParams(new KeyRef((byte) 0x01, (byte) 0xff), StaticKeys.getDefaultKeys()); public static void before(SecurityDomainTestState state) throws Throwable { assumeTrue("Device does not support SCP11a", state.getDeviceInfo().getVersion().isAtLeast(5, 7, 2)); + assumeFalse("SCP03 authentication not supported over NFC on FIPS capable devices", + state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); state.withSecurityDomain(SecurityDomainSession::reset); } - public static void testScp11aImportKey(SecurityDomainTestState state) throws Throwable { - testScp11aImportKey(state, OCE_CERTS_V1, OCE_V1, OCE_V1_PASSWORD); - } - - public static void testScp11aImportKeyAlt(SecurityDomainTestState state) throws Throwable { - testScp11aImportKey(state, OCE_CERTS_V2, OCE_V2, OCE_V2_PASSWORD); - } - - private static void testScp11aImportKey( - SecurityDomainTestState state, - byte[] oceCerts, - byte[] oce, - char[] password) throws Throwable { - - state.withSecurityDomain(SecurityDomainSession::reset); - - // replace default SCP03 keys so that we can authenticate later - ScpKeyParams scp03KeyParams = replaceDefaultScp03Key(state); - - PublicKeyValues pk = state.withSecurityDomain(scp03KeyParams, session -> { - return setupScp11a(session, oceCerts); - }); - - // direct auth - state.withSecurityDomain( - getScp11aKeyParams(oce, password, pk.toPublicKey()), - session -> { - Map> keyInformation = session.getKeyInformation(); - assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); - }); - - // read public key and auth - state.withSecurityDomain(session -> { - List certs = session.getCertificateBundle(AUTH_SCP11A_KEY); - PublicKey publicKey = certs.get(certs.size() - 1).getPublicKey(); - ScpKeyParams params = getScp11aKeyParams(oce, password, publicKey); - session.authenticate(params); - Map> keyInformation = session.getKeyInformation(); - assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); - }); - - // read public key and then auth - PublicKey publicKey = state.withSecurityDomain(session -> { - List certs = session.getCertificateBundle(AUTH_SCP11A_KEY); - return certs.get(certs.size() - 1).getPublicKey(); - }); - - state.withSecurityDomain( - getScp11aKeyParams(oce, password, publicKey), - session -> { - Map> keyInformation = session.getKeyInformation(); - assertNotNull(keyInformation.get(AUTH_SCP11A_KEY)); - }); - } - - private static final ScpKeyParams defaultScp03KeyParams = - new Scp03KeyParams(defaultRef, StaticKeys.getDefaultKeys()); - public static void testScp11aAuthenticate(SecurityDomainTestState state) throws Throwable { final byte kvn = 0x03; - ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, session -> { + ScpKeyParams keyParams = state.withSecurityDomain(defaultKeyParams, session -> { return loadKeys(session, ScpKid.SCP11a, kvn); }); @@ -152,7 +87,7 @@ public static void testScp11aAuthenticate(SecurityDomainTestState state) throws public static void testScp11aAllowList(SecurityDomainTestState state) throws Throwable { final byte kvn = 0x05; - ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, session -> { + ScpKeyParams keyParams = state.withSecurityDomain(defaultKeyParams, session -> { Scp11KeyParams params = loadKeys(session, ScpKid.SCP11a, kvn); assertNotNull(params.getOceKeyRef()); @@ -173,9 +108,10 @@ public static void testScp11aAllowList(SecurityDomainTestState state) throws Thr public static void testScp11aAllowListBlocked(SecurityDomainTestState state) throws Throwable { final byte kvn = 0x03; - ScpKeyParams scp03KeyParams = replaceDefaultScp03Key(state); + ScpKeyParams scp03KeyParams = importScp03Key(state); Scp11KeyParams keyParams = state.withSecurityDomain(scp03KeyParams, session -> { + // make space for new key session.deleteKey(new KeyRef(ScpKid.SCP11b, (byte) 1), false); Scp11KeyParams scp11KeyParams = loadKeys(session, ScpKid.SCP11a, kvn); @@ -210,7 +146,7 @@ public static void testScp11aAllowListBlocked(SecurityDomainTestState state) thr public static void testScp11bAuthenticate(SecurityDomainTestState state) throws Throwable { final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); - List chain = state.withSecurityDomain(defaultScp03KeyParams, session -> { + List chain = state.withSecurityDomain(defaultKeyParams, session -> { return session.getCertificateBundle(ref); }); @@ -225,7 +161,7 @@ public static void testScp11bAuthenticate(SecurityDomainTestState state) throws public static void testScp11bWrongPubKey(SecurityDomainTestState state) throws Throwable { final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); - List chain = state.withSecurityDomain(defaultScp03KeyParams, session -> { + List chain = state.withSecurityDomain(defaultKeyParams, session -> { return session.getCertificateBundle(ref); }); @@ -243,7 +179,7 @@ public static void testScp11bImport(SecurityDomainTestState state) throws Throwa final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x2); ScpKeyParams keyParams = state.withSecurityDomain(session -> { - session.authenticate(defaultScp03KeyParams); + session.authenticate(defaultKeyParams); KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); ECGenParameterSpec ecParams = new ECGenParameterSpec("secp256r1"); kpg.initialize(ecParams); @@ -267,7 +203,7 @@ private static void verifyScp11bAuth(SecurityDomainSession session) public static void testScp11cAuthenticate(SecurityDomainTestState state) throws Throwable { final byte kvn = 0x03; - ScpKeyParams keyParams = state.withSecurityDomain(defaultScp03KeyParams, session -> { + ScpKeyParams keyParams = state.withSecurityDomain(defaultKeyParams, session -> { return loadKeys(session, ScpKid.SCP11c, kvn); }); @@ -277,10 +213,12 @@ public static void testScp11cAuthenticate(SecurityDomainTestState state) throws }); } - private static ScpKeyParams replaceDefaultScp03Key(SecurityDomainTestState state) throws Throwable { + private static ScpKeyParams importScp03Key(SecurityDomainTestState state) throws Throwable { assumeFalse("SCP03 management not supported over NFC on FIPS capable devices", state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + final KeyRef scp03Ref = new KeyRef((byte) 0x01, (byte) 0x01); + final StaticKeys staticKeys = new StaticKeys( RandomUtils.getRandomBytes(16), RandomUtils.getRandomBytes(16), @@ -288,46 +226,14 @@ private static ScpKeyParams replaceDefaultScp03Key(SecurityDomainTestState state ); state.withSecurityDomain(session -> { - session.authenticate(defaultScp03KeyParams); - session.putKey(AUTH_SCP03_KEY, staticKeys, 0); + session.authenticate(defaultKeyParams); + session.putKey(scp03Ref, staticKeys, 0); }); - return new Scp03KeyParams(AUTH_SCP03_KEY, staticKeys); + return new Scp03KeyParams(scp03Ref, staticKeys); } - private static Scp11KeyParams getScp11aKeyParams(byte[] pkcs12, char[] password, PublicKey pk) - throws Throwable { - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - - try (InputStream is = new ByteArrayInputStream(pkcs12)) { - keyStore.load(is, password); - - final Enumeration aliases = keyStore.aliases(); - assertTrue(aliases.hasMoreElements()); - String alias = keyStore.aliases().nextElement(); - assertTrue(keyStore.isKeyEntry(alias)); - - Key sk = keyStore.getKey(keyStore.aliases().nextElement(), password); - assertTrue("No private key in pkcs12", sk instanceof PrivateKey); - - ScpCertificates certs = ScpCertificates.from(getCertificateChain(keyStore, alias)); - - List certChain = new ArrayList<>(certs.bundle); - if (certs.leaf != null) { - certChain.add(certs.leaf); - } - - return new Scp11KeyParams( - AUTH_SCP11A_KEY, - pk, - CA_KLOC_KEY_REF, - (PrivateKey) sk, - certChain - ); - } - } - - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "SameParameterValue"}) private static ScpCertificates getOceCertificates(byte[] pem) throws CertificateException, IOException { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); @@ -361,33 +267,6 @@ private static List getCertificateChain(KeyStore keyStore, Stri return certificateChain; } - private static PublicKeyValues setupScp11a(SecurityDomainSession session, byte[] pem) - throws Throwable { - // generate new SCP11a key - PublicKeyValues generatedPk = session.generateEcKey(AUTH_SCP11A_KEY, 0); - - // delete default SCP11b key - session.deleteKey(new KeyRef(ScpKid.SCP11b, (byte) 1), false); - - // import OCE CA-KLOC certificate - ScpCertificates certs = getOceCertificates(pem); - - if (certs.ca == null) { - fail("Input does not contain valid CA-KLOC certificate"); - } - - session.putKey(CA_KLOC_KEY_REF, PublicKeyValues.fromPublicKey(certs.ca.getPublicKey()), 0); - - byte[] ski = getSki(certs.ca); - assertNotNull("CA certificate missing Subject Key Identifier", ski); - session.storeCaIssuer(CA_KLOC_KEY_REF, ski); - - // delete our SCP03 keys - session.deleteKey(AUTH_SCP03_KEY, false); - - return generatedPk; - } - private static Scp11KeyParams loadKeys(SecurityDomainSession session, byte kid, byte kvn) throws Throwable { KeyRef sessionRef = new KeyRef(kid, kvn); @@ -395,7 +274,7 @@ private static Scp11KeyParams loadKeys(SecurityDomainSession session, byte kid, PublicKeyValues publicKeyValues = session.generateEcKey(sessionRef, 0); - ScpCertificates oceCerts = getOceCertificates(OCE_CERTS_V1); + ScpCertificates oceCerts = getOceCertificates(OCE_CERTS); assertNotNull("Missing CA", oceCerts.ca); session.putKey(oceRef, PublicKeyValues.fromPublicKey(oceCerts.ca.getPublicKey()), 0); @@ -405,15 +284,15 @@ private static Scp11KeyParams loadKeys(SecurityDomainSession session, byte kid, KeyStore keyStore = KeyStore.getInstance("PKCS12"); - try (InputStream is = new ByteArrayInputStream(OCE_V1)) { - keyStore.load(is, OCE_V1_PASSWORD); + try (InputStream is = new ByteArrayInputStream(OCE)) { + keyStore.load(is, OCE_PASSWORD); final Enumeration aliases = keyStore.aliases(); assertTrue(aliases.hasMoreElements()); String alias = keyStore.aliases().nextElement(); assertTrue(keyStore.isKeyEntry(alias)); - Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_V1_PASSWORD); + Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_PASSWORD); assertTrue("No private key in pkcs12", sk instanceof PrivateKey); ScpCertificates certs = ScpCertificates.from(getCertificateChain(keyStore, alias)); diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java index dc0c3a41..40546cb0 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java @@ -22,7 +22,7 @@ class Scp11TestData { @SuppressWarnings("SpellCheckingInspection") - static final byte[] OCE_CERTS_V1 = ("-----BEGIN CERTIFICATE-----\n" + + static final byte[] OCE_CERTS = ("-----BEGIN CERTIFICATE-----\n" + "MIIB8DCCAZegAwIBAgIUf0lxsK1R+EydqZKLLV/vXhaykgowCgYIKoZIzj0EAwIw\n" + "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + "NDA1MjgwOTIyMDlaFw0yNDA4MjYwOTIyMDlaMC8xLTArBgNVBAMMJEV4YW1wbGUg\n" + @@ -50,96 +50,41 @@ class Scp11TestData { // PKCS12 certificate with a private key and full certificate chain @SuppressWarnings("SpellCheckingInspection") - public static byte[] OCE_V1 = Base64.fromUrlSafeString("MIIIfAIBAzCCCDIGCSqGSIb3DQEHAaCCCCME" + - "gggfMIIIGzCCBtIGCSqGSIb3DQEHBqCCBsMwgga_AgEAMIIGuAYJKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTB" + - "KMCkGCSqGSIb3DQEFDDAcBAg8IcJO44iSgAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAllIH" + - "doQx_USA3jmRMeciiAggZQAHCPJ5lzPV0Z5tnssXZZ1AWm8AcKEq28gWUTVqVxc-0EcbKQHig1Jx7rqC3q4" + - "G4sboIRw1vDH6q5O8eGsbkeNuYBim8fZ08JrsjeJABJoEiJrPqplMWA7H6a7athg3YSu1v4OR3UKN5Gyzn3" + - "s0Yx5yMm_xzw204TEK5_1LpK8AMcUliFSq7jw3Xl1RY0zjMSWyQjX0KmB9IdubqQCfhy8zkKluAQADtHsEY" + - "An0F3LoMETQytyUSkIvGMZoFemkCWV7zZ5n5IPhXL7gvnTu0WS8UxEnz_-FYdF43cjmwGfSb3OpaxOND4PB" + - "CpwzbFfVCLa6mUBlwq1KQWRm1-PFm4LnL-3s2mxfjJAsVYP4U722_FHpW8rdTsyvdift9lsQjas2jIjCu8P" + - "FClFZJLQldu5FxOhKzx2gsjYS_aeTdefwjlRiGtEFSrE1snKBbnBeRYFocBjhTD_sy3Vj0i5sbWwTx7iq67" + - "joWydWAMp_lGSZ6akWRsyku_282jlwYsc3pR05qCHkbV0TzJcZofhXBwRgH5NKfulnJ1gH-i3e3RT3TauAK" + - "lqCeAfvDvA3-jxEDy_puPncod7WH0m9P4OmXjZ0s5EI4U-v6bKPgL7LlTCEI6yj15P7kxmruoxZlDAmhixV" + - "mlwJ8ZbVxD6Q-AOhXYPg-il3AYaRAS-VyJla0K-ac6hpYVAnbZCPzgHVkKC6iq4a_azf2b4uq9ks109jjnr" + - "yAChdBsGdmStpZaPW4koMSAIJf12vGRp5jNjSaxaIL5QxTn0WCO8FHi1oqTmlTSWvR8wwZLiBmqQtnNTpew" + - "iLL7C22lerUT7pYvKLCq_nnPYtb5UrSTHrmTNOUzEGVOSAGUWV293S4yiPGIwxT3dPE5_UaU_yKq1RonMRa" + - "PhOZEESZEwLKVCqyDVEbAt7Hdahp-Ex0FVrC5JQhpVQ0Wn6uCptF2Jup70u-P2kVWjxrGBuRrlgEkKuHcoh" + - "WoO9EMX_bLK9KcY4s1ofnfgSNagsAyX7N51Bmahgz1MCFOEcuFa375QYQhqkyLO2ZkNTpFQtjHjX0izZWO5" + - "5LN3rNpcD9-fZt6ldoZCpg-t6y5xqHy-7soH0BpxF1oGIHAUkYSuXpLY0M7Pt3qqvsJ4_ycmFUEyoGv8Ib_" + - "ieUBbebPz0Uhn-jaTpjgtKCyym7nBxVCuUv39vZ31nhNr4WaFsjdB_FOJh1s4KI6kQgzCSObrIVXBcLCTXP" + - "fZ3jWxspKIREHn-zNuW7jIkbugSRiNFfVArcc7cmU4av9JPSmFiZzeyA0gkrkESTg8DVPT16u7W5HREX4Cw" + - "mKu-12R6iYQ_po9Hcy6NJ8ShLdAzU0-q_BzgH7Cb8qimjgfGBA3Mesc-P98FlCzAjB2EgucRuXuehM_Femm" + - "ZyNl0qI1Mj9qOgx_HeYaJaYD-yXwojApmetFGtDtMJsDxwL0zK7eGXeHHa7pd7OybKdSjDq25CCTOZvfR0D" + - "D55FDIGCy0FsJTcferzPFlkz_Q45vEwuGfEBnXXS9IhH4ySvJmDmyfLMGiHW6t-9gjyEEg-dwSOq9yXYScf" + - "CsefRl7-o_9nDoNQ8s_XS7LKlJ72ZEBaKeAxcm6q4wVwUWITNNl1R3EYAsFBWzYt4Ka9Ob3igVaNfeG9K4p" + - "fQqMWcPpqVp4FuIsEpDWZYuv71s-WMYCs1JMfHbHDUczdRet1Ir2vLDGeWwvci70AzeKvvQ9OwBVESRec6c" + - "Vrgt3EJWLey5sXY01WpMm526fwtLolSMpCf-dNePT97nXemQCcr3QXimagHTSGPngG3577FPrSQJl-lCJDY" + - "xBFFtnd6hq4OcVr5HiNAbLnSjBWbzqxhHMmgoojy4rwtHmrfyVYKXyl-98r-Lobitv2tpnBqmjL6dMPRBOJ" + - "vQl8-Wp4MGBsi1gvTgW_-pLlMXT--1iYyxBeK9_AN5hfjtrivewE3JY531jwkrl3rUl50MKwBJMMAtQQIYr" + - "Dg7DAg_-QcOi-2mgo9zJPzR2jIXF0wP-9FA4-MITa2v78QVXcesh63agcFJCayGAL1StnbSBvvDqK5vEei3" + - "uGZbeJEpU1hikQx57w3UzS9O7OSQMFvRBOrFBQsYC4JzfF0soIweGNpJxpm-UNYz-hB9vCb8-3OHA069M0C" + - "AlJVOTF9uEpLVRzK-1kwggFBBgkqhkiG9w0BBwGgggEyBIIBLjCCASowggEmBgsqhkiG9w0BDAoBAqCB7zC" + - "B7DBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIexxrwNlHM34CAggAMAwGCCqGSIb3DQIJBQAwHQ" + - "YJYIZIAWUDBAEqBBAkK96h6gHJglyJl1_yEylvBIGQh62z7u5RoQ9y5wIXbE3_oMQTKVfCSrtqGUmj38sxD" + - "Y7yIoTVQq7sw0MPNeYHROgGUAzawU0DlXMGuOWrbgzYeURZs0_HZ2Cqk8qhVnD8TgpB2n0U0NB7aJRHlkzT" + - "l5MLFAwn3NE49CSzb891lGwfLYXYCfNfqltD7xZ7uvz6JAo_y6UtY8892wrRv4UdejyfMSUwIwYJKoZIhvc" + - "NAQkVMRYEFJBU0s1_6SLbIRbyeq65gLWqClWNMEEwMTANBglghkgBZQMEAgEFAAQgqkOJRTcBlnx5yn57k2" + - "3PH-qUXUGPEuYkrGy-DzEQiikECB0BXjHOZZhuAgIIAA=="); - public static char[] OCE_V1_PASSWORD = "password".toCharArray(); - - @SuppressWarnings("SpellCheckingInspection") - static final byte[] OCE_CERTS_V2 = ("-----BEGIN CERTIFICATE-----\n" + - "MIICRTCCAeugAwIBAgICEAAwCgYIKoZIzj0EAwIwaDELMAkGA1UEBhMCU1YxCzAJ\n" + - "BgNVBAgMAlNWMRIwEAYDVQQHDAlTdG9ja2hvbG0xEDAOBgNVBAoMB1Rlc3RPcmcx\n" + - "FDASBgNVBAsMC1Rlc3RPcmdVbml0MRAwDgYDVQQDDAdDQS1LTE9DMCAXDTI0MDgw\n" + - "MjA3NTI0MVoYDzIxMjQwNzA5MDc1MjQxWjBTMQswCQYDVQQGEwJTVjELMAkGA1UE\n" + - "CAwCU1YxEDAOBgNVBAoMB1Rlc3RPcmcxFDASBgNVBAsMC1Rlc3RPcmdVbml0MQ8w\n" + - "DQYDVQQDDAZDQS1PQ0UwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARQ8mUZsuiR\n" + - "njsqLh8WMPz2CB6rhF7FZhLNqBQCEBT9yP2TPSqtSizYj0IBnYVFpNEo+e70dU7F\n" + - "3VmjkbuEXMTAo4GXMIGUMB8GA1UdIwQYMBaAFHMPyjll59Oc2tkOYx70pZ0u6JcJ\n" + - "MAsGA1UdDwQEAwIDCDAZBgNVHSAEEjAQMA4GDCqGSIb8a2QACgIBADAUBgwqhkiG\n" + - "/GtkAAoCAAEEBAQCvyAwFAYMKoZIhvxrZAAKAgACBAQEAl8gMB0GA1UdDgQWBBTv\n" + - "xZlvtJr2cz2ZOX+fjOyJKKIUzDAKBggqhkjOPQQDAgNIADBFAiBlnbco6Ciwf+3Y\n" + - "EyK8ZU07zeQPp47+XUEfgPF1qL9TgwIhANSqEpSjoMJTpFO8gzOey+Q83mXOiCRp\n" + - "c6go7DG5ObQh\n" + - "-----END CERTIFICATE-----\n" - + "-----BEGIN CERTIFICATE-----\n" + - "MIICLjCCAdSgAwIBAgIUOhaGJ8+QFxtHCXhvLRGhJY/wq7wwCgYIKoZIzj0EAwIw\n" + - "aDELMAkGA1UEBhMCU1YxCzAJBgNVBAgMAlNWMRIwEAYDVQQHDAlTdG9ja2hvbG0x\n" + - "EDAOBgNVBAoMB1Rlc3RPcmcxFDASBgNVBAsMC1Rlc3RPcmdVbml0MRAwDgYDVQQD\n" + - "DAdDQS1LTE9DMCAXDTI0MDgwMjA3NTE1MFoYDzIyMjQwNjE1MDc1MTUwWjBoMQsw\n" + - "CQYDVQQGEwJTVjELMAkGA1UECAwCU1YxEjAQBgNVBAcMCVN0b2NraG9sbTEQMA4G\n" + - "A1UECgwHVGVzdE9yZzEUMBIGA1UECwwLVGVzdE9yZ1VuaXQxEDAOBgNVBAMMB0NB\n" + - "LUtMT0MwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATRC0DYFzG0Lm0TwekgXYPz\n" + - "2ScKOARoft2t/E4WUyFiX0Snsy6S2y+hYP+bscnjUEKZplxE91MlsBNVhG09ZMx3\n" + - "o1owWDAdBgNVHQ4EFgQUcw/KOWXn05za2Q5jHvSlnS7olwkwCwYDVR0PBAQDAgEG\n" + - "MA8GA1UdEwEB/wQFMAMBAf8wGQYDVR0gBBIwEDAOBgwqhkiG/GtkAAoCARQwCgYI\n" + - "KoZIzj0EAwIDSAAwRQIhAPhg4A/O3RNNiBniSAzenpE1ftkaKSk21oMd95XKwdqb\n" + - "AiBzSpZzDimTeD/24luQ27oAOAmPI808aX8YMctqlo0LEg==\n" + - "-----END CERTIFICATE-----\n").getBytes(StandardCharsets.UTF_8); - - @SuppressWarnings("SpellCheckingInspection") - public static byte[] OCE_V2 = Base64.fromUrlSafeString("MIIE7AIBAzCCBKIGCSqGSIb3DQEHAaCCBJME" + - "ggSPMIIEizCCAzoGCSqGSIb3DQEHBqCCAyswggMnAgEAMIIDIAYJKoZIhvcNAQcBMF8GCSqGSIb3DQEFDTB" + - "SMDEGCSqGSIb3DQEFDDAkBBAn11qHSzbFWry2uT9sm1TkAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAw" + - "QBKgQQyd4c6OQQ_iirEEQOSE9XoYCCArDY1YqqfjynV3J09obO-_0WDGjJ-isRyYXbpRwOrw3r8-m7TmN1a" + - "nC7t0Gwb-Do2y3gbX54pko8lXY0rUyT0GTC1fr820Ghi72ovO6rr9JwSv2ndjOe-A4e65aisjMlHPZKJi1y" + - "YWsV9dJzO6OO75nnauX6SCeYCMTWzRKMZj7NHPaGFsl_jyaEqxAteY5tXYudzGmOYLIcSPMj_r0QbfL_Fo8" + - "DtkI6DcXOSVQnu-fHQl8SZAN-6qZ2eQr4TdsS3njn4gscgqsXnKw0FIhv3Js7ab89WHpLrjPHW31RJPSPWc" + - "vTFgMRb4O_2OmEm7aiwj4YnvBdnxtYmeCi5lbthZxW3MFu72DEAZQsnXvflGaV29rafUhKgwWWbiUvX0f4r" + - "XIUrfbQ1udf1I5l7LNC7RMwHYsObSOwoX6dDzj1kGroz1B_MeiIznT0RAd-YvLZ1SQM99MB8_gktWyJUO1e" + - "HjF4faBHgGAGCA0svdryKgy7D7TzvS2TjNOTWaJvRlaxzFmerXTmZytnDEbkU9eVyiiPR37KcpD2_KiLJzn" + - "vWKSo3wImP9-MPnxwNrkEcKf4qHZaE3ky_WLbnC2R8K5odIt7_01A0Nm-f-1NbuxtZBLy6tKnYUf3ir_PPm" + - "pjphS-D3coHn4Tlfa_chGNsMnP-TWDkxCgCyxHDvotpIynHxAibS9GHGHDF6nJekS9cFkGLJ9axJg2Pygic" + - "g78ls-CcDeAllFT46sU1d8Pz9A9MhJYmKc-AWBM00r9P32JC7kAANbOWErgUDwsRMxxHqVDo1U7QTL8LkBI" + - "CmVkpqzF7ObxwU04PB7E0k22sMp1zg95foVst3rZHriJ85CPq7Ht2ZsyLKmJNgqTfkgEJ_kVMuRRRcs4Pod" + - "4aHZL40qy6qrMru-qPTnVD0_2kV59W3xbd_ybMIIBSQYJKoZIhvcNAQcBoIIBOgSCATYwggEyMIIBLgYLKo" + - "ZIhvcNAQwKAQKggfcwgfQwXwYJKoZIhvcNAQUNMFIwMQYJKoZIhvcNAQUMMCQEEI2wPOcHSnElmbOyeO0dj" + - "bACAggAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBDH2f8UV8_hw3QAA7KO6WCYBIGQF6lkbV6ZPhC-" + - "s0WB-25i_Q6uPFieZsmqBsLHoVzXE8US_Kz4YitiVkVPY8JAcJSMI6DcQVkGPJW7suipx2evtne9kJI8TYh" + - "VsX0g7pv3HyTg2roDlHCbedoRHZcoC8lqy078LSdJicJmRLN0aj417oxRbMm972_pCHajk60hhA2A_fDoLA" + - "bEPJoO1qdFcjCjMSUwIwYJKoZIhvcNAQkVMRYEFOgid7IToNeY7_auRfGdSPhz0f-yMEEwMTANBglghkgBZ" + - "QMEAgEFAAQggxlm83v4OwO2nvegRpOK2G4HvCi0iO31XksWx0Pf5DkECAqUVhzeEdSPAgIIAA=="); - public static char[] OCE_V2_PASSWORD = "password".toCharArray(); + static byte[] OCE = Base64.fromUrlSafeString("MIIIfAIBAzCCCDIGCSqGSIb3DQEHAaCCCCMEgggfMIIIGz" + + "CCBtIGCSqGSIb3DQEHBqCCBsMwgga_AgEAMIIGuAYJKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTBKMCkGCSqGS" + + "Ib3DQEFDDAcBAg8IcJO44iSgAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAllIHdoQx_USA3j" + + "mRMeciiAggZQAHCPJ5lzPV0Z5tnssXZZ1AWm8AcKEq28gWUTVqVxc-0EcbKQHig1Jx7rqC3q4G4sboIRw1v" + + "DH6q5O8eGsbkeNuYBim8fZ08JrsjeJABJoEiJrPqplMWA7H6a7athg3YSu1v4OR3UKN5Gyzn3s0Yx5yMm_x" + + "zw204TEK5_1LpK8AMcUliFSq7jw3Xl1RY0zjMSWyQjX0KmB9IdubqQCfhy8zkKluAQADtHsEYAn0F3LoMET" + + "QytyUSkIvGMZoFemkCWV7zZ5n5IPhXL7gvnTu0WS8UxEnz_-FYdF43cjmwGfSb3OpaxOND4PBCpwzbFfVCL" + + "a6mUBlwq1KQWRm1-PFm4LnL-3s2mxfjJAsVYP4U722_FHpW8rdTsyvdift9lsQjas2jIjCu8PFClFZJLQld" + + "u5FxOhKzx2gsjYS_aeTdefwjlRiGtEFSrE1snKBbnBeRYFocBjhTD_sy3Vj0i5sbWwTx7iq67joWydWAMp_" + + "lGSZ6akWRsyku_282jlwYsc3pR05qCHkbV0TzJcZofhXBwRgH5NKfulnJ1gH-i3e3RT3TauAKlqCeAfvDvA" + + "3-jxEDy_puPncod7WH0m9P4OmXjZ0s5EI4U-v6bKPgL7LlTCEI6yj15P7kxmruoxZlDAmhixVmlwJ8ZbVxD" + + "6Q-AOhXYPg-il3AYaRAS-VyJla0K-ac6hpYVAnbZCPzgHVkKC6iq4a_azf2b4uq9ks109jjnryAChdBsGdm" + + "StpZaPW4koMSAIJf12vGRp5jNjSaxaIL5QxTn0WCO8FHi1oqTmlTSWvR8wwZLiBmqQtnNTpewiLL7C22ler" + + "UT7pYvKLCq_nnPYtb5UrSTHrmTNOUzEGVOSAGUWV293S4yiPGIwxT3dPE5_UaU_yKq1RonMRaPhOZEESZEw" + + "LKVCqyDVEbAt7Hdahp-Ex0FVrC5JQhpVQ0Wn6uCptF2Jup70u-P2kVWjxrGBuRrlgEkKuHcohWoO9EMX_bL" + + "K9KcY4s1ofnfgSNagsAyX7N51Bmahgz1MCFOEcuFa375QYQhqkyLO2ZkNTpFQtjHjX0izZWO55LN3rNpcD9" + + "-fZt6ldoZCpg-t6y5xqHy-7soH0BpxF1oGIHAUkYSuXpLY0M7Pt3qqvsJ4_ycmFUEyoGv8Ib_ieUBbebPz0" + + "Uhn-jaTpjgtKCyym7nBxVCuUv39vZ31nhNr4WaFsjdB_FOJh1s4KI6kQgzCSObrIVXBcLCTXPfZ3jWxspKI" + + "REHn-zNuW7jIkbugSRiNFfVArcc7cmU4av9JPSmFiZzeyA0gkrkESTg8DVPT16u7W5HREX4CwmKu-12R6iY" + + "Q_po9Hcy6NJ8ShLdAzU0-q_BzgH7Cb8qimjgfGBA3Mesc-P98FlCzAjB2EgucRuXuehM_FemmZyNl0qI1Mj" + + "9qOgx_HeYaJaYD-yXwojApmetFGtDtMJsDxwL0zK7eGXeHHa7pd7OybKdSjDq25CCTOZvfR0DD55FDIGCy0" + + "FsJTcferzPFlkz_Q45vEwuGfEBnXXS9IhH4ySvJmDmyfLMGiHW6t-9gjyEEg-dwSOq9yXYScfCsefRl7-o_" + + "9nDoNQ8s_XS7LKlJ72ZEBaKeAxcm6q4wVwUWITNNl1R3EYAsFBWzYt4Ka9Ob3igVaNfeG9K4pfQqMWcPpqV" + + "p4FuIsEpDWZYuv71s-WMYCs1JMfHbHDUczdRet1Ir2vLDGeWwvci70AzeKvvQ9OwBVESRec6cVrgt3EJWLe" + + "y5sXY01WpMm526fwtLolSMpCf-dNePT97nXemQCcr3QXimagHTSGPngG3577FPrSQJl-lCJDYxBFFtnd6hq" + + "4OcVr5HiNAbLnSjBWbzqxhHMmgoojy4rwtHmrfyVYKXyl-98r-Lobitv2tpnBqmjL6dMPRBOJvQl8-Wp4MG" + + "Bsi1gvTgW_-pLlMXT--1iYyxBeK9_AN5hfjtrivewE3JY531jwkrl3rUl50MKwBJMMAtQQIYrDg7DAg_-Qc" + + "Oi-2mgo9zJPzR2jIXF0wP-9FA4-MITa2v78QVXcesh63agcFJCayGAL1StnbSBvvDqK5vEei3uGZbeJEpU1" + + "hikQx57w3UzS9O7OSQMFvRBOrFBQsYC4JzfF0soIweGNpJxpm-UNYz-hB9vCb8-3OHA069M0CAlJVOTF9uE" + + "pLVRzK-1kwggFBBgkqhkiG9w0BBwGgggEyBIIBLjCCASowggEmBgsqhkiG9w0BDAoBAqCB7zCB7DBXBgkqh" + + "kiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIexxrwNlHM34CAggAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUD" + + "BAEqBBAkK96h6gHJglyJl1_yEylvBIGQh62z7u5RoQ9y5wIXbE3_oMQTKVfCSrtqGUmj38sxDY7yIoTVQq7" + + "sw0MPNeYHROgGUAzawU0DlXMGuOWrbgzYeURZs0_HZ2Cqk8qhVnD8TgpB2n0U0NB7aJRHlkzTl5MLFAwn3N" + + "E49CSzb891lGwfLYXYCfNfqltD7xZ7uvz6JAo_y6UtY8892wrRv4UdejyfMSUwIwYJKoZIhvcNAQkVMRYEF" + + "JBU0s1_6SLbIRbyeq65gLWqClWNMEEwMTANBglghkgBZQMEAgEFAAQgqkOJRTcBlnx5yn57k23PH-qUXUGP" + + "EuYkrGy-DzEQiikECB0BXjHOZZhuAgIIAA=="); + static char[] OCE_PASSWORD = "password".toCharArray(); } From 76d893d1569c158a4dcc6aef7d13fac111df84b7 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 6 Aug 2024 17:08:08 +0200 Subject: [PATCH 41/46] make log message consistent --- .../java/com/yubico/yubikit/core/smartcard/scp/ScpState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java index 62f2f978..38d606df 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java @@ -86,7 +86,7 @@ public ScpState(SessionKeys keys, byte[] macChain) { public byte[] encrypt(byte[] data) { // Pad the data - logger.trace("Plain data: {}", StringUtils.bytesToHex(data)); + logger.trace("Plaintext data: {}", StringUtils.bytesToHex(data)); int padLen = 16 - (data.length % 16); byte[] padded = Arrays.copyOf(data, data.length + padLen); padded[data.length] = (byte) 0x80; From 1fefaa269d56fb59792ed7a937a5e2c4b84dfe07 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 8 Aug 2024 16:02:12 +0200 Subject: [PATCH 42/46] remove unchecked calls in test code --- .../com/yubico/yubikit/testing/TestState.java | 218 +----------------- .../yubikit/testing/fido/FidoTestState.java | 51 ++++ .../yubikit/testing/mpe/MpeTestState.java | 30 +++ .../yubikit/testing/oath/OathTestState.java | 40 ++++ .../testing/openpgp/OpenPgpTestState.java | 31 +++ .../yubikit/testing/piv/PivTestState.java | 33 +++ .../testing/sd/SecurityDomainTestState.java | 68 ++++++ 7 files changed, 259 insertions(+), 212 deletions(-) diff --git a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java index 249cb00d..ebccb449 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -16,59 +16,47 @@ 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.core.smartcard.scp.SecurityDomainSession; -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 com.yubico.yubikit.testing.sd.SecurityDomainTestState; import java.io.IOException; import javax.annotation.Nullable; public class TestState { - public static class Builder> { + public abstract static class Builder> { final protected YubiKeyDevice device; @Nullable private Byte scpKid = null; @Nullable private ReconnectDeviceCallback reconnectDeviceCallback = null; + public abstract T getThis(); + public Builder(YubiKeyDevice device) { this.device = device; } public T scpKid(@Nullable Byte scpKid) { this.scpKid = scpKid; - //noinspection unchecked - return (T) this; + return getThis(); } public T reconnectDeviceCallback(@Nullable ReconnectDeviceCallback reconnectDeviceCallback) { this.reconnectDeviceCallback = reconnectDeviceCallback; - //noinspection unchecked - return (T) this; + return getThis(); } - public TestState build() throws Throwable { - return new TestState(this); - } + public abstract TestState build() throws Throwable; } protected YubiKeyDevice currentDevice; @@ -91,11 +79,6 @@ public boolean isUsbTransport() { return isUsbTransport; } - @SuppressWarnings("unused") - public interface DeviceCallback { - void invoke() throws Throwable; - } - public interface StatefulDeviceCallback { void invoke(S state) throws Throwable; } @@ -112,10 +95,6 @@ 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(); } @@ -145,191 +124,6 @@ public boolean isFipsApproved(DeviceInfo deviceInfo, Capability capability) { (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 R withCtap2(SessionCallbackT callback) throws Throwable { - R result; - try (YubiKeyConnection connection = openConnection()) { - final Ctap2Session ctap2 = getCtap2Session(connection); - assumeTrue("No CTAP2 support", ctap2 != null); - result = callback.invoke(ctap2); - } - reconnect(); - return result; - } - - public void withCtap2(SessionCallback callback) throws Throwable { - try (YubiKeyConnection connection = openConnection()) { - final Ctap2Session ctap2 = getCtap2Session(connection); - assumeTrue("No CTAP2 support", ctap2 != null); - callback.invoke(ctap2); - } - reconnect(); - } - - 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 - protected OathSession getOathSession(SmartCardConnection connection, ScpParameters scpParameters) - throws IOException { - try { - return new OathSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - // no OATH support - } - return null; - } - - // Security domain helpers - public void withSecurityDomain(SessionCallback callback) throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - callback.invoke(getSecurityDomainSession(connection)); - } - reconnect(); - } - - public R withSecurityDomain(SessionCallbackT callback) throws Throwable { - R result; - try (SmartCardConnection connection = openSmartCardConnection()) { - result = callback.invoke(getSecurityDomainSession(connection)); - } - reconnect(); - return result; - } - - public void withSecurityDomain(ScpKeyParams scpKeyParams, SessionCallback callback) throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - callback.invoke(getSecurityDomainSession(scpKeyParams, connection)); - } - reconnect(); - } - - public void withSecurityDomain(StatefulSessionCallback callback) - throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - //noinspection unchecked - callback.invoke(getSecurityDomainSession(connection), (T) this); - } - reconnect(); - } - - public R withSecurityDomain(ScpKeyParams scpKeyParams, SessionCallbackT callback) throws Throwable { - R result; - try (SmartCardConnection connection = openSmartCardConnection()) { - result = callback.invoke(getSecurityDomainSession(scpKeyParams, connection)); - } - reconnect(); - return result; - } - - @Nullable - protected SecurityDomainSession getSecurityDomainSession(SmartCardConnection connection) - throws IOException { - try { - return new SecurityDomainSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - // no OATH support - } - return null; - } - - @Nullable - protected SecurityDomainSession getSecurityDomainSession(ScpKeyParams scpKeyParams, SmartCardConnection connection) - throws IOException { - try { - return new SecurityDomainSession(connection, scpKeyParams); - } 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)) { diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java index 2e9294f1..83d89d34 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java @@ -25,6 +25,8 @@ import com.yubico.yubikit.core.YubiKeyConnection; 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.smartcard.SmartCardConnection; import com.yubico.yubikit.fido.client.BasicWebAuthnClient; import com.yubico.yubikit.fido.client.ClientError; import com.yubico.yubikit.fido.client.CredentialManager; @@ -63,6 +65,11 @@ public Builder(YubiKeyDevice device, PinUvAuthProtocol pinUvAuthProtocol) { this.pinUvAuthProtocol = pinUvAuthProtocol; } + @Override + public Builder getThis() { + return this; + } + public Builder setPin(boolean setPin) { this.setPin = setPin; return this; @@ -202,4 +209,48 @@ void verifyOrSetPin(Ctap2Session session) throws IOException, CommandException { "and try again."); } } + + public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { + callback.invoke(this); + } + + public void withCtap2(TestState.StatefulSessionCallback callback) + throws Throwable { + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + callback.invoke(ctap2, this); + } + reconnect(); + } + + public R withCtap2(SessionCallbackT callback) throws Throwable { + R result; + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + result = callback.invoke(ctap2); + } + reconnect(); + return result; + } + + public void withCtap2(SessionCallback callback) throws Throwable { + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + callback.invoke(ctap2); + } + reconnect(); + } + + @Nullable + public static Ctap2Session getCtap2Session(YubiKeyConnection connection) + throws IOException, CommandException { + return (connection instanceof FidoConnection) + ? new Ctap2Session((FidoConnection) connection) + : connection instanceof SmartCardConnection + ? new Ctap2Session((SmartCardConnection) connection) + : null; + } } 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 index 79af5b8e..1b4eec17 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java @@ -20,12 +20,17 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assume.assumeTrue; +import com.yubico.yubikit.core.YubiKeyConnection; import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.core.smartcard.SmartCardConnection; +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.piv.PivSession; import com.yubico.yubikit.testing.TestState; +import com.yubico.yubikit.testing.fido.FidoTestState; +import com.yubico.yubikit.testing.piv.PivTestState; public class MpeTestState extends TestState { public static class Builder extends TestState.Builder { @@ -34,6 +39,11 @@ public Builder(YubiKeyDevice device) { super(device); } + @Override + public Builder getThis() { + return this; + } + public MpeTestState build() throws Throwable { return new MpeTestState(this); } @@ -65,4 +75,24 @@ boolean isFidoResetBlocked() { final DeviceInfo deviceInfo = getDeviceInfo(); return (deviceInfo.getResetBlocked() & Capability.FIDO2.bit) == Capability.FIDO2.bit; } + + public void withPiv(StatefulSessionCallback callback) + throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + final PivSession piv = PivTestState.getPivSession(connection, scpParameters); + assumeTrue("No PIV support", piv != null); + callback.invoke(piv, this); + } + reconnect(); + } + + public void withCtap2(TestState.StatefulSessionCallback callback) + throws Throwable { + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = FidoTestState.getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + callback.invoke(ctap2, this); + } + reconnect(); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java index 2f467f79..f2b25862 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java @@ -24,8 +24,13 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.management.Capability; import com.yubico.yubikit.oath.OathSession; +import com.yubico.yubikit.testing.ScpParameters; import com.yubico.yubikit.testing.TestState; +import java.io.IOException; + +import javax.annotation.Nullable; + public class OathTestState extends TestState { public boolean isFipsApproved; public char[] password; @@ -36,6 +41,11 @@ public Builder(YubiKeyDevice device) { super(device); } + @Override + public Builder getThis() { + return this; + } + public OathTestState build() throws Throwable { return new OathTestState(this); } @@ -78,4 +88,34 @@ protected OathTestState(OathTestState.Builder builder) throws Throwable { assertTrue("Device not OATH FIPS approved as expected", isFipsApproved); } } + + public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { + callback.invoke(this); + } + + public void withOath(StatefulSessionCallback callback) + throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getOathSession(connection, scpParameters), this); + } + reconnect(); + } + + public void withOath(SessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getOathSession(connection, scpParameters)); + } + reconnect(); + } + + @Nullable + public static OathSession getOathSession(SmartCardConnection connection, ScpParameters scpParameters) + throws IOException { + try { + return new OathSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + // no OATH support + } + return null; + } } 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 b3582e3a..d1c8d7e9 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 @@ -21,15 +21,22 @@ import static org.junit.Assume.assumeTrue; import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.application.ApplicationNotAvailableException; +import com.yubico.yubikit.core.application.CommandException; 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.ScpParameters; import com.yubico.yubikit.testing.TestState; import org.junit.Assume; +import java.io.IOException; + +import javax.annotation.Nullable; + public class OpenPgpTestState extends TestState { private static final char[] COMPLEX_USER_PIN = "112345678".toCharArray(); @@ -45,6 +52,11 @@ public Builder(YubiKeyDevice device) { super(device); } + @Override + public Builder getThis() { + return this; + } + public OpenPgpTestState build() throws Throwable { return new OpenPgpTestState(this); } @@ -98,4 +110,23 @@ protected OpenPgpTestState(OpenPgpTestState.Builder builder) throws Throwable { assertTrue("Device not OpenPgp FIPS approved as expected", isFipsApproved); } } + + public void withOpenPgp(StatefulSessionCallback callback) + throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getOpenPgpSession(connection, scpParameters), this); + } + reconnect(); + } + + @Nullable + public static OpenPgpSession getOpenPgpSession(SmartCardConnection connection, ScpParameters scpParameters) + throws IOException, CommandException { + try { + return new OpenPgpSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + // no OpenPgp support + } + return null; + } } 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 cfb77863..1373efd7 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 @@ -23,14 +23,21 @@ import static org.junit.Assume.assumeTrue; import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.application.ApplicationNotAvailableException; +import com.yubico.yubikit.core.smartcard.ApduException; 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.ScpParameters; import com.yubico.yubikit.testing.TestState; +import java.io.IOException; + +import javax.annotation.Nullable; + public class PivTestState extends TestState { static final char[] DEFAULT_PIN = "123456".toCharArray(); @@ -60,6 +67,11 @@ public Builder(YubiKeyDevice device) { super(device); } + @Override + public Builder getThis() { + return this; + } + public PivTestState build() throws Throwable { return new PivTestState(this); } @@ -131,4 +143,25 @@ protected PivTestState(Builder builder) throws Throwable { boolean isInvalidKeyType(KeyType keyType) { return isFipsApproved && (keyType == KeyType.RSA1024 || keyType == KeyType.X25519); } + + public void withPiv(StatefulSessionCallback callback) + throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + final PivSession piv = getPivSession(connection, scpParameters); + assumeTrue("No PIV support", piv != null); + callback.invoke(piv, this); + } + reconnect(); + } + + @Nullable + public static PivSession getPivSession(SmartCardConnection connection, ScpParameters scpParameters) + throws IOException { + try { + return new PivSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException | ApduException ignored) { + // no PIV support + } + return null; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java index 33a561c3..b8bc1f73 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java @@ -20,14 +20,19 @@ import static org.junit.Assume.assumeTrue; import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.application.ApplicationNotAvailableException; import com.yubico.yubikit.core.smartcard.SmartCardConnection; +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; import com.yubico.yubikit.testing.TestState; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import java.io.IOException; import java.security.Security; +import javax.annotation.Nullable; + public class SecurityDomainTestState extends TestState { public static class Builder extends TestState.Builder { @@ -36,6 +41,11 @@ public Builder(YubiKeyDevice device) { super(device); } + @Override + public Builder getThis() { + return this; + } + public SecurityDomainTestState build() throws Throwable { return new SecurityDomainTestState(this); } @@ -59,4 +69,62 @@ public static void setupJca() { Security.removeProvider("BC"); Security.addProvider(new BouncyCastleProvider()); } + + public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { + callback.invoke(this); + } + + public void withSecurityDomain(SessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getSecurityDomainSession(connection)); + } + reconnect(); + } + + public R withSecurityDomain(SessionCallbackT callback) throws Throwable { + R result; + try (SmartCardConnection connection = openSmartCardConnection()) { + result = callback.invoke(getSecurityDomainSession(connection)); + } + reconnect(); + return result; + } + + public void withSecurityDomain(ScpKeyParams scpKeyParams, SessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getSecurityDomainSession(scpKeyParams, connection)); + } + reconnect(); + } + + public R withSecurityDomain(ScpKeyParams scpKeyParams, SessionCallbackT callback) throws Throwable { + R result; + try (SmartCardConnection connection = openSmartCardConnection()) { + result = callback.invoke(getSecurityDomainSession(scpKeyParams, connection)); + } + reconnect(); + return result; + } + + @Nullable + protected SecurityDomainSession getSecurityDomainSession(SmartCardConnection connection) + throws IOException { + try { + return new SecurityDomainSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + // no Security Domain support + } + return null; + } + + @Nullable + public static SecurityDomainSession getSecurityDomainSession(ScpKeyParams scpKeyParams, SmartCardConnection connection) + throws IOException { + try { + return new SecurityDomainSession(connection, scpKeyParams); + } catch (ApplicationNotAvailableException ignored) { + // no Security Domain support + } + return null; + } } From 65ff8e744afc5e987a2ba9dd2f1e39901611f594 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 8 Aug 2024 17:58:14 +0200 Subject: [PATCH 43/46] self review --- .../yubico/yubikit/core/smartcard/AppId.java | 2 +- .../yubikit/core/smartcard/ScpProcessor.java | 2 -- .../core/smartcard/SmartCardProtocol.java | 2 +- .../core/smartcard/scp/Scp03KeyParams.java | 2 +- .../yubikit/core/smartcard/scp/ScpKid.java | 3 -- .../yubikit/core/smartcard/scp/ScpState.java | 36 +++++++++++-------- .../core/smartcard/scp/StaticKeys.java | 8 +++-- .../core/smartcard/scp/package-info.java | 2 +- .../yubikit/management/ManagementSession.java | 2 +- .../com/yubico/yubikit/oath/OathSession.java | 2 +- .../yubikit/openpgp/OpenPgpSession.java | 2 +- .../com/yubico/yubikit/piv/PivSession.java | 3 +- .../com/yubico/yubikit/testing/MpeUtils.java | 28 --------------- .../com/yubico/yubikit/testing/TestState.java | 7 ++++ .../yubikit/testing/mpe/MpeTestState.java | 1 - .../yubikit/testing/piv/PivTestState.java | 1 - .../yubikit/testing/piv/PivTestUtils.java | 14 -------- .../yubikit/testing/sd/Scp03DeviceTests.java | 2 +- .../yubikit/testing/sd/Scp11DeviceTests.java | 2 +- .../yubikit/testing/sd/ScpCertificates.java | 2 +- .../yubikit/yubiotp/YubiOtpSession.java | 2 +- 21 files changed, 46 insertions(+), 79 deletions(-) delete mode 100644 testing/src/main/java/com/yubico/yubikit/testing/MpeUtils.java diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java index dccc759f..1c182ff2 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 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. diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java index 5337f7dd..96f82245 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java @@ -23,8 +23,6 @@ import java.nio.ByteBuffer; import java.util.Arrays; -import javax.security.auth.DestroyFailedException; - public class ScpProcessor extends ChainedResponseProcessor { private final ScpState state; diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java index 89822961..c4944fe1 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2022 Yubico. + * Copyright (C) 2019-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java index aafaa092..4102299c 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java @@ -25,7 +25,7 @@ public class Scp03KeyParams implements ScpKeyParams { final StaticKeys keys; /** - * @param keyRef the reference to the key set to authenticat with. + * @param keyRef the reference to the key set to authenticate with. * @param keys the key material for authentication. */ public Scp03KeyParams(KeyRef keyRef, StaticKeys keys) { diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java index 1edf1cf3..a916ffe4 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java @@ -16,9 +16,6 @@ package com.yubico.yubikit.core.smartcard.scp; -/** - * Named - */ public final class ScpKid { public static final byte SCP03 = 0x1; public static final byte SCP11a = 0x11; diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java index 38d606df..5533d45c 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java @@ -59,8 +59,6 @@ import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; -import javax.security.auth.DestroyFailedException; - /** * Internal SCP state class for managing SCP state, handling encryption/decryption and MAC. @@ -86,7 +84,7 @@ public ScpState(SessionKeys keys, byte[] macChain) { public byte[] encrypt(byte[] data) { // Pad the data - logger.trace("Plaintext data: {}", StringUtils.bytesToHex(data)); + Logger.trace(logger, "Plaintext data: {}", StringUtils.bytesToHex(data)); int padLen = 16 - (data.length % 16); byte[] padded = Arrays.copyOf(data, data.length + padLen); padded[data.length] = (byte) 0x80; @@ -115,9 +113,11 @@ public byte[] decrypt(byte[] encrypted) throws BadResponseException { // Decrypt byte[] decrypted = null; try { - @SuppressWarnings("GetInstance") Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + @SuppressWarnings("GetInstance") + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, keys.senc); - byte[] ivData = ByteBuffer.allocate(16).put((byte) 0x80).put(new byte[11]).putInt(encCounter - 1).array(); + byte[] ivData = ByteBuffer.allocate(16).put((byte) 0x80).put(new byte[11]) + .putInt(encCounter - 1).array(); byte[] iv = cipher.doFinal(ivData); cipher = Cipher.getInstance("AES/CBC/NoPadding"); @@ -125,7 +125,7 @@ public byte[] decrypt(byte[] encrypted) throws BadResponseException { decrypted = cipher.doFinal(encrypted); for (int i = decrypted.length - 1; i > 0; i--) { if (decrypted[i] == (byte) 0x80) { - logger.trace("Plaintext resp: {}", StringUtils.bytesToHex(decrypted)); + Logger.trace(logger, "Plaintext resp: {}", StringUtils.bytesToHex(decrypted)); return Arrays.copyOf(decrypted, i); } else if (decrypted[i] != 0x00) { break; @@ -157,7 +157,8 @@ public byte[] mac(byte[] data) { } public byte[] unmac(byte[] data, short sw) throws BadResponseException { - byte[] msg = ByteBuffer.allocate(data.length - 8 + 2).put(data, 0, data.length - 8).putShort(sw).array(); + byte[] msg = ByteBuffer.allocate(data.length - 8 + 2).put(data, 0, data.length - 8) + .putShort(sw).array(); try { Mac mac = Mac.getInstance("AESCMAC"); @@ -179,7 +180,8 @@ public static Pair scp03Init(ApduProcessor processor, Scp03Key hostChallenge = RandomUtils.getRandomBytes(8); } - ApduResponse resp = processor.sendApdu(new Apdu(0x80, SecurityDomainSession.INS_INITIALIZE_UPDATE, keyParams.getKeyRef().getKvn(), 0x00, hostChallenge)); + ApduResponse resp = processor.sendApdu(new Apdu(0x80, SecurityDomainSession.INS_INITIALIZE_UPDATE, keyParams.getKeyRef() + .getKvn(), 0x00, hostChallenge)); if (resp.getSw() != SW.OK) { throw new ApduException(resp.getSw()); } @@ -197,12 +199,14 @@ public static Pair scp03Init(ApduProcessor processor, Scp03Key byte[] context = ByteBuffer.allocate(16).put(hostChallenge).put(cardChallenge).array(); SessionKeys sessionKeys = keyParams.keys.derive(context); - byte[] genCardCryptogram = StaticKeys.deriveKey(sessionKeys.smac, (byte) 0x00, context, (byte) 0x40).getEncoded(); + byte[] genCardCryptogram = StaticKeys.deriveKey(sessionKeys.smac, (byte) 0x00, context, (byte) 0x40) + .getEncoded(); if (!MessageDigest.isEqual(genCardCryptogram, cardCryptogram)) { throw new BadResponseException("Wrong SCP03 key set"); } - byte[] hostCryptogram = StaticKeys.deriveKey(sessionKeys.smac, (byte) 0x01, context, (byte) 0x40).getEncoded(); + byte[] hostCryptogram = StaticKeys.deriveKey(sessionKeys.smac, (byte) 0x01, context, (byte) 0x40) + .getEncoded(); return new Pair<>(new ScpState(sessionKeys, new byte[16]), hostCryptogram); } @@ -281,8 +285,10 @@ public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyPara // Static host key (SCP11a/c), or ephemeral key again (SCP11b) PrivateKey skOceEcka = keyParams.skOceEcka != null ? keyParams.skOceEcka : ephemeralOceEcka.getPrivate(); - int ins = keyParams.getKeyRef().getKid() == ScpKid.SCP11b ? SecurityDomainSession.INS_INTERNAL_AUTHENTICATE : SecurityDomainSession.INS_EXTERNAL_AUTHENTICATE; - ApduResponse resp = processor.sendApdu(new Apdu(0x80, ins, keyParams.getKeyRef().getKvn(), keyParams.getKeyRef().getKid(), data)); + int ins = keyParams.getKeyRef() + .getKid() == ScpKid.SCP11b ? SecurityDomainSession.INS_INTERNAL_AUTHENTICATE : SecurityDomainSession.INS_EXTERNAL_AUTHENTICATE; + ApduResponse resp = processor.sendApdu(new Apdu(0x80, ins, keyParams.getKeyRef() + .getKvn(), keyParams.getKeyRef().getKid(), data)); if (resp.getSw() != SW.OK) { throw new ApduException(resp.getSw()); } @@ -305,14 +311,16 @@ public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyPara KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH"); keyAgreement.init(ephemeralOceEcka.getPrivate()); - keyAgreement.doPhase(PublicKeyValues.Ec.fromEncodedPoint(epkOceEcka.getCurveParams(), epkSdEckaEncodedPoint).toPublicKey(), true); + keyAgreement.doPhase(PublicKeyValues.Ec.fromEncodedPoint(epkOceEcka.getCurveParams(), epkSdEckaEncodedPoint) + .toPublicKey(), true); byte[] ka1 = keyAgreement.generateSecret(); keyAgreement.init(skOceEcka); keyAgreement.doPhase(pk, true); byte[] ka2 = keyAgreement.generateSecret(); - byte[] keyMaterial = ByteBuffer.allocate(ka1.length + ka2.length).put(ka1).put(ka2).array(); + byte[] keyMaterial = ByteBuffer.allocate(ka1.length + ka2.length).put(ka1).put(ka2) + .array(); List keys = new ArrayList<>(); int counter = 1; diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java index 1bdb23ea..2db6b586 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java @@ -27,12 +27,14 @@ import javax.crypto.spec.SecretKeySpec; public class StaticKeys { - private static final byte[] DEFAULT_KEY = new byte[]{0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f}; + private static final byte[] DEFAULT_KEY = new byte[]{ + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f + }; final SecretKey enc; final SecretKey mac; - @Nullable - final SecretKey dek; + @Nullable final SecretKey dek; public StaticKeys(byte[] enc, byte[] mac, @Nullable byte[] dek) { this.enc = new SecretKeySpec(enc, "AES"); diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java index 2892dcc9..5b461322 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2022 Yubico. + * 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. diff --git a/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java b/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java index f5eff1b8..0764b4e6 100755 --- a/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java +++ b/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java @@ -168,7 +168,7 @@ void setMode(byte[] data) throws IOException, CommandException { } @Override - void deviceReset() throws IOException, CommandException { + void deviceReset() { throw new UnsupportedOperationException("deviceReset not supported on YubiKey NEO"); } }; diff --git a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java index 29fdd8af..8cdc8c4f 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 Yubico. + * Copyright (C) 2019-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java index 33e195aa..9b0cfe0e 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Yubico. + * Copyright (C) 2023-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java b/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java index 336b61b1..687e2d86 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java @@ -29,7 +29,6 @@ import com.yubico.yubikit.core.keys.PublicKeyValues; import com.yubico.yubikit.core.smartcard.Apdu; import com.yubico.yubikit.core.smartcard.ApduException; -import com.yubico.yubikit.core.smartcard.ApduFormat; import com.yubico.yubikit.core.smartcard.AppId; import com.yubico.yubikit.core.smartcard.SW; import com.yubico.yubikit.core.smartcard.SmartCardConnection; @@ -1458,7 +1457,7 @@ private int getRetriesFromCode(int statusCode) { if (statusCode == SW.AUTH_METHOD_BLOCKED) { return 0; } - if (version.isAtLeast(1, 0, 0) && version.isLessThan(1, 0, 4)) { + if (version.isLessThan(1, 0, 4)) { if (statusCode >= 0x6300 && statusCode <= 0x63ff) { return statusCode & 0xff; } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/MpeUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/MpeUtils.java deleted file mode 100644 index 469be307..00000000 --- a/testing/src/main/java/com/yubico/yubikit/testing/MpeUtils.java +++ /dev/null @@ -1,28 +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.management.DeviceInfo; -import com.yubico.yubikit.support.DeviceUtil; - -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/TestState.java b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java index ebccb449..618b2fb5 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -27,6 +27,7 @@ import com.yubico.yubikit.management.Capability; import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.management.ManagementSession; +import com.yubico.yubikit.support.DeviceUtil; import java.io.IOException; @@ -170,4 +171,10 @@ protected ManagementSession getManagementSession(YubiKeyConnection connection, S return session; } + + protected 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/mpe/MpeTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java index 1b4eec17..758910c3 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java @@ -16,7 +16,6 @@ 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; 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 1373efd7..92a5b02c 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,7 +16,6 @@ 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; 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 ac7d2a42..b957a33c 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 @@ -27,8 +27,6 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.junit.Assert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -60,16 +58,6 @@ @SuppressWarnings("SpellCheckingInspection") public class PivTestUtils { - private static final Logger logger = LoggerFactory.getLogger(PivTestUtils.class); - - 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, - }; - private enum StaticKey { RSA1024( KeyType.RSA1024, "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBALWeZ0E5O2l_iH" + @@ -443,14 +431,12 @@ public static void cv25519Tests() throws Exception { public static void ecSignAndVerify(PrivateKey privateKey, PublicKey publicKey) throws Exception { for (String algorithm : EC_SIGNATURE_ALGORITHMS) { - logger.debug("Test {}", algorithm); verify(publicKey, Signature.getInstance(algorithm), sign(privateKey, Signature.getInstance(algorithm))); } } public static void ed25519SignAndVerify(PrivateKey privateKey, PublicKey publicKey) throws Exception { String algorithm = "ED25519"; - logger.debug("Test {}", algorithm); verify(publicKey, Signature.getInstance(algorithm), sign(privateKey, Signature.getInstance(algorithm))); } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java index 06c7c0c8..9250b1b8 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Yubico. + * 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. diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java index ffab766c..1d1049da 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Yubico. + * 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. diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java index 4e277807..c300f379 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java @@ -26,7 +26,7 @@ import javax.annotation.Nullable; -public class ScpCertificates { +class ScpCertificates { @Nullable final X509Certificate ca; final List bundle; @Nullable final X509Certificate leaf; diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java index 79e9a850..4b9299dd 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 Yubico. + * Copyright (C) 2019-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 400cc8550bf4096e3438f996f676bf668bc0d490 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 9 Aug 2024 11:32:30 +0200 Subject: [PATCH 44/46] remove 'default' from PIV test state var names --- .../testing/piv/PivCertificateTests.java | 2 +- .../yubikit/testing/piv/PivDeviceTests.java | 40 +++++++++---------- .../testing/piv/PivJcaDecryptTests.java | 4 +- .../testing/piv/PivJcaDeviceTests.java | 14 +++---- .../testing/piv/PivJcaSigningTests.java | 4 +- .../yubikit/testing/piv/PivMoveKeyTests.java | 4 +- .../piv/PivPinComplexityDeviceTests.java | 2 +- .../yubikit/testing/piv/PivTestState.java | 24 +++++------ 8 files changed, 47 insertions(+), 47 deletions(-) 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 9c8cce67..42a71fca 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 @@ -46,7 +46,7 @@ public static void putCompressedCertificate(PivSession piv, PivTestState state) } private static void putCertificate(PivSession piv, PivTestState state, boolean compressed) throws IOException, ApduException, CertificateException, BadResponseException { - piv.authenticate(state.defaultManagementKey); + piv.authenticate(state.managementKey); 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 2bd9e168..5c9d3e3f 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 @@ -52,12 +52,12 @@ public static void testManagementKey(PivSession piv, PivTestState state) throws } logger.debug("Change management key"); - piv.authenticate(state.defaultManagementKey); + piv.authenticate(state.managementKey); piv.setManagementKey(managementKeyType, key2, false); logger.debug("Authenticate with the old key"); try { - piv.authenticate(state.defaultManagementKey); + piv.authenticate(state.managementKey); Assert.fail("Authenticated with wrong key"); } catch (ApduException e) { Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); @@ -65,7 +65,7 @@ public static void testManagementKey(PivSession piv, PivTestState state) throws logger.debug("Change management key"); piv.authenticate(key2); - piv.setManagementKey(managementKeyType, state.defaultManagementKey, false); + piv.setManagementKey(managementKeyType, state.managementKey, false); } public static void testManagementKeyType(PivSession piv, PivTestState state) throws BadResponseException, IOException, ApduException { @@ -75,12 +75,12 @@ public static void testManagementKeyType(PivSession piv, PivTestState state) thr byte[] aes128Key = Hex.decode("01020304010203040102030401020304"); logger.debug("Change management key type"); - piv.authenticate(state.defaultManagementKey); + piv.authenticate(state.managementKey); piv.setManagementKey(ManagementKeyType.AES128, aes128Key, false); Assert.assertEquals(ManagementKeyType.AES128, piv.getManagementKeyType()); try { - piv.authenticate(state.defaultManagementKey); + piv.authenticate(state.managementKey); Assert.fail("Authenticated with wrong key type"); } catch (IllegalArgumentException e) { // ignored @@ -88,16 +88,16 @@ public static void testManagementKeyType(PivSession piv, PivTestState state) thr // set original management key type piv.authenticate(aes128Key); - piv.setManagementKey(managementKeyType, state.defaultManagementKey, false); + piv.setManagementKey(managementKeyType, state.managementKey, false); } 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(state.defaultManagementKey); + piv.authenticate(state.managementKey); logger.debug("Verify PIN"); char[] pin2 = "11231123".toCharArray(); - piv.verifyPin(state.defaultPin); + piv.verifyPin(state.pin); MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(3)); logger.debug("Verify with wrong PIN"); @@ -111,7 +111,7 @@ public static void testPin(PivSession piv, PivTestState state) throws ApduExcept logger.debug("Change PIN with wrong PIN"); try { - piv.changePin(pin2, state.defaultPin); + piv.changePin(pin2, state.pin); Assert.fail("Change PIN with wrong PIN"); } catch (InvalidPinException e) { MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(1)); @@ -119,12 +119,12 @@ public static void testPin(PivSession piv, PivTestState state) throws ApduExcept } logger.debug("Change PIN"); - piv.changePin(state.defaultPin, pin2); + piv.changePin(state.pin, pin2); piv.verifyPin(pin2); logger.debug("Verify with wrong PIN"); try { - piv.verifyPin(state.defaultPin); + piv.verifyPin(state.pin); Assert.fail("Verify with wrong PIN"); } catch (InvalidPinException e) { MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); @@ -132,17 +132,17 @@ public static void testPin(PivSession piv, PivTestState state) throws ApduExcept } logger.debug("Change PIN"); - piv.changePin(pin2, state.defaultPin); + piv.changePin(pin2, state.pin); } 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(state.defaultManagementKey); + piv.authenticate(state.managementKey); // Change PUK char[] puk2 = "12341234".toCharArray(); - piv.changePuk(state.defaultPuk, puk2); - piv.verifyPin(state.defaultPin); + piv.changePuk(state.puk, puk2); + piv.verifyPin(state.pin); // Block PIN while (piv.getPinAttempts() > 0) { @@ -155,7 +155,7 @@ public static void testPuk(PivSession piv, PivTestState state) throws ApduExcept // Verify PIN blocked try { - piv.verifyPin(state.defaultPin); + piv.verifyPin(state.pin); } catch (InvalidPinException e) { MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(0)); MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(0)); @@ -163,24 +163,24 @@ public static void testPuk(PivSession piv, PivTestState state) throws ApduExcept // Try unblock with wrong PUK try { - piv.unblockPin(state.defaultPuk, state.defaultPin); + piv.unblockPin(state.puk, state.pin); Assert.fail("Unblock with wrong PUK"); } catch (InvalidPinException e) { MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); } // Unblock PIN - piv.unblockPin(puk2, state.defaultPin); + piv.unblockPin(puk2, state.pin); // Try to change PUK with wrong PUK try { - piv.changePuk(state.defaultPuk, puk2); + piv.changePuk(state.puk, puk2); Assert.fail("Change PUK with wrong PUK"); } catch (InvalidPinException e) { MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); } // Change PUK - piv.changePuk(puk2, state.defaultPuk); + piv.changePuk(puk2, state.puk); } } 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 367db54a..650c956e 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 @@ -75,10 +75,10 @@ public static void testDecrypt(PivSession piv, PivTestState state, KeyType keyTy return; } - piv.authenticate(state.defaultManagementKey); + piv.authenticate(state.managementKey); logger.debug("Generate key: {}", keyType); KeyPairGenerator kpg = KeyPairGenerator.getInstance("YKPivRSA"); - kpg.initialize(new PivAlgorithmParameterSpec(Slot.KEY_MANAGEMENT, keyType, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, state.defaultPin)); + kpg.initialize(new PivAlgorithmParameterSpec(Slot.KEY_MANAGEMENT, keyType, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, state.pin)); 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 405ee18a..d03dea34 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 @@ -48,7 +48,7 @@ public class PivJcaDeviceTests { @SuppressWarnings("NewApi") // casting to Destroyable is supported from API 26 public static void testImportKeys(PivSession piv, PivTestState state) throws Exception { setupJca(piv); - piv.authenticate(state.defaultManagementKey); + piv.authenticate(state.managementKey); KeyStore keyStore = KeyStore.getInstance("YKPiv"); keyStore.load(null); @@ -68,7 +68,7 @@ public static void testImportKeys(PivSession piv, PivTestState state) throws Exc 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, state.defaultPin); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.pin); PivTestUtils.rsaEncryptAndDecrypt(privateKey, keyPair.getPublic()); PivTestUtils.rsaSignAndVerify(privateKey, keyPair.getPublic()); @@ -84,7 +84,7 @@ public static void testImportKeys(PivSession piv, PivTestState state) throws Exc 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, state.defaultPin); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.pin); PivTestUtils.ecKeyAgreement(privateKey, keyPair.getPublic()); PivTestUtils.ecSignAndVerify(privateKey, keyPair.getPublic()); @@ -108,7 +108,7 @@ public static void testImportKeys(PivSession piv, PivTestState state) throws Exc 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, state.defaultPin); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.pin); if (keyType == KeyType.X25519) { PivTestUtils.x25519KeyAgreement(privateKey, keyPair.getPublic()); @@ -147,7 +147,7 @@ public static void testGenerateKeysPreferBC(PivSession piv, PivTestState state) } private static void generateKeys(PivSession piv, PivTestState state) throws Exception { - piv.authenticate(state.defaultManagementKey); + piv.authenticate(state.managementKey); KeyPairGenerator ecGen = KeyPairGenerator.getInstance("YKPivEC"); for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384, KeyType.ED25519, KeyType.X25519)) { @@ -160,7 +160,7 @@ private static void generateKeys(PivSession piv, PivTestState state) throws Exce continue; } - ecGen.initialize(new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, state.defaultPin)); + ecGen.initialize(new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, state.pin)); KeyPair keyPair = ecGen.generateKeyPair(); if (keyType == KeyType.ED25519) { @@ -191,7 +191,7 @@ private static void generateKeys(PivSession piv, PivTestState state) throws Exce continue; } - rsaGen.initialize(new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, state.defaultPin)); + rsaGen.initialize(new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, state.pin)); 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 907736b3..2cb2f033 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 @@ -82,11 +82,11 @@ public static void testSign(PivSession piv, PivTestState state, KeyType keyType) logger.debug("Ignoring keyType: {}", keyType); return; } - piv.authenticate(state.defaultManagementKey); + piv.authenticate(state.managementKey); 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, state.defaultPin)); + kpg.initialize(new PivAlgorithmParameterSpec(Slot.SIGNATURE, keyType, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, state.pin)); 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 27fa79d6..432cdca1 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 @@ -59,7 +59,7 @@ static void moveKey(PivSession piv, PivTestState state) Slot srcSlot = Slot.RETIRED1; Slot dstSlot = Slot.RETIRED2; - piv.authenticate(state.defaultManagementKey); + piv.authenticate(state.managementKey); for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384, KeyType.RSA1024, KeyType.RSA2048, KeyType.ED25519, KeyType.X25519)) { @@ -87,7 +87,7 @@ static void moveKey(PivSession piv, PivTestState state) keyStore.load(null); PublicKey publicKey = keyPair.getPublic(); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(dstSlot.getStringAlias(), state.defaultPin); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(dstSlot.getStringAlias(), state.pin); 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 index f29d5dd8..39ed4d29 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java @@ -76,6 +76,6 @@ static void testPinComplexity(PivSession piv, PivTestState state) throws Throwab piv.verifyPin(complexPin); // the value of default PIN in the state is correct for pin complexity settings - piv.changePin(complexPin, state.defaultPin); + piv.changePin(complexPin, state.pin); } } 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 92a5b02c..a08b0548 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 @@ -56,9 +56,9 @@ public class PivTestState extends TestState { }; public final boolean isFipsApproved; - public char[] defaultPin; - public char[] defaultPuk; - public byte[] defaultManagementKey; + public char[] pin; + public char[] puk; + public byte[] managementKey; public static class Builder extends TestState.Builder { @@ -79,9 +79,9 @@ public PivTestState build() throws Throwable { protected PivTestState(Builder builder) throws Throwable { super(builder); - defaultPin = DEFAULT_PIN; - defaultPuk = DEFAULT_PUK; - defaultManagementKey = DEFAULT_MANAGEMENT_KEY; + pin = DEFAULT_PIN; + puk = DEFAULT_PUK; + managementKey = DEFAULT_MANAGEMENT_KEY; assumeTrue("No SmartCard support", currentDevice.supportsConnection(SmartCardConnection.class)); @@ -116,15 +116,15 @@ protected PivTestState(Builder builder) throws Throwable { 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.changePin(pin, COMPLEX_PIN); + pivSession.changePuk(puk, COMPLEX_PUK); + pivSession.authenticate(managementKey); pivSession.setManagementKey(ManagementKeyType.AES192, COMPLEX_MANAGEMENT_KEY, false); - defaultPin = COMPLEX_PIN; - defaultPuk = COMPLEX_PUK; - defaultManagementKey = COMPLEX_MANAGEMENT_KEY; + pin = COMPLEX_PIN; + puk = COMPLEX_PUK; + managementKey = COMPLEX_MANAGEMENT_KEY; } } From fa702d5b944a836f370352858d323cba8aeb7dce Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 9 Aug 2024 12:22:44 +0200 Subject: [PATCH 45/46] remove unnecessary public methods, update tests --- .../core/smartcard/scp/Scp11KeyParams.java | 9 --- .../yubikit/testing/sd/Scp11DeviceTests.java | 77 +++++++++++++------ 2 files changed, 54 insertions(+), 32 deletions(-) diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java index ec23197c..d79dcee2 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java @@ -70,13 +70,4 @@ public Scp11KeyParams(KeyRef keyRef, PublicKey pkSdEcka) { public KeyRef getKeyRef() { return keyRef; } - - public List getCertificates() { - return certificates; - } - - @Nullable - public KeyRef getOceKeyRef() { - return oceKeyRef; - } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java index 1d1049da..397fcbbd 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java @@ -64,6 +64,8 @@ public class Scp11DeviceTests { private static final ScpKeyParams defaultKeyParams = new Scp03KeyParams(new KeyRef((byte) 0x01, (byte) 0xff), StaticKeys.getDefaultKeys()); + private static final byte OCE_KID = 0x010; + public static void before(SecurityDomainTestState state) throws Throwable { assumeTrue("Device does not support SCP11a", state.getDeviceInfo().getVersion().isAtLeast(5, 7, 2)); @@ -86,18 +88,31 @@ public static void testScp11aAuthenticate(SecurityDomainTestState state) throws public static void testScp11aAllowList(SecurityDomainTestState state) throws Throwable { final byte kvn = 0x05; + final KeyRef oceKeyRef = new KeyRef(OCE_KID, kvn); ScpKeyParams keyParams = state.withSecurityDomain(defaultKeyParams, session -> { - Scp11KeyParams params = loadKeys(session, ScpKid.SCP11a, kvn); - assertNotNull(params.getOceKeyRef()); + return loadKeys(session, ScpKid.SCP11a, kvn); + }); - List serials = new ArrayList<>(); - for (X509Certificate cert : params.getCertificates()) { - serials.add(cert.getSerialNumber()); - } + state.withSecurityDomain(keyParams, session -> { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream is = new ByteArrayInputStream(OCE)) { + keyStore.load(is, OCE_PASSWORD); - session.storeAllowlist(params.getOceKeyRef(), serials); - return params; + ScpCertificates certs = getCertificates(keyStore); + + List certChain = new ArrayList<>(certs.bundle); + if (certs.leaf != null) { + certChain.add(certs.leaf); + } + + List serials = new ArrayList<>(); + for (X509Certificate cert : certChain) { + serials.add(cert.getSerialNumber()); + } + + session.storeAllowlist(oceKeyRef, serials); + } }); state.withSecurityDomain(keyParams, session -> { @@ -107,6 +122,7 @@ public static void testScp11aAllowList(SecurityDomainTestState state) throws Thr public static void testScp11aAllowListBlocked(SecurityDomainTestState state) throws Throwable { final byte kvn = 0x03; + final KeyRef oceKeyRef = new KeyRef(OCE_KID, kvn); ScpKeyParams scp03KeyParams = importScp03Key(state); @@ -115,13 +131,12 @@ public static void testScp11aAllowListBlocked(SecurityDomainTestState state) thr session.deleteKey(new KeyRef(ScpKid.SCP11b, (byte) 1), false); Scp11KeyParams scp11KeyParams = loadKeys(session, ScpKid.SCP11a, kvn); - assertNotNull(scp11KeyParams.getOceKeyRef()); final List serials = Arrays.asList( BigInteger.valueOf(1), BigInteger.valueOf(2), BigInteger.valueOf(3), BigInteger.valueOf(4), BigInteger.valueOf(5)); - session.storeAllowlist(scp11KeyParams.getOceKeyRef(), serials); + session.storeAllowlist(oceKeyRef, serials); return scp11KeyParams; }); @@ -133,8 +148,7 @@ public static void testScp11aAllowListBlocked(SecurityDomainTestState state) thr // reset the allow list state.withSecurityDomain(scp03KeyParams, session -> { - assertNotNull(keyParams.getOceKeyRef()); - session.storeAllowlist(keyParams.getOceKeyRef(), new ArrayList<>()); + session.storeAllowlist(oceKeyRef, new ArrayList<>()); }); // authenticate with scp11a will not throw @@ -270,7 +284,7 @@ private static List getCertificateChain(KeyStore keyStore, Stri private static Scp11KeyParams loadKeys(SecurityDomainSession session, byte kid, byte kvn) throws Throwable { KeyRef sessionRef = new KeyRef(kid, kvn); - KeyRef oceRef = new KeyRef((byte) 0x10, kvn); + KeyRef oceRef = new KeyRef(OCE_KID, kvn); PublicKeyValues publicKeyValues = session.generateEcKey(sessionRef, 0); @@ -287,15 +301,8 @@ private static Scp11KeyParams loadKeys(SecurityDomainSession session, byte kid, try (InputStream is = new ByteArrayInputStream(OCE)) { keyStore.load(is, OCE_PASSWORD); - final Enumeration aliases = keyStore.aliases(); - assertTrue(aliases.hasMoreElements()); - String alias = keyStore.aliases().nextElement(); - assertTrue(keyStore.isKeyEntry(alias)); - - Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_PASSWORD); - assertTrue("No private key in pkcs12", sk instanceof PrivateKey); - - ScpCertificates certs = ScpCertificates.from(getCertificateChain(keyStore, alias)); + PrivateKey sk = getPrivateKey(keyStore); + ScpCertificates certs = getCertificates(keyStore); List certChain = new ArrayList<>(certs.bundle); if (certs.leaf != null) { @@ -306,9 +313,33 @@ private static Scp11KeyParams loadKeys(SecurityDomainSession session, byte kid, sessionRef, publicKeyValues.toPublicKey(), oceRef, - (PrivateKey) sk, + sk, certChain ); } } + + static PrivateKey getPrivateKey(KeyStore keyStore) throws Throwable { + final Enumeration aliases = keyStore.aliases(); + assertTrue(aliases.hasMoreElements()); + String alias = keyStore.aliases().nextElement(); + assertTrue(keyStore.isKeyEntry(alias)); + + Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_PASSWORD); + assertTrue("No private key in pkcs12", sk instanceof PrivateKey); + + return (PrivateKey) sk; + } + + static ScpCertificates getCertificates(KeyStore keyStore) throws Throwable { + final Enumeration aliases = keyStore.aliases(); + assertTrue(aliases.hasMoreElements()); + String alias = keyStore.aliases().nextElement(); + assertTrue(keyStore.isKeyEntry(alias)); + + Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_PASSWORD); + assertTrue("No private key in pkcs12", sk instanceof PrivateKey); + + return ScpCertificates.from(getCertificateChain(keyStore, alias)); + } } From f6e7c172822b285bec34b8c035f3d4275a4ba963 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 9 Aug 2024 12:26:48 +0200 Subject: [PATCH 46/46] simplify storeAllowlist test --- .../yubikit/testing/sd/Scp11DeviceTests.java | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java index 397fcbbd..65d35ac1 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java @@ -95,24 +95,12 @@ public static void testScp11aAllowList(SecurityDomainTestState state) throws Thr }); state.withSecurityDomain(keyParams, session -> { - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - try (InputStream is = new ByteArrayInputStream(OCE)) { - keyStore.load(is, OCE_PASSWORD); - - ScpCertificates certs = getCertificates(keyStore); - - List certChain = new ArrayList<>(certs.bundle); - if (certs.leaf != null) { - certChain.add(certs.leaf); - } - - List serials = new ArrayList<>(); - for (X509Certificate cert : certChain) { - serials.add(cert.getSerialNumber()); - } - - session.storeAllowlist(oceKeyRef, serials); - } + final List serials = Arrays.asList( + // serial numbers from OCE + new BigInteger("7f4971b0ad51f84c9da9928b2d5fef5e16b2920a", 16), + new BigInteger("6b90028800909f9ffcd641346933242748fbe9ad", 16) + ); + session.storeAllowlist(oceKeyRef, serials); }); state.withSecurityDomain(keyParams, session -> {