Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build-pull-requests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:

build-artifacts:
name: Build artifacts
permissions:
contents: read
packages: read
runs-on: windows-latest
needs: run-tests

Expand Down
33 changes: 30 additions & 3 deletions Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using Microsoft.Extensions.Logging;
using Yubico.Core.Logging;
using Yubico.YubiKey.Fido2.Commands;
using Yubico.YubiKey.Scp;

namespace Yubico.YubiKey.Fido2
{
Expand Down Expand Up @@ -218,6 +219,7 @@ public ReadOnlyMemory<byte>? AuthenticatorCredStoreState
/// YubiKey.
/// </summary>
/// <remarks>
/// <para>
/// Because this class implements <c>IDisposable</c>, use the <c>using</c> keyword. For example,
/// <code language="csharp">
/// IYubiKeyDevice yubiKeyToUse = SelectYubiKey();
Expand All @@ -226,17 +228,42 @@ public ReadOnlyMemory<byte>? AuthenticatorCredStoreState
/// /* Perform FIDO2 operations. */
/// }
/// </code>
/// </para>
/// <para>
/// To establish an SCP-protected FIDO2 session:
/// <code language="csharp">
/// using (var fido2 = new Fido2Session(yubiKeyToUse, keyParameters: Scp03KeyParameters.DefaultKey))
/// {
/// /* All FIDO2 commands are encrypted via SCP. */
/// }
/// </code>
/// </para>
/// <para>
/// <b>Transport notes for FIDO2 over SCP:</b> On YubiKey firmware 5.8 and later, FIDO2 is
/// available over both HID and USB CCID (SmartCard), so SCP works over USB as well as NFC.
/// On earlier firmware, FIDO2 communicates via HID only over USB, which does not support SCP
/// (a SmartCard-layer protocol). Over NFC, all firmware versions expose FIDO2 via SmartCard.
/// </para>
/// </remarks>
/// <param name="yubiKey">
/// The object that represents the actual YubiKey on which the FIDO2 operations should be performed.
/// </param>
/// <param name="persistentPinUvAuthToken">If supplied, will be used for credential management read-only operations
/// <param name="persistentPinUvAuthToken">If supplied, will be used for credential management read-only operations.
/// </param>
/// <param name="keyParameters">
/// Optional parameters for establishing a Secure Channel Protocol (SCP) connection.
/// When provided, all communication with the YubiKey will be encrypted and authenticated
/// using the specified SCP protocol (e.g., SCP03 or SCP11). On firmware prior to 5.8, this
/// requires an NFC connection. On firmware 5.8+, SCP is also supported over USB.
/// </param>
/// <exception cref="ArgumentNullException">
/// The <paramref name="yubiKey"/> argument is <c>null</c>.
/// </exception>
public Fido2Session(IYubiKeyDevice yubiKey, ReadOnlyMemory<byte>? persistentPinUvAuthToken = null)
: base(Log.GetLogger<Fido2Session>(), yubiKey, YubiKeyApplication.Fido2, keyParameters: null)
public Fido2Session(
IYubiKeyDevice yubiKey,
ReadOnlyMemory<byte>? persistentPinUvAuthToken = null,
ScpKeyParameters? keyParameters = null)
: base(Log.GetLogger<Fido2Session>(), yubiKey, YubiKeyApplication.Fido2, keyParameters)
{
Guard.IsNotNull(yubiKey, nameof(yubiKey));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ public static bool HasFeature(this IYubiKeyDevice yubiKeyDevice, YubiKeyFeature
|| HasApplication(yubiKeyDevice, YubiKeyCapabilities.Oath)
|| HasApplication(yubiKeyDevice, YubiKeyCapabilities.OpenPgp)
|| HasApplication(yubiKeyDevice, YubiKeyCapabilities.Otp)
|| HasApplication(yubiKeyDevice, YubiKeyCapabilities.YubiHsmAuth)),
|| HasApplication(yubiKeyDevice, YubiKeyCapabilities.YubiHsmAuth)
|| HasApplication(yubiKeyDevice, YubiKeyCapabilities.Fido2)),

YubiKeyFeature.Scp03Oath =>
yubiKeyDevice.FirmwareVersion >= FirmwareVersion.V5_6_3
Expand Down
150 changes: 150 additions & 0 deletions Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Xunit;
using Yubico.Core.Tlv;
using Yubico.YubiKey.Cryptography;
using Yubico.YubiKey.Fido2;
using Yubico.YubiKey.Piv;
using Yubico.YubiKey.Piv.Commands;
using Yubico.YubiKey.Scp03;
Expand All @@ -31,6 +32,28 @@ namespace Yubico.YubiKey.Scp
public class Scp03Tests
{
private readonly ReadOnlyMemory<byte> _defaultPin = new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 };
private readonly ReadOnlyMemory<byte> _fido2Pin = "11234567"u8.ToArray();

private bool Fido2KeyCollector(KeyEntryData data)
{
if (data.Request == KeyEntryRequest.Release)
{
return true;
}

if (data.Request == KeyEntryRequest.TouchRequest)
{
return true;
}

if (data.Request is KeyEntryRequest.VerifyFido2Pin or KeyEntryRequest.SetFido2Pin)
{
data.SubmitValue(_fido2Pin.Span);
return true;
}

return false;
}

public Scp03Tests()
{
Expand Down Expand Up @@ -404,6 +427,133 @@ public void Scp03_PivSession_TryVerifyPinAndGetMetaData_Succeeds(
}


[SkippableTheory(typeof(DeviceNotFoundException))]
[InlineData(StandardTestDevice.Fw5, Transport.NfcSmartCard)]
[InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)]
public void Scp03_Fido2Session_GetAuthenticatorInfo_Succeeds(
StandardTestDevice desiredDeviceType,
Transport transport)
{
var testDevice = GetDevice(desiredDeviceType, transport);
Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0);
Assert.True(testDevice.HasFeature(YubiKeyFeature.Scp03));

// FIDO2 over CCID requires firmware 5.8+. Over NFC, all applets are
// selectable via SmartCard. Over USB, FIDO2 is available on CCID
// starting with firmware 5.8; older keys only expose FIDO2 over HID.
if (transport == Transport.UsbSmartCard)
{
Skip.IfNot(
testDevice.FirmwareVersion >= FirmwareVersion.V5_8_0,
"FIDO2 over USB CCID requires firmware 5.8+");
}
else
{
Skip.IfNot(
testDevice.AvailableNfcCapabilities.HasFlag(YubiKeyCapabilities.Fido2),
"FIDO2 is not available over NFC on this device");
}

using var fido2Session = new Fido2Session(testDevice, keyParameters: Scp03KeyParameters.DefaultKey);

var info = fido2Session.AuthenticatorInfo;
Assert.NotNull(info);
Assert.NotEmpty(info.Versions);
}

[SkippableTheory(typeof(DeviceNotFoundException))]
[InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)]
public void Scp03_Fido2Session_MakeCredential_Over_UsbCcid_Succeeds(
StandardTestDevice desiredDeviceType,
Transport transport)
{
var testDevice = GetDevice(desiredDeviceType, transport);
Assert.True(testDevice.HasFeature(YubiKeyFeature.Scp03));

Skip.IfNot(
testDevice.FirmwareVersion >= FirmwareVersion.V5_8_0,
"FIDO2 over USB CCID requires firmware 5.8+");

using var fido2Session = new Fido2Session(testDevice, keyParameters: Scp03KeyParameters.DefaultKey);
Assert.Equal("ScpConnection", fido2Session.Connection.GetType().Name);

fido2Session.KeyCollector = Fido2KeyCollector;

// Ensure PIN is set and verify it
var pinOption = fido2Session.AuthenticatorInfo.GetOptionValue(AuthenticatorOptions.clientPin);
if (pinOption == OptionValue.False)
{
fido2Session.TrySetPin(_fido2Pin);
}
else if (fido2Session.AuthenticatorInfo.ForcePinChange == true)
{
Skip.If(true, "Key requires PIN change — cannot test MakeCredential in this state");
}

bool verified;
try
{
verified = fido2Session.TryVerifyPin(
_fido2Pin,
permissions: null,
relyingPartyId: null,
retriesRemaining: out _,
rebootRequired: out _);
}
catch (Fido2.Fido2Exception)
{
verified = false;
}

Skip.IfNot(verified, "PIN verification failed — key may have a different PIN set. Reset FIDO2 app to use default test PIN.");

// MakeCredential — requires touch
var rp = new RelyingParty("scp03-ccid-test.yubico.com");
var userId = new UserEntity(new byte[] { 0x01, 0x02, 0x03 })
{
Name = "scp03-ccid-test",
DisplayName = "SCP03 CCID Test"
};

var mcParams = new MakeCredentialParameters(rp, userId)
{
ClientDataHash = new byte[]
{
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38
}
};

var mcData = fido2Session.MakeCredential(mcParams);
Assert.True(mcData.VerifyAttestation(mcParams.ClientDataHash));
}

[SkippableTheory(typeof(DeviceNotFoundException))]
[InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)]
public void Scp03_Fido2Session_Pre58_UsbCcid_Skips_Gracefully(
StandardTestDevice desiredDeviceType,
Transport transport)
{
var testDevice = GetDevice(desiredDeviceType, transport);

if (testDevice.FirmwareVersion >= FirmwareVersion.V5_8_0)
{
// On 5.8+, FIDO2 over CCID should work — verify it does
using var session = new Fido2Session(testDevice, keyParameters: Scp03KeyParameters.DefaultKey);
Assert.NotNull(session.AuthenticatorInfo);
}
else
{
// On pre-5.8, FIDO2 AID SELECT over CCID should fail with ApduException (0x6A82)
Assert.ThrowsAny<Exception>(() =>
{
using var session = new Fido2Session(testDevice, keyParameters: Scp03KeyParameters.DefaultKey);
});
}
}

[SkippableTheory(typeof(DeviceNotFoundException))]
[InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)]
[InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)]
Expand Down
38 changes: 38 additions & 0 deletions Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using Yubico.Core.Devices.Hid;
using Yubico.Core.Tlv;
using Yubico.YubiKey.Cryptography;
using Yubico.YubiKey.Fido2;
using Yubico.YubiKey.Oath;
using Yubico.YubiKey.Otp;
using Yubico.YubiKey.Piv;
Expand Down Expand Up @@ -131,6 +132,43 @@ public void Scp11b_App_OtpSession_Operations_Succeeds(
configObj.Execute();
}

[SkippableTheory(typeof(DeviceNotFoundException))]
[InlineData(StandardTestDevice.Fw5, Transport.NfcSmartCard)]
[InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)]
[InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)]
[InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)]
public void Scp11b_App_Fido2Session_GetAuthenticatorInfo_Succeeds(
StandardTestDevice desiredDeviceType,
Transport transport)
{
var testDevice = GetDevice(desiredDeviceType, transport);

// FIDO2 over CCID requires firmware 5.8+. Over NFC, all applets are
// selectable via SmartCard. Over USB, FIDO2 is available on CCID
// starting with firmware 5.8; older keys only expose FIDO2 over HID.
if (transport == Transport.UsbSmartCard)
{
Skip.IfNot(
testDevice.FirmwareVersion >= FirmwareVersion.V5_8_0,
"FIDO2 over USB CCID requires firmware 5.8+");
}
else
{
Skip.IfNot(
testDevice.AvailableNfcCapabilities.HasFlag(YubiKeyCapabilities.Fido2),
"FIDO2 is not available over NFC on this device");
}

var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1);
var keyParams = Get_Scp11b_SecureConnection_Parameters(testDevice, keyReference);

using var session = new Fido2Session(testDevice, keyParameters: keyParams);

var info = session.AuthenticatorInfo;
Assert.NotNull(info);
Assert.NotEmpty(info.Versions);
}

[SkippableTheory(typeof(DeviceNotFoundException))]
[InlineData(StandardTestDevice.Fw5)]
[InlineData(StandardTestDevice.Fw5Fips)]
Expand Down
Loading
Loading