From fa3a480d427f6fd3208229731d89910030333ee7 Mon Sep 17 00:00:00 2001 From: Dennis Dyallo Date: Tue, 17 Mar 2026 22:05:14 +0100 Subject: [PATCH 1/7] feat: Add FIDO2 session support for SCP03 and SCP11 secure channels Enable Fido2Session to accept ScpKeyParameters for encrypted communication over NFC. Add FIDO2 to the SCP03 feature gate, integration tests for both SCP03 and SCP11b with NFC transport, and skip conditions for devices that don't expose FIDO2 over SmartCard. Co-Authored-By: Claude Opus 4.6 --- .../src/Yubico/YubiKey/Fido2/Fido2Session.cs | 13 +++++++-- .../YubiKey/YubiKeyFeatureExtensions.cs | 3 ++- .../YubiKey/Fido2/FidoIntegrationTestBase.cs | 2 +- .../Yubico/YubiKey/Scp/Scp03Tests.cs | 27 +++++++++++++++++++ .../Yubico/YubiKey/Scp/Scp11Tests.cs | 25 +++++++++++++++++ 5 files changed, 66 insertions(+), 4 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs index 8926c3c95..9e551fcbb 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs @@ -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 { @@ -230,13 +231,21 @@ public ReadOnlyMemory? AuthenticatorCredStoreState /// /// The object that represents the actual YubiKey on which the FIDO2 operations should be performed. /// + /// + /// 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). + /// /// If supplied, will be used for credential management read-only operations /// /// /// The argument is null. /// - public Fido2Session(IYubiKeyDevice yubiKey, ReadOnlyMemory? persistentPinUvAuthToken = null) - : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Fido2, keyParameters: null) + public Fido2Session( + IYubiKeyDevice yubiKey, + ScpKeyParameters? keyParameters = null, + ReadOnlyMemory? persistentPinUvAuthToken = null) + : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Fido2, keyParameters) { Guard.IsNotNull(yubiKey, nameof(yubiKey)); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs index 021161bdd..b6593062d 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs @@ -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 diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Fido2/FidoIntegrationTestBase.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Fido2/FidoIntegrationTestBase.cs index 8642a716e..911d19c27 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Fido2/FidoIntegrationTestBase.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Fido2/FidoIntegrationTestBase.cs @@ -111,7 +111,7 @@ protected Fido2Session GetSession( { session = persistentPinUvAuthToken is null ? new Fido2Session(testDevice) - : new Fido2Session(testDevice, persistentPinUvAuthToken); + : new Fido2Session(testDevice, persistentPinUvAuthToken: persistentPinUvAuthToken); session.KeyCollector = KeyCollector.HandleRequest; diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs index 2161b0362..9b565a771 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -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; @@ -404,6 +405,32 @@ public void Scp03_PivSession_TryVerifyPinAndGetMetaData_Succeeds( } + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.NfcSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + 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 SCP requires NFC (SmartCard protocol). Over USB, FIDO2 uses HID + // and the FIDO2 AID is not selectable over CCID on most YubiKey devices. + // This matches the Android SDK behavior where FIDO2+SCP tests run over NFC. + Skip.IfNot( + testDevice.AvailableUsbCapabilities.HasFlag(YubiKeyCapabilities.Fido2) + || transport == Transport.NfcSmartCard, + "FIDO2 is not available over SmartCard on this device"); + + using var fido2Session = new Fido2Session(testDevice, Scp03KeyParameters.DefaultKey); + + var info = fido2Session.AuthenticatorInfo; + Assert.NotNull(info); + Assert.NotEmpty(info.Versions); + } + [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs index 3d9af2c89..0ad020a07 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -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; @@ -131,6 +132,30 @@ public void Scp11b_App_OtpSession_Operations_Succeeds( configObj.Execute(); } + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_App_Fido2Session_GetAuthenticatorInfo_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + + // FIDO2 over SCP requires SmartCard protocol (NFC). Over USB, FIDO2 uses HID + // and the FIDO2 AID is not selectable over CCID on most YubiKey devices. + Skip.IfNot( + testDevice.AvailableUsbCapabilities.HasFlag(YubiKeyCapabilities.Fido2), + "FIDO2 is not available over SmartCard on this device"); + + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + var keyParams = Get_Scp11b_SecureConnection_Parameters(testDevice, keyReference); + + using var session = new Fido2Session(testDevice, keyParams); + + var info = session.AuthenticatorInfo; + Assert.NotNull(info); + Assert.NotEmpty(info.Versions); + } + [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5)] [InlineData(StandardTestDevice.Fw5Fips)] From 35346736e99257f87836d20faadf4e69b2a9bbae Mon Sep 17 00:00:00 2001 From: Dennis Dyallo Date: Tue, 17 Mar 2026 22:30:48 +0100 Subject: [PATCH 2/7] docs: Add NFC requirement and SCP usage example to Fido2Session Document that FIDO2 over SCP requires NFC since USB FIDO2 uses HID which is incompatible with SCP's SmartCard-layer protocol. Co-Authored-By: Claude Opus 4.6 --- .../src/Yubico/YubiKey/Fido2/Fido2Session.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs index 9e551fcbb..9c0108901 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs @@ -219,6 +219,7 @@ public ReadOnlyMemory? AuthenticatorCredStoreState /// YubiKey. /// /// + /// /// Because this class implements IDisposable, use the using keyword. For example, /// /// IYubiKeyDevice yubiKeyToUse = SelectYubiKey(); @@ -227,6 +228,22 @@ public ReadOnlyMemory? AuthenticatorCredStoreState /// /* Perform FIDO2 operations. */ /// } /// + /// + /// + /// To establish an SCP-protected FIDO2 session: + /// + /// using (var fido2 = new Fido2Session(yubiKeyToUse, Scp03KeyParameters.DefaultKey)) + /// { + /// /* All FIDO2 commands are encrypted via SCP. */ + /// } + /// + /// + /// + /// Important: FIDO2 over SCP requires an NFC connection. Over USB, FIDO2 communicates + /// via the HID interface, which does not support SCP (a SmartCard-layer protocol). Over NFC, + /// all communication uses the SmartCard protocol, so both FIDO2 and SCP are available on the + /// same interface. + /// /// /// /// The object that represents the actual YubiKey on which the FIDO2 operations should be performed. @@ -234,7 +251,8 @@ public ReadOnlyMemory? AuthenticatorCredStoreState /// /// 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). + /// using the specified SCP protocol (e.g., SCP03 or SCP11). Requires an NFC connection — + /// FIDO2 over SCP is not supported over USB. /// /// If supplied, will be used for credential management read-only operations /// From 85b6320ae648db3d91e61d1f70abe897ad75ef9d Mon Sep 17 00:00:00 2001 From: Dennis Dyallo Date: Tue, 17 Mar 2026 23:00:11 +0100 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20Address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?reorder=20constructor=20params=20and=20fix=20FIDO2=20SCP=20test?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move keyParameters to last position in Fido2Session constructor to preserve backwards compatibility with existing (device, token) callers. Fix FIDO2 SCP tests: force NFC transport, remove Fw5Fips+NFC conflict with GetDevice assertion, check AvailableNfcCapabilities instead of AvailableUsbCapabilities. Co-Authored-By: Claude Opus 4.6 --- .../src/Yubico/YubiKey/Fido2/Fido2Session.cs | 8 ++++---- .../Yubico/YubiKey/Fido2/FidoIntegrationTestBase.cs | 2 +- .../tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs | 9 +++------ .../tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs | 10 +++++----- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs index 9c0108901..5d2f4a6fd 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs @@ -248,21 +248,21 @@ public ReadOnlyMemory? AuthenticatorCredStoreState /// /// The object that represents the actual YubiKey on which the FIDO2 operations should be performed. /// + /// If supplied, will be used for credential management read-only operations. + /// /// /// 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). Requires an NFC connection — /// FIDO2 over SCP is not supported over USB. /// - /// If supplied, will be used for credential management read-only operations - /// /// /// The argument is null. /// public Fido2Session( IYubiKeyDevice yubiKey, - ScpKeyParameters? keyParameters = null, - ReadOnlyMemory? persistentPinUvAuthToken = null) + ReadOnlyMemory? persistentPinUvAuthToken = null, + ScpKeyParameters? keyParameters = null) : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Fido2, keyParameters) { Guard.IsNotNull(yubiKey, nameof(yubiKey)); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Fido2/FidoIntegrationTestBase.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Fido2/FidoIntegrationTestBase.cs index 911d19c27..8642a716e 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Fido2/FidoIntegrationTestBase.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Fido2/FidoIntegrationTestBase.cs @@ -111,7 +111,7 @@ protected Fido2Session GetSession( { session = persistentPinUvAuthToken is null ? new Fido2Session(testDevice) - : new Fido2Session(testDevice, persistentPinUvAuthToken: persistentPinUvAuthToken); + : new Fido2Session(testDevice, persistentPinUvAuthToken); session.KeyCollector = KeyCollector.HandleRequest; diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs index 9b565a771..0f411be70 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -407,7 +407,6 @@ public void Scp03_PivSession_TryVerifyPinAndGetMetaData_Succeeds( [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5, Transport.NfcSmartCard)] - [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] public void Scp03_Fido2Session_GetAuthenticatorInfo_Succeeds( StandardTestDevice desiredDeviceType, Transport transport) @@ -418,13 +417,11 @@ public void Scp03_Fido2Session_GetAuthenticatorInfo_Succeeds( // FIDO2 over SCP requires NFC (SmartCard protocol). Over USB, FIDO2 uses HID // and the FIDO2 AID is not selectable over CCID on most YubiKey devices. - // This matches the Android SDK behavior where FIDO2+SCP tests run over NFC. Skip.IfNot( - testDevice.AvailableUsbCapabilities.HasFlag(YubiKeyCapabilities.Fido2) - || transport == Transport.NfcSmartCard, - "FIDO2 is not available over SmartCard on this device"); + testDevice.AvailableNfcCapabilities.HasFlag(YubiKeyCapabilities.Fido2), + "FIDO2 is not available over NFC on this device"); - using var fido2Session = new Fido2Session(testDevice, Scp03KeyParameters.DefaultKey); + using var fido2Session = new Fido2Session(testDevice, keyParameters: Scp03KeyParameters.DefaultKey); var info = fido2Session.AuthenticatorInfo; Assert.NotNull(info); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs index 0ad020a07..e64a4b4e9 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -138,18 +138,18 @@ public void Scp11b_App_OtpSession_Operations_Succeeds( public void Scp11b_App_Fido2Session_GetAuthenticatorInfo_Succeeds( StandardTestDevice desiredDeviceType) { - var testDevice = GetDevice(desiredDeviceType); + var testDevice = GetDevice(desiredDeviceType, Transport.NfcSmartCard); - // FIDO2 over SCP requires SmartCard protocol (NFC). Over USB, FIDO2 uses HID + // FIDO2 over SCP requires NFC (SmartCard protocol). Over USB, FIDO2 uses HID // and the FIDO2 AID is not selectable over CCID on most YubiKey devices. Skip.IfNot( - testDevice.AvailableUsbCapabilities.HasFlag(YubiKeyCapabilities.Fido2), - "FIDO2 is not available over SmartCard on this device"); + 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, keyParams); + using var session = new Fido2Session(testDevice, keyParameters: keyParams); var info = session.AuthenticatorInfo; Assert.NotNull(info); From f75234f6acc1d1322b13dab4d4be5f34cb128386 Mon Sep 17 00:00:00 2001 From: Dennis Dyallo Date: Tue, 17 Mar 2026 23:12:27 +0100 Subject: [PATCH 4/7] fix: Add permissions for contents and packages in build-artifacts job --- .github/workflows/build-pull-requests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-pull-requests.yml b/.github/workflows/build-pull-requests.yml index 6ece89849..ce25e1b82 100644 --- a/.github/workflows/build-pull-requests.yml +++ b/.github/workflows/build-pull-requests.yml @@ -46,6 +46,9 @@ jobs: build-artifacts: name: Build artifacts + permissions: + contents: read + packages: read runs-on: windows-latest needs: run-tests From 39c21177e5c3c7cf4c9671bccc00ecac082485cc Mon Sep 17 00:00:00 2001 From: Dennis Dyallo Date: Tue, 17 Mar 2026 23:13:52 +0100 Subject: [PATCH 5/7] docs: fix positioning in docs Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs index 5d2f4a6fd..ff0774581 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs @@ -232,7 +232,7 @@ public ReadOnlyMemory? AuthenticatorCredStoreState /// /// To establish an SCP-protected FIDO2 session: /// - /// using (var fido2 = new Fido2Session(yubiKeyToUse, Scp03KeyParameters.DefaultKey)) + /// using (var fido2 = new Fido2Session(yubiKeyToUse, keyParameters: Scp03KeyParameters.DefaultKey)) /// { /// /* All FIDO2 commands are encrypted via SCP. */ /// } From 4e7b1df072f2e373590936c9768b6ecbe4f60656 Mon Sep 17 00:00:00 2001 From: Dennis Dyallo Date: Wed, 18 Mar 2026 16:47:36 +0100 Subject: [PATCH 6/7] feat: Add FIDO2 over USB CCID support for SCP on firmware 5.8+ FIDO2+SCP now works over USB CCID on firmware 5.8+ in addition to NFC. Updates docs, integration tests (SCP03 MakeCredential, SCP11b), and adds sandbox probe plugin for USB CCID testing. Pre-5.8 keys gracefully skip with firmware version checks. Co-Authored-By: Claude Opus 4.6 --- .../src/Yubico/YubiKey/Fido2/Fido2Session.cs | 12 +- .../Yubico/YubiKey/Scp/Scp03Tests.cs | 136 ++++- .../Yubico/YubiKey/Scp/Scp11Tests.cs | 31 +- .../sandbox/Plugins/Fido2CcidProbePlugin.cs | 151 +++++ Yubico.YubiKey/tests/sandbox/Program.cs | 1 + docs/findings-and-assumptions.md | 244 ++++++++ docs/rust-to-netsdk-ffi.md | 545 ++++++++++++++++++ 7 files changed, 1100 insertions(+), 20 deletions(-) create mode 100644 Yubico.YubiKey/tests/sandbox/Plugins/Fido2CcidProbePlugin.cs create mode 100644 docs/findings-and-assumptions.md create mode 100644 docs/rust-to-netsdk-ffi.md diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs index ff0774581..5055fe1a0 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs @@ -239,10 +239,10 @@ public ReadOnlyMemory? AuthenticatorCredStoreState /// /// /// - /// Important: FIDO2 over SCP requires an NFC connection. Over USB, FIDO2 communicates - /// via the HID interface, which does not support SCP (a SmartCard-layer protocol). Over NFC, - /// all communication uses the SmartCard protocol, so both FIDO2 and SCP are available on the - /// same interface. + /// Transport notes for FIDO2 over SCP: 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. /// /// /// @@ -253,8 +253,8 @@ public ReadOnlyMemory? AuthenticatorCredStoreState /// /// 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). Requires an NFC connection — - /// FIDO2 over SCP is not supported over USB. + /// 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. /// /// /// The argument is null. diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs index 0f411be70..f29464a21 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -32,6 +32,28 @@ namespace Yubico.YubiKey.Scp public class Scp03Tests { private readonly ReadOnlyMemory _defaultPin = new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }; + private readonly ReadOnlyMemory _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() { @@ -407,6 +429,7 @@ 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) @@ -415,11 +438,21 @@ public void Scp03_Fido2Session_GetAuthenticatorInfo_Succeeds( Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); Assert.True(testDevice.HasFeature(YubiKeyFeature.Scp03)); - // FIDO2 over SCP requires NFC (SmartCard protocol). Over USB, FIDO2 uses HID - // and the FIDO2 AID is not selectable over CCID on most YubiKey devices. - Skip.IfNot( - testDevice.AvailableNfcCapabilities.HasFlag(YubiKeyCapabilities.Fido2), - "FIDO2 is not available over NFC on this device"); + // 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); @@ -428,6 +461,99 @@ public void Scp03_Fido2Session_GetAuthenticatorInfo_Succeeds( 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(() => + { + using var session = new Fido2Session(testDevice, keyParameters: Scp03KeyParameters.DefaultKey); + }); + } + } + [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs index e64a4b4e9..72830e0c4 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -133,18 +133,31 @@ public void Scp11b_App_OtpSession_Operations_Succeeds( } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] + [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) + StandardTestDevice desiredDeviceType, + Transport transport) { - var testDevice = GetDevice(desiredDeviceType, Transport.NfcSmartCard); + var testDevice = GetDevice(desiredDeviceType, transport); - // FIDO2 over SCP requires NFC (SmartCard protocol). Over USB, FIDO2 uses HID - // and the FIDO2 AID is not selectable over CCID on most YubiKey devices. - Skip.IfNot( - testDevice.AvailableNfcCapabilities.HasFlag(YubiKeyCapabilities.Fido2), - "FIDO2 is not available over NFC on this device"); + // 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); diff --git a/Yubico.YubiKey/tests/sandbox/Plugins/Fido2CcidProbePlugin.cs b/Yubico.YubiKey/tests/sandbox/Plugins/Fido2CcidProbePlugin.cs new file mode 100644 index 000000000..7e2e81caa --- /dev/null +++ b/Yubico.YubiKey/tests/sandbox/Plugins/Fido2CcidProbePlugin.cs @@ -0,0 +1,151 @@ +// Quick probe: test FIDO2 AID selection over USB CCID (SmartCard) on 5.8+ keys +// Tests: plain CCID, SCP03, and SCP11b — all over USB SmartCard +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Yubico.Core.Devices.SmartCard; +using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.DeviceExtensions; +using Yubico.YubiKey.Fido2; +using Yubico.YubiKey.Fido2.Commands; +using Yubico.YubiKey.Scp; + +namespace Yubico.YubiKey.TestApp.Plugins +{ + internal class Fido2CcidProbePlugin : PluginBase + { + public override string Name => "Fido2CcidProbe"; + public override string Description => "Probes FIDO2 over USB CCID (SmartCard) on 5.8+ keys — SCP03 and SCP11b"; + + public Fido2CcidProbePlugin(IOutput output) : base(output) + { + Parameters["command"].Description = "[serial] Serial number of the YubiKey to test (e.g. 125)"; + } + + public override bool Execute() + { + int? targetSerial = string.IsNullOrEmpty(Command) ? null : int.Parse(Command); + + Output.WriteLine("=== FIDO2 over USB CCID Probe (SCP03 + SCP11b) ==="); + Output.WriteLine(); + + var allKeys = YubiKeyDevice.FindAll(); + Output.WriteLine($"Found {allKeys.Count()} YubiKey(s) total"); + + foreach (var key in allKeys) + { + Output.WriteLine($" Serial: {key.SerialNumber}, FW: {key.FirmwareVersion}, Transports: {key.AvailableTransports}"); + Output.WriteLine($" USB Capabilities: {key.AvailableUsbCapabilities}"); + Output.WriteLine($" HasSmartCard: {((YubiKeyDevice)key).HasSmartCard}, IsNfc: {((YubiKeyDevice)key).IsNfcDevice}"); + } + + var targetKey = allKeys.FirstOrDefault(k => + targetSerial == null || k.SerialNumber == targetSerial); + + if (targetKey == null) + { + Output.WriteLine($"No YubiKey found{(targetSerial.HasValue ? $" with serial {targetSerial}" : "")}"); + return false; + } + + Output.WriteLine(); + Output.WriteLine($"--- Target: Serial={targetKey.SerialNumber}, FW={targetKey.FirmwareVersion} ---"); + + var device = (YubiKeyDevice)targetKey; + Output.WriteLine($"FIDO2 in AvailableUsbCapabilities: {targetKey.AvailableUsbCapabilities.HasFlag(YubiKeyCapabilities.Fido2)}"); + + // ---- Test 1: Standard HID path ---- + RunTest("Test 1: Standard Connect(Fido2) — HID path", () => + { + using var conn = targetKey.Connect(YubiKeyApplication.Fido2); + Output.WriteLine($" Connection type: {conn.GetType().Name}"); + var info = conn.SendCommand(new GetInfoCommand()).GetData(); + Output.WriteLine($" AAGUID: {BitConverter.ToString(info.Aaguid.ToArray())}"); + Output.WriteLine($" Versions: {string.Join(", ", info.Versions ?? Array.Empty())}"); + }); + + // ---- Test 2: Direct SmartCard FIDO2 ---- + RunTest("Test 2: Direct SmartCardConnection for FIDO2 over USB CCID", () => + { + if (!device.HasSmartCard) { Output.WriteLine(" SKIPPED — no SmartCard interface"); return; } + var scDevice = device.GetSmartCardDevice(); + Output.WriteLine($" SmartCard path: {scDevice.Path}, IsNfc: {scDevice.IsNfcTransport()}"); + + using var scConn = new SmartCardConnection(scDevice, YubiKeyApplication.Fido2); + Output.WriteLine($" SmartCardConnection created for FIDO2!"); + var info = scConn.SendCommand(new GetInfoCommand()).GetData(); + Output.WriteLine($" AAGUID: {BitConverter.ToString(info.Aaguid.ToArray())}"); + Output.WriteLine($" Transports: {string.Join(", ", info.Transports ?? Array.Empty())}"); + }); + + // ---- Test 3: FIDO2 + SCP03 (default keys) over USB CCID ---- + RunTest("Test 3: Fido2Session + SCP03 (DefaultKey) over USB CCID", () => + { + using var session = new Fido2Session(targetKey, keyParameters: Scp03KeyParameters.DefaultKey); + Output.WriteLine($" Connection type: {session.Connection.GetType().Name}"); + Output.WriteLine($" AAGUID: {BitConverter.ToString(session.AuthenticatorInfo.Aaguid.ToArray())}"); + Output.WriteLine($" Versions: {string.Join(", ", session.AuthenticatorInfo.Versions ?? Array.Empty())}"); + }); + + // ---- Test 4: FIDO2 + SCP11b over USB CCID ---- + RunTest("Test 4: Fido2Session + SCP11b over USB CCID", () => + { + // Step 1: Reset Security Domain to clean state + Output.WriteLine(" Resetting Security Domain..."); + using (var sdSession = new SecurityDomainSession(targetKey)) + { + sdSession.Reset(); + } + Output.WriteLine(" Security Domain reset OK"); + + // Step 2: Get SCP11b key parameters (generates key ref on device) + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + Output.WriteLine($" Getting SCP11b certificates for {keyReference}..."); + + IReadOnlyCollection certs; + using (var sdSession = new SecurityDomainSession(targetKey)) + { + certs = sdSession.GetCertificates(keyReference); + } + + var leaf = certs.Last(); + var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!; + var keyParams = new Scp11KeyParameters( + keyReference, + ECPublicKey.CreateFromParameters(ecDsaPublicKey.ExportParameters(false))); + Output.WriteLine($" SCP11b key params created (leaf cert subject: {leaf.Subject})"); + + // Step 3: Open FIDO2 session with SCP11b + using var session = new Fido2Session(targetKey, keyParameters: keyParams); + Output.WriteLine($" Connection type: {session.Connection.GetType().Name}"); + Output.WriteLine($" AAGUID: {BitConverter.ToString(session.AuthenticatorInfo.Aaguid.ToArray())}"); + Output.WriteLine($" Versions: {string.Join(", ", session.AuthenticatorInfo.Versions ?? Array.Empty())}"); + }); + + Output.WriteLine(); + Output.WriteLine("=== Probe Complete ==="); + return true; + } + + private void RunTest(string name, Action action) + { + Output.WriteLine(); + Output.WriteLine(name); + try + { + action(); + Output.WriteLine($" >>> PASS"); + } + catch (Exception ex) + { + Output.WriteLine($" >>> FAIL: {ex.GetType().Name}: {ex.Message}"); + if (ex.InnerException != null) + { + Output.WriteLine($" Inner: {ex.InnerException.GetType().Name}: {ex.InnerException.Message}"); + } + } + } + } +} diff --git a/Yubico.YubiKey/tests/sandbox/Program.cs b/Yubico.YubiKey/tests/sandbox/Program.cs index bbc7856f1..ab8a33104 100644 --- a/Yubico.YubiKey/tests/sandbox/Program.cs +++ b/Yubico.YubiKey/tests/sandbox/Program.cs @@ -54,6 +54,7 @@ class Program : IOutput, IDisposable ["feature"] = (output) => new YubiKeyFeaturePlugin(output), ["david"] = (output) => new DavidPlugin(output), ["oath"] = (output) => new OathPlugin(output), + ["fido2ccid"] = (output) => new Fido2CcidProbePlugin(output), }; #region IDisposable Implementation diff --git a/docs/findings-and-assumptions.md b/docs/findings-and-assumptions.md new file mode 100644 index 000000000..5e2225676 --- /dev/null +++ b/docs/findings-and-assumptions.md @@ -0,0 +1,244 @@ +# FIDO2 + SCP Support: Findings and Assumptions + +**Branch:** `feature/fido2-scp-support` +**Date:** 2026-03-18 (updated) +**Status:** Resolved — FIDO2+SCP works over NFC (all firmware) and USB CCID (firmware 5.8+) + +--- + +## Goal + +Enable FIDO2 sessions over SCP03/SCP11 secure channels in the .NET SDK, with the end objective of supporting **Rust FFI interop** — allowing a Rust environment to call into a FIDO2 session and execute custom commands over an SCP-protected connection. The .NET SDK handles SCP channel establishment and encryption transparently, with Rust sending cleartext APDUs across the FFI boundary. + +This requires two things: +1. **`Fido2Session` must accept `ScpKeyParameters`** — previously hard-coded to `null`, unlike `PivSession`, `OathSession`, `OtpSession`, and `YubiHsmAuthSession` which already support SCP +2. **The SCP connection pipeline must work with FIDO2** — the `ScpConnection` constructor flow must correctly establish a secure channel for the FIDO2 applet + +This is a feature parity gap — the Android SDK (`yubikit-android`) already supports FIDO2 + SCP. + +--- + +## Root Cause of Initial Failure + +### The Error + +``` +Yubico.Core.Iso7816.ApduException : Failed to select the smart card application. 0x6A82 + at SmartCardConnection.SelectApplication() + at SmartCardConnection.SetPipeline(IApduTransform) + at ScpConnection..ctor(ISmartCardDevice, YubiKeyApplication, ScpKeyParameters) +``` + +`0x6A82` = ISO 7816 "file not found" — the FIDO2 AID could not be selected over USB CCID. + +### Diagnosis + +The initial tests used `Transport.UsbSmartCard` on **pre-5.8 firmware**. On firmware prior to 5.8, FIDO2 communicates via **HID** over USB, not CCID. The FIDO2 AID (`A0 00 00 06 47 2F 00 01`) is not selectable over the USB CCID (SmartCard) interface on those firmware versions. + +**On firmware 5.8+**, Yubico added the FIDO2 applet to the USB CCID interface. The FIDO2 AID is selectable over both HID and CCID, making FIDO2+SCP possible over USB as well as NFC. + +The equivalent PIV + SCP03 test passed on the same devices because PIV is always available over CCID. + +### Key insight from `ConnectionFactory.cs` + +`ConnectionFactory.CreateConnection()` for FIDO2 **prefers HID** (`ConnectionFactory.cs:123-130`) and only falls back to SmartCard if no HID device is available. Normal FIDO2 usage never goes through CCID. SCP connections always use SmartCard (`ConnectionFactory.cs:88-101`). On pre-5.8 firmware, this means FIDO2+SCP is inherently an **NFC use case**. On 5.8+, FIDO2+SCP works over both USB CCID and NFC. + +--- + +## Assumptions — Verified + +| # | Assumption | Result | +|---|-----------|--------| +| A1 | FIDO2 AID is selectable over USB CCID | **FALSE on pre-5.8** — `0x6A82`. **TRUE on 5.8+** — FIDO2 AID selectable over USB CCID. | +| A2 | `ResetSecurityDomainOnAllowedDevices()` interferes | **N/A** — irrelevant once correct transport/firmware used. | +| A3 | PIV+SCP works because PIV forwards GP commands to Security Domain | **TRUE** — confirmed by working PIV+SCP03 tests. | +| A4 | FIDO2 applet does NOT forward GP commands to Security Domain | **FALSE** — FIDO2+SCP03 and FIDO2+SCP11b both succeed over NFC (all firmware) and USB CCID (5.8+). The FIDO2 applet handles GP SCP commands correctly. | +| A5 | Android SDK's FIDO2+SCP relies on the applet supporting GP SCP | **TRUE** — Android tests run over NFC and succeed. Same flow works in .NET over NFC and over USB CCID on 5.8+. | +| A6 | `ScpConnection` needs restructuring for FIDO2 | **FALSE** — the existing `ScpConnection` flow is architecturally correct. Only the transport was wrong on pre-5.8 firmware. | + +### Key conclusion + +**Problem 2 from the initial analysis was wrong.** The FIDO2 applet *does* handle GlobalPlatform SCP commands (INITIALIZE UPDATE / EXTERNAL AUTHENTICATE). The `ScpConnection` constructor flow — SELECT app, then SCP handshake on that applet — works identically for FIDO2 as it does for PIV/OATH/OTP. No restructuring needed. + +**Additionally**, on firmware 5.8+, the FIDO2 AID is registered on the USB CCID interface, so SCP works over USB — not just NFC. This was confirmed with MakeCredential (full PIN + touch + attestation) over SCP03 on a 5.8+ key connected via USB. + +--- + +## Changes Made + +### 1. `Fido2Session.cs` — Constructor updated + +```csharp +// BEFORE +public Fido2Session(IYubiKeyDevice yubiKey, ReadOnlyMemory? persistentPinUvAuthToken = null) + : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Fido2, keyParameters: null) + +// AFTER +public Fido2Session( + IYubiKeyDevice yubiKey, + ReadOnlyMemory? persistentPinUvAuthToken = null, + ScpKeyParameters? keyParameters = null) + : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Fido2, keyParameters) +``` + +### 2. `YubiKeyFeatureExtensions.cs` — Feature gate + +Added `YubiKeyCapabilities.Fido2` to the `YubiKeyFeature.Scp03` capability check so `ApplicationSession.GetConnection()` correctly validates FIDO2+SCP: + +```csharp +YubiKeyFeature.Scp03 => + yubiKeyDevice.FirmwareVersion >= FirmwareVersion.V5_3_0 + && (HasApplication(yubiKeyDevice, YubiKeyCapabilities.Piv) + || HasApplication(yubiKeyDevice, YubiKeyCapabilities.Oath) + || HasApplication(yubiKeyDevice, YubiKeyCapabilities.OpenPgp) + || HasApplication(yubiKeyDevice, YubiKeyCapabilities.Otp) + || HasApplication(yubiKeyDevice, YubiKeyCapabilities.YubiHsmAuth) + || HasApplication(yubiKeyDevice, YubiKeyCapabilities.Fido2)), // NEW +``` + +### 3. `FidoIntegrationTestBase.cs` — Fixed caller + +```csharp +// Named parameter to resolve ambiguity with new ScpKeyParameters parameter +new Fido2Session(testDevice, persistentPinUvAuthToken: persistentPinUvAuthToken) +``` + +### 4. `Scp03Tests.cs` — FIDO2 integration tests + +- Added `[InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)]` for 5.8+ USB CCID testing +- Added firmware 5.8+ skip condition for USB CCID tests +- Added `Scp03_Fido2Session_MakeCredential_Over_UsbCcid_Succeeds` — full MakeCredential with PIN + touch + attestation over SCP03 +- Added `Scp03_Fido2Session_Pre58_UsbCcid_Skips_Gracefully` — validates correct behavior on both 5.8+ and pre-5.8 keys + +### 5. `Scp11Tests.cs` — FIDO2 integration tests + +- Changed from single NfcSmartCard parameter to theory with both NFC and USB transports +- Added `[InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)]` with firmware 5.8+ skip condition + +### Build & Test Status + +- Solution builds with **0 errors, 0 warnings** +- All **3575 unit tests pass** +- Integration tests **pass over NFC** for both SCP03 and SCP11b (all firmware) +- Integration tests **pass over USB CCID** for SCP03 and SCP11b (firmware 5.8+) +- Full MakeCredential (PIN + touch + attestation) confirmed over SCP03 + USB CCID on 5.8+ + +--- + +## Connection Flows Explained + +### How YubiKey exposes interfaces over USB vs NFC + +Over **USB**, the YubiKey exposes multiple USB interfaces simultaneously: +- **HID FIDO** — for FIDO2/U2F (CTAP2 frames over HID reports) +- **HID Keyboard** — for OTP (keystroke injection) +- **CCID (SmartCard)** — for PIV, OATH, OTP, OpenPGP, YubiHSM Auth, Security Domain + +On **pre-5.8 firmware**, the FIDO2 applet is **only registered on the HID FIDO interface** over USB. It is not registered on CCID. This is a firmware-level decision. + +On **firmware 5.8+**, the FIDO2 applet is **registered on both HID FIDO and CCID** over USB. This means FIDO2 is selectable via SmartCard commands over USB, enabling SCP over USB. + +Over **NFC**, there is only **one interface** — ISO 14443 (SmartCard). All applets are selectable through it, including FIDO2, on all firmware versions. + +### The four FIDO2 connection flows + +#### Flow 1: USB, no SCP — works (all firmware) + +``` +Fido2Session(yubiKey, keyParameters: null) + → ConnectionFactory.CreateConnection(Fido2) + → _hidFidoDevice != null → YES + → return new FidoConnection(_hidFidoDevice) +``` + +Uses HID FIDO interface directly. No SELECT APDU. CTAP2 binary frames go over USB HID reports. This is the normal path — SmartCard/CCID is never involved. + +#### Flow 2: NFC, no SCP — works (all firmware) + +``` +Fido2Session(yubiKey, keyParameters: null) + → ConnectionFactory.CreateConnection(Fido2) + → _hidFidoDevice is null (NFC has no HID) + → _smartCardDevice != null → YES (NFC is always SmartCard) + → return new SmartCardConnection(_smartCardDevice, Fido2) + → SelectApplication() → SELECT FIDO2 AID → OK +``` + +Over NFC, there's no HID, so the SDK falls back to SmartCard. The FIDO2 AID is selectable because NFC exposes all applets. CTAP2 commands are wrapped in ISO 7816 APDUs. + +#### Flow 3: USB, with SCP — firmware-dependent + +**Firmware 5.8+: WORKS** + +``` +Fido2Session(yubiKey, keyParameters: scp03Key) + → ConnectionFactory.CreateScpConnection(Fido2, scp03Key) + → new ScpConnection(_smartCardDevice, Fido2, scp03Key) + → SelectApplication() → SELECT FIDO2 AID over CCID → OK (5.8+ registers FIDO2 on CCID) + → SCP handshake (INITIALIZE UPDATE / EXTERNAL AUTHENTICATE) → OK + → All subsequent CTAP2 APDUs encrypted → OK +``` + +On 5.8+, the FIDO2 AID is registered on both HID and CCID. SCP is a SmartCard-layer protocol, so it routes through CCID. SELECT succeeds, SCP handshake succeeds, and all FIDO2 operations (including MakeCredential with PIN + touch) work through the encrypted channel. + +**Pre-5.8 firmware: FAILS** + +``` +Fido2Session(yubiKey, keyParameters: scp03Key) + → ConnectionFactory.CreateScpConnection(Fido2, scp03Key) + → new ScpConnection(_smartCardDevice, Fido2, scp03Key) + → SelectApplication() → SELECT FIDO2 AID over CCID → 0x6A82 FAIL +``` + +On pre-5.8, the firmware does not register the FIDO2 applet on CCID over USB. SELECT fails with "file not found." This is not an SDK issue — the card simply doesn't have the FIDO2 AID on CCID. + +#### Flow 4: NFC, with SCP — works (all firmware) + +``` +Fido2Session(yubiKey, keyParameters: scp03Key) + → ConnectionFactory.CreateScpConnection(Fido2, scp03Key) + → new ScpConnection(_smartCardDevice, Fido2, scp03Key) + → SelectApplication() → SELECT FIDO2 AID → OK (NFC exposes all applets) + → SCP handshake (INITIALIZE UPDATE / EXTERNAL AUTHENTICATE) → OK + → All subsequent CTAP2 APDUs encrypted → OK +``` + +Over NFC, SmartCard is the only interface, all applets are selectable, and the FIDO2 applet correctly handles GlobalPlatform SCP commands. This is the primary use case: mobile devices communicating with YubiKeys over NFC with SCP channel protection. + +### Summary + +| Flow | SDK transport | Firmware | FIDO2 available? | SCP possible? | Result | +|------|--------------|----------|------------------|---------------|--------| +| 1. USB, no SCP | HID FIDO | All | Yes (HID) | N/A | **Works** | +| 2. NFC, no SCP | SmartCard | All | Yes (NFC exposes all) | N/A | **Works** | +| 3. USB, with SCP | SmartCard (CCID) | Pre-5.8 | No (not on CCID) | Blocked | **Fails** | +| 3. USB, with SCP | SmartCard (CCID) | 5.8+ | Yes (on CCID) | Yes | **Works** | +| 4. NFC, with SCP | SmartCard (NFC) | All | Yes (NFC exposes all) | Yes | **Works** | + +The constraint on pre-5.8: **SCP requires SmartCard. USB FIDO2 is HID-only. They are incompatible transports over USB.** Over NFC, both coexist on a single SmartCard interface. + +On 5.8+: **FIDO2 is available on both HID and CCID over USB.** SCP routes through CCID and succeeds. + +--- + +## Remaining Work + +### Rust FFI path +With FIDO2+SCP validated over NFC (all firmware) and USB CCID (5.8+): +- Wrap `Fido2Session` with `ScpKeyParameters` in NativeAOT exports +- Expose `Connection.SendCommand()` for custom APDUs through the encrypted channel +- Create `[UnmanagedCallersOnly]` entry points for Rust consumption +- On 5.8+ firmware, USB connections are viable — no NFC reader required + +--- + +## Reference Files + +| File | Role | +|------|------| +| `Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs` | Session constructor — accepts `ScpKeyParameters` | +| `Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs` | Feature gate — FIDO2 in SCP03 check | +| `Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs` | SCP connection — works as-is, no changes needed | +| `Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs` | Base class — `SelectApplication()` and `SetPipeline()` | +| `Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs` | Connection routing — FIDO2 prefers HID, SCP always uses SmartCard | +| `yubikit-android/.../fido/ctap/Ctap2Session.java:1650-1661` | Android reference — confirms FIDO2+SCP support | diff --git a/docs/rust-to-netsdk-ffi.md b/docs/rust-to-netsdk-ffi.md new file mode 100644 index 000000000..869ab2773 --- /dev/null +++ b/docs/rust-to-netsdk-ffi.md @@ -0,0 +1,545 @@ +# Rust-to-.NET SDK FFI: Technical Handover + +**Date:** 2026-03-17 +**From:** Dennis (SDK team) +**For:** Rust team +**Branch:** `feature/fido2-scp-support` + +--- + +## TL;DR + +The .NET SDK now supports FIDO2 over SCP03/SCP11. On firmware 5.8+, this works over both USB CCID and NFC. On pre-5.8 firmware, NFC is required. To expose this to Rust, we'd compile a NativeAOT shared library (`.dll`/`.so`/`.dylib`) with `[UnmanagedCallersOnly]` C-ABI exports. Rust links against it and calls functions like `fido2_session_open_scp03()` and `fido2_send_command()` — the .NET SDK handles all SCP encryption transparently. + +**However**, this approach ships a 15-30 MB .NET runtime in the native binary, requires per-platform builds, GCHandle lifecycle management, and NativeAOT trimming workarounds. If the Rust team only needs an SCP-encrypted APDU pipe (not the full FIDO2 command set), implementing SCP03 directly in Rust (~500 lines using `aes`/`cmac` crates + `pcsc`) is likely simpler, smaller, and easier to maintain. See the **Feasibility assessment** section for the full tradeoff analysis. + +--- + +## What this enables + +The .NET YubiKey SDK now supports FIDO2 sessions over SCP03 and SCP11 secure channels. This document describes how to expose that capability to Rust via a NativeAOT-compiled shared library. + +The end result: Rust sends cleartext FIDO2/CTAP2 APDUs across an FFI boundary. The .NET SDK handles SCP channel establishment, encryption, and key management transparently. Rust never touches SCP directly. + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ Rust Application │ +│ │ +│ extern "C" { │ +│ fn fido2_session_open_scp03(...) -> i32; │ +│ fn fido2_send_command(...) -> i32; │ +│ } │ +└──────────────────┬──────────────────────────────────┘ + │ FFI (C ABI) + │ +┌──────────────────▼──────────────────────────────────┐ +│ Yubico.YubiKey.NativeInterop │ +│ (NativeAOT shared library) │ +│ │ +│ [UnmanagedCallersOnly] static methods │ +│ GCHandle-based opaque handles │ +│ Error codes (no exceptions across FFI) │ +└──────────────────┬──────────────────────────────────┘ + │ .NET project reference + │ +┌──────────────────▼──────────────────────────────────┐ +│ Yubico.YubiKey SDK │ +│ │ +│ Fido2Session(device, ScpKeyParameters) │ +│ ScpConnection → SCP03/SCP11 handshake │ +│ Encrypted APDU transport │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## Transport support for FIDO2+SCP + +FIDO2 over SCP support depends on firmware version: + +| Transport | Firmware | FIDO2 interface | SCP possible? | +|-----------|----------|----------------|---------------| +| USB, no SCP | All | HID FIDO | N/A (works) | +| USB, with SCP | Pre-5.8 | CCID — FIDO2 AID not on CCID | **No** | +| USB, with SCP | 5.8+ | CCID — FIDO2 AID registered on CCID | **Yes** | +| NFC, no SCP | All | SmartCard | N/A (works) | +| NFC, with SCP | All | SmartCard | **Yes** | + +**Why:** SCP is a SmartCard-layer protocol (ISO 7816 secure messaging) that requires CCID. On pre-5.8 firmware, the YubiKey exposes FIDO2 only on the HID interface over USB — not on CCID. On firmware 5.8+, the FIDO2 applet is registered on both HID and CCID over USB, so SCP works over USB. Over NFC, there is only one interface (ISO 14443 / SmartCard), so all applets — including FIDO2 — are selectable on all firmware versions. + +**Implication for Rust:** On firmware 5.8+, FIDO2+SCP works over USB — no NFC reader required. On pre-5.8 firmware, FIDO2+SCP requires NFC. The Rust application should check the firmware version and transport to determine SCP availability. + +--- + +## .NET NativeInterop project + +### Project setup + +```xml + + + + net8.0 + true + true + Yubico.YubiKey.NativeInterop + yubico_yubikey + + + + + + +``` + +### Publishing per platform + +```bash +# Windows (produces yubico_yubikey.dll) +dotnet publish -r win-x64 -c Release + +# Linux (produces yubico_yubikey.so) +dotnet publish -r linux-x64 -c Release + +# macOS Intel (produces yubico_yubikey.dylib) +dotnet publish -r osx-x64 -c Release + +# macOS Apple Silicon (produces yubico_yubikey.dylib) +dotnet publish -r osx-arm64 -c Release +``` + +Output lands in `bin/Release/net8.0//native/`. + +### NativeAOT prerequisites + +- .NET 8 SDK +- Platform-specific AOT toolchain: + - **Windows:** Visual Studio with C++ workload (for `link.exe`) + - **Linux:** `clang`, `zlib1g-dev` + - **macOS:** Xcode command line tools + +--- + +## FFI surface (C ABI) + +### Design principles + +| Principle | Implementation | +|-----------|---------------| +| No exceptions across FFI | Every function returns `int` error code. `0` = success. | +| Opaque handles | Managed objects are pinned via `GCHandle`, returned as `void*`. Rust never dereferences them. | +| Caller-allocated buffers | Rust allocates output buffers, passes `(ptr, len)`. Avoids cross-allocator issues. | +| No strings | Use byte arrays. UTF-8 where text is needed. | +| Separate functions per SCP variant | Simpler than passing discriminated unions across FFI. | + +### Error codes + +```c +#define YUBIKEY_OK 0 +#define YUBIKEY_ERR_NULL_HANDLE -1 +#define YUBIKEY_ERR_NOT_FOUND -2 +#define YUBIKEY_ERR_CONNECTION -3 +#define YUBIKEY_ERR_COMMAND -4 +#define YUBIKEY_ERR_BUFFER_SMALL -5 +#define YUBIKEY_ERR_INVALID_ARG -6 +``` + +### Proposed API + +#### Device enumeration + +```c +// Returns the number of connected YubiKeys, or 0 on error. +int yubikey_count_devices(void); + +// Opens a handle to the YubiKey at the given index. +// *out_handle receives an opaque device handle. +int yubikey_open_device(int index, void** out_handle); + +// Gets the serial number of the device. +int yubikey_device_serial(void* device_handle, int* out_serial); + +// Releases a device handle. Safe to call with NULL. +void yubikey_close_device(void* device_handle); +``` + +#### FIDO2 session lifecycle + +```c +// Opens a FIDO2 session without SCP (plain HID for USB, SmartCard for NFC). +int fido2_session_open(void* device_handle, void** out_session); + +// Opens a FIDO2 session with SCP03 using default keys. +// Works over NFC (all firmware) and USB CCID (firmware 5.8+). +int fido2_session_open_scp03(void* device_handle, void** out_session); + +// Opens a FIDO2 session with custom SCP03 keys. +// Each key pointer must reference exactly 16 bytes. +// Works over NFC (all firmware) and USB CCID (firmware 5.8+). +int fido2_session_open_scp03_custom( + void* device_handle, + const uint8_t* channel_mac_key, // 16 bytes + const uint8_t* channel_enc_key, // 16 bytes + const uint8_t* data_enc_key, // 16 bytes + void** out_session); + +// Closes and disposes a FIDO2 session. Safe to call with NULL. +void fido2_session_close(void* session_handle); +``` + +#### Commands + +```c +// Sends a raw APDU through the (optionally SCP-encrypted) channel. +// The APDU is cleartext — the SDK encrypts it if SCP is active. +// Response is written to out_buffer. *out_len receives actual length. +int fido2_send_command( + void* session_handle, + const uint8_t* apdu, // input APDU bytes + int apdu_len, + uint8_t* out_buffer, // caller-allocated response buffer + int buffer_len, + int* out_len); // actual response length + +// Gets authenticator info (FIDO2 GetInfo command). +// Writes a UTF-8 comma-separated list of supported versions. +int fido2_get_info_versions( + void* session_handle, + uint8_t* out_buffer, + int buffer_len, + int* out_len); +``` + +--- + +## C# implementation reference + +### Handle management pattern + +```csharp +// Pinning a managed object for FFI +var session = new Fido2Session(device, Scp03KeyParameters.DefaultKey); +var handle = GCHandle.Alloc(session); +*outSession = GCHandle.ToIntPtr(handle); // → void* in Rust + +// Recovering it later +var session = (Fido2Session)GCHandle.FromIntPtr(sessionHandle).Target!; + +// Releasing it +var handle = GCHandle.FromIntPtr(sessionHandle); +if (handle.Target is IDisposable d) d.Dispose(); +handle.Free(); +``` + +### [UnmanagedCallersOnly] method pattern + +```csharp +[UnmanagedCallersOnly(EntryPoint = "fido2_session_open_scp03")] +public static int OpenFido2SessionScp03(IntPtr deviceHandle, IntPtr* outSession) +{ + try + { + if (deviceHandle == IntPtr.Zero) return ERR_NULL_HANDLE; + + var device = (IYubiKeyDevice)GCHandle.FromIntPtr(deviceHandle).Target!; + var session = new Fido2Session(device, Scp03KeyParameters.DefaultKey); + + var sessionHandle = GCHandle.Alloc(session); + *outSession = GCHandle.ToIntPtr(sessionHandle); + return OK; + } + catch + { + return ERR_CONNECTION_FAILED; + } +} +``` + +### Key constraint: AllowUnsafeBlocks + +`[UnmanagedCallersOnly]` methods with pointer parameters (`IntPtr*`, `byte*`) require `true` in the `.csproj`. + +--- + +## Rust consumption + +### Linking + +```toml +# Cargo.toml +[build-dependencies] +# If using bindgen to auto-generate from a C header: +bindgen = "0.71" + +# Or declare the link manually: +# No extra deps needed — just extern "C" declarations +``` + +```rust +// build.rs (if needed) +fn main() { + // Point to the NativeAOT output directory + println!("cargo:rustc-link-search=native=path/to/native/output"); + println!("cargo:rustc-link-lib=dylib=yubico_yubikey"); +} +``` + +### Rust bindings + +```rust +use std::ffi::c_void; +use std::os::raw::c_int; + +const OK: c_int = 0; +const ERR_BUFFER_SMALL: c_int = -5; + +extern "C" { + fn yubikey_count_devices() -> c_int; + fn yubikey_open_device(index: c_int, out_handle: *mut *mut c_void) -> c_int; + fn yubikey_close_device(handle: *mut c_void); + + fn fido2_session_open_scp03( + device: *mut c_void, + out_session: *mut *mut c_void, + ) -> c_int; + fn fido2_session_close(session: *mut c_void); + + fn fido2_send_command( + session: *mut c_void, + apdu: *const u8, + apdu_len: c_int, + out_buf: *mut u8, + buf_len: c_int, + out_len: *mut c_int, + ) -> c_int; + + fn fido2_get_info_versions( + session: *mut c_void, + out_buf: *mut u8, + buf_len: c_int, + out_len: *mut c_int, + ) -> c_int; +} +``` + +### Safe Rust wrapper (suggested) + +```rust +use std::ptr; + +pub struct YubiKeyDevice { + handle: *mut c_void, +} + +impl YubiKeyDevice { + pub fn open(index: i32) -> Result { + let mut handle: *mut c_void = ptr::null_mut(); + let rc = unsafe { yubikey_open_device(index, &mut handle) }; + if rc != OK { return Err(rc); } + Ok(Self { handle }) + } +} + +impl Drop for YubiKeyDevice { + fn drop(&mut self) { + unsafe { yubikey_close_device(self.handle); } + } +} + +pub struct Fido2Session { + handle: *mut c_void, +} + +impl Fido2Session { + /// Opens a FIDO2 session with SCP03 encryption. + /// Works over NFC (all firmware) and USB CCID (firmware 5.8+). + pub fn open_scp03(device: &YubiKeyDevice) -> Result { + let mut handle: *mut c_void = ptr::null_mut(); + let rc = unsafe { fido2_session_open_scp03(device.handle, &mut handle) }; + if rc != OK { return Err(rc); } + Ok(Self { handle }) + } + + /// Sends a cleartext APDU. The .NET SDK encrypts it via SCP if active. + pub fn send_command(&self, apdu: &[u8]) -> Result, i32> { + let mut buf = vec![0u8; 4096]; + let mut out_len: c_int = 0; + let rc = unsafe { + fido2_send_command( + self.handle, + apdu.as_ptr(), + apdu.len() as c_int, + buf.as_mut_ptr(), + buf.len() as c_int, + &mut out_len, + ) + }; + if rc != OK { return Err(rc); } + buf.truncate(out_len as usize); + Ok(buf) + } +} + +impl Drop for Fido2Session { + fn drop(&mut self) { + unsafe { fido2_session_close(self.handle); } + } +} +``` + +### Usage example + +```rust +fn main() -> Result<(), i32> { + let count = unsafe { yubikey_count_devices() }; + println!("Found {} YubiKey(s)", count); + + let device = YubiKeyDevice::open(0)?; + let session = Fido2Session::open_scp03(&device)?; + + // Send a CTAP2 GetInfo command (0x04) through the SCP-encrypted channel + let get_info_cmd = [0x04]; + let response = session.send_command(&get_info_cmd)?; + println!("Response: {} bytes", response.len()); + + Ok(()) // Drop cleans up session and device handles +} +``` + +--- + +## Implementation plan + +### Phase 1: Skeleton (Windows first) + +1. Create `Yubico.YubiKey.NativeInterop` project with NativeAOT config +2. Implement device enumeration exports (`yubikey_count_devices`, `yubikey_open_device`, `yubikey_close_device`) +3. Implement `fido2_session_open` (plain, no SCP) and `fido2_session_close` +4. Publish for `win-x64`, verify Rust can link and call functions +5. Write a minimal Rust test that enumerates devices and opens/closes a session + +### Phase 2: SCP support + +1. Implement `fido2_session_open_scp03` and `fido2_session_open_scp03_custom` +2. Implement `fido2_send_command` (raw APDU passthrough) +3. Test over NFC or USB CCID (5.8+ firmware) with a YubiKey +4. Add SCP11 variants if needed + +### Phase 3: Cross-platform + +1. Publish for `linux-x64` and `osx-arm64` +2. Set up CI to produce all three native libraries +3. Package as a Rust crate with platform-specific lib selection + +### Phase 4: Production hardening + +1. Thread safety audit (one session per thread, or add locking) +2. Comprehensive error codes with `fido2_get_last_error` for detailed messages +3. Logging bridge (route .NET SDK logs to Rust's tracing/log) +4. Memory leak testing (ensure all GCHandles are freed) + +--- + +## Known risks and considerations + +| Risk | Mitigation | +|------|------------| +| **NativeAOT trims unused code** | The SDK uses reflection in some areas. May need `` or `rd.xml` to preserve types. Test early. | +| **NativeAOT binary size** | Expect 15-30 MB for the shared library. The full .NET runtime is embedded. | +| **Thread safety** | `Fido2Session` is not thread-safe. Document that each session handle must be used from one thread at a time. | +| **GCHandle leaks** | If Rust crashes or doesn't call `_close` functions, managed objects leak. Consider a timeout/finalizer strategy. | +| **USB device access** | On Linux, requires udev rules for non-root access. On Windows, requires the correct smart card drivers for NFC readers. | +| **Transport compatibility** | FIDO2+SCP requires CCID: works over NFC (all firmware) and USB CCID (5.8+). On pre-5.8 firmware over USB, Rust code must handle graceful fallback. | + +--- + +## Files to reference + +| File | What it shows | +|------|---------------| +| `Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs` | Constructor accepting `ScpKeyParameters`, XML docs with transport notes | +| `Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs` | How the SCP channel is established | +| `Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs` | How connections are routed (HID vs SmartCard) | +| `Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs` | Feature gate — FIDO2 in SCP03 capability check | +| `docs/findings-and-assumptions.md` | Full investigation of FIDO2+SCP transport constraints | + +--- + +## Feasibility assessment + +### What the FFI path actually buys you + +Through all the NativeAOT machinery, the .NET SDK provides: + +1. **SCP03 handshake** — INITIALIZE UPDATE + EXTERNAL AUTHENTICATE (2 APDUs, AES-128 key derivation) +2. **SCP11 handshake** — EC key agreement, certificate chain validation, session key derivation +3. **APDU encryption/MAC** — AES-CBC encryption + CMAC per the GlobalPlatform SCP spec +4. **SELECT FIDO2 AID** — a fixed 7-byte APDU +5. **Device enumeration** — PC/SC reader discovery + +That's the entire value crossing the FFI boundary. Everything else (CTAP2 commands, credential management, PIN handling) happens in Rust anyway. + +### Cost of the FFI path + +| Cost | Detail | +|------|--------| +| **Binary size** | 15-30 MB native library (embeds .NET runtime + GC + JIT stubs) | +| **Build complexity** | NativeAOT requires MSVC (Windows), clang (Linux), Xcode (macOS) — per platform | +| **Trimming issues** | The SDK uses reflection (logging, DI, serialization). Expect `rd.xml` and `` debugging. | +| **FFI surface maintenance** | Every SDK API change requires updating the interop layer and Rust bindings | +| **Debugging** | Errors are opaque integers. Stack traces don't cross the FFI boundary. | +| **GCHandle lifecycle** | Managed objects pinned via `GCHandle` leak if Rust doesn't call `_close`. No destructor safety net. | +| **Two runtimes** | Process hosts both the .NET GC and Rust's allocator. Memory behavior is less predictable. | + +### Alternative: native Rust SCP implementation + +SCP03 is a well-specified protocol (GlobalPlatform Card Specification, Amendment D): + +| Component | Rust implementation | Complexity | +|-----------|-------------------|------------| +| Key derivation | `cmac` crate (AES-CMAC) | ~30 lines | +| Session encryption | `aes` crate (AES-128-CBC) | ~20 lines | +| MAC generation | `cmac` crate | ~20 lines | +| APDU wrapping | Prepend MAC, encrypt payload | ~40 lines | +| Handshake | INITIALIZE UPDATE + EXTERNAL AUTHENTICATE | ~100 lines | +| NFC communication | `pcsc` crate (PC/SC smart card) | ~50 lines | +| SELECT application | Fixed APDU construction | ~10 lines | +| **Total** | | **~300-500 lines** | + +### Comparison + +| | NativeAOT FFI | Native Rust SCP | +|---|---|---| +| **Time to implement** | 2-3 weeks | 1-2 weeks | +| **Binary size** | 15-30 MB | ~1 MB | +| **Runtime dependencies** | .NET 8 runtime (embedded) | None (static linking) | +| **Debugging** | Painful (cross-runtime) | Normal Rust tooling | +| **Maintenance burden** | Coupled to .NET SDK versions | Self-contained | +| **Platform builds** | 3 separate NativeAOT publishes | `cargo build` (cross-compile) | +| **SCP03 support** | Free (SDK has it) | ~500 lines of Rust | +| **SCP11 support** | Free (SDK has it) | Significant work (~2000 lines: EC key agreement, X.509 cert chains, multiple key slots) | +| **Full FIDO2 command set** | Free (SDK has it) | Must implement from scratch | + +### Recommendation + +**If you need SCP03 only:** Implement in Rust. The protocol is simple, the crypto crates are mature, and you avoid all FFI complexity. The `pcsc` crate handles NFC reader access. + +**If you need SCP11:** Evaluate whether the EC key agreement and certificate handling justifies the FFI overhead. SCP11 is substantially more complex than SCP03. The .NET SDK has a battle-tested implementation. + +**If you need the full FIDO2 command set (credential management, PIN/UV, attestation):** The FFI path makes sense — reimplementing all of FIDO2 in Rust is weeks of work and the SDK already has it. + +**If you only need an encrypted APDU pipe:** You're shipping 30 MB of .NET runtime for something that's 500 lines of Rust. Don't do it. + +--- + +## Questions for the Rust team + +1. **APDU format:** Do you want to send raw ISO 7816 APDUs, or CTAP2 command bytes? The SDK can handle either — determines which `SendCommand` overload to expose. +2. **SCP11 support:** Do you need SCP11b in addition to SCP03? If so, the FFI surface needs a way to pass EC key material (public key, certificates). +3. **Key management:** Will SCP keys be hardcoded, loaded from config, or provisioned at runtime? Affects whether we need `fido2_session_open_scp03_custom` or just `_scp03` with defaults. +4. **Error detail:** Is the integer error code sufficient, or do you want a `fido2_get_last_error(buf, len)` function that returns the .NET exception message as UTF-8? +5. **Concurrency:** Will multiple Rust threads access sessions simultaneously? If so, we need to add locking in the interop layer. From 1abf5580946652d29accc62770f6df4fb4817562 Mon Sep 17 00:00:00 2001 From: Dennis Dyallo Date: Wed, 18 Mar 2026 16:55:36 +0100 Subject: [PATCH 7/7] chore: Remove internal docs not intended for the repository Co-Authored-By: Claude Opus 4.6 --- docs/findings-and-assumptions.md | 244 -------------- docs/rust-to-netsdk-ffi.md | 545 ------------------------------- 2 files changed, 789 deletions(-) delete mode 100644 docs/findings-and-assumptions.md delete mode 100644 docs/rust-to-netsdk-ffi.md diff --git a/docs/findings-and-assumptions.md b/docs/findings-and-assumptions.md deleted file mode 100644 index 5e2225676..000000000 --- a/docs/findings-and-assumptions.md +++ /dev/null @@ -1,244 +0,0 @@ -# FIDO2 + SCP Support: Findings and Assumptions - -**Branch:** `feature/fido2-scp-support` -**Date:** 2026-03-18 (updated) -**Status:** Resolved — FIDO2+SCP works over NFC (all firmware) and USB CCID (firmware 5.8+) - ---- - -## Goal - -Enable FIDO2 sessions over SCP03/SCP11 secure channels in the .NET SDK, with the end objective of supporting **Rust FFI interop** — allowing a Rust environment to call into a FIDO2 session and execute custom commands over an SCP-protected connection. The .NET SDK handles SCP channel establishment and encryption transparently, with Rust sending cleartext APDUs across the FFI boundary. - -This requires two things: -1. **`Fido2Session` must accept `ScpKeyParameters`** — previously hard-coded to `null`, unlike `PivSession`, `OathSession`, `OtpSession`, and `YubiHsmAuthSession` which already support SCP -2. **The SCP connection pipeline must work with FIDO2** — the `ScpConnection` constructor flow must correctly establish a secure channel for the FIDO2 applet - -This is a feature parity gap — the Android SDK (`yubikit-android`) already supports FIDO2 + SCP. - ---- - -## Root Cause of Initial Failure - -### The Error - -``` -Yubico.Core.Iso7816.ApduException : Failed to select the smart card application. 0x6A82 - at SmartCardConnection.SelectApplication() - at SmartCardConnection.SetPipeline(IApduTransform) - at ScpConnection..ctor(ISmartCardDevice, YubiKeyApplication, ScpKeyParameters) -``` - -`0x6A82` = ISO 7816 "file not found" — the FIDO2 AID could not be selected over USB CCID. - -### Diagnosis - -The initial tests used `Transport.UsbSmartCard` on **pre-5.8 firmware**. On firmware prior to 5.8, FIDO2 communicates via **HID** over USB, not CCID. The FIDO2 AID (`A0 00 00 06 47 2F 00 01`) is not selectable over the USB CCID (SmartCard) interface on those firmware versions. - -**On firmware 5.8+**, Yubico added the FIDO2 applet to the USB CCID interface. The FIDO2 AID is selectable over both HID and CCID, making FIDO2+SCP possible over USB as well as NFC. - -The equivalent PIV + SCP03 test passed on the same devices because PIV is always available over CCID. - -### Key insight from `ConnectionFactory.cs` - -`ConnectionFactory.CreateConnection()` for FIDO2 **prefers HID** (`ConnectionFactory.cs:123-130`) and only falls back to SmartCard if no HID device is available. Normal FIDO2 usage never goes through CCID. SCP connections always use SmartCard (`ConnectionFactory.cs:88-101`). On pre-5.8 firmware, this means FIDO2+SCP is inherently an **NFC use case**. On 5.8+, FIDO2+SCP works over both USB CCID and NFC. - ---- - -## Assumptions — Verified - -| # | Assumption | Result | -|---|-----------|--------| -| A1 | FIDO2 AID is selectable over USB CCID | **FALSE on pre-5.8** — `0x6A82`. **TRUE on 5.8+** — FIDO2 AID selectable over USB CCID. | -| A2 | `ResetSecurityDomainOnAllowedDevices()` interferes | **N/A** — irrelevant once correct transport/firmware used. | -| A3 | PIV+SCP works because PIV forwards GP commands to Security Domain | **TRUE** — confirmed by working PIV+SCP03 tests. | -| A4 | FIDO2 applet does NOT forward GP commands to Security Domain | **FALSE** — FIDO2+SCP03 and FIDO2+SCP11b both succeed over NFC (all firmware) and USB CCID (5.8+). The FIDO2 applet handles GP SCP commands correctly. | -| A5 | Android SDK's FIDO2+SCP relies on the applet supporting GP SCP | **TRUE** — Android tests run over NFC and succeed. Same flow works in .NET over NFC and over USB CCID on 5.8+. | -| A6 | `ScpConnection` needs restructuring for FIDO2 | **FALSE** — the existing `ScpConnection` flow is architecturally correct. Only the transport was wrong on pre-5.8 firmware. | - -### Key conclusion - -**Problem 2 from the initial analysis was wrong.** The FIDO2 applet *does* handle GlobalPlatform SCP commands (INITIALIZE UPDATE / EXTERNAL AUTHENTICATE). The `ScpConnection` constructor flow — SELECT app, then SCP handshake on that applet — works identically for FIDO2 as it does for PIV/OATH/OTP. No restructuring needed. - -**Additionally**, on firmware 5.8+, the FIDO2 AID is registered on the USB CCID interface, so SCP works over USB — not just NFC. This was confirmed with MakeCredential (full PIN + touch + attestation) over SCP03 on a 5.8+ key connected via USB. - ---- - -## Changes Made - -### 1. `Fido2Session.cs` — Constructor updated - -```csharp -// BEFORE -public Fido2Session(IYubiKeyDevice yubiKey, ReadOnlyMemory? persistentPinUvAuthToken = null) - : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Fido2, keyParameters: null) - -// AFTER -public Fido2Session( - IYubiKeyDevice yubiKey, - ReadOnlyMemory? persistentPinUvAuthToken = null, - ScpKeyParameters? keyParameters = null) - : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Fido2, keyParameters) -``` - -### 2. `YubiKeyFeatureExtensions.cs` — Feature gate - -Added `YubiKeyCapabilities.Fido2` to the `YubiKeyFeature.Scp03` capability check so `ApplicationSession.GetConnection()` correctly validates FIDO2+SCP: - -```csharp -YubiKeyFeature.Scp03 => - yubiKeyDevice.FirmwareVersion >= FirmwareVersion.V5_3_0 - && (HasApplication(yubiKeyDevice, YubiKeyCapabilities.Piv) - || HasApplication(yubiKeyDevice, YubiKeyCapabilities.Oath) - || HasApplication(yubiKeyDevice, YubiKeyCapabilities.OpenPgp) - || HasApplication(yubiKeyDevice, YubiKeyCapabilities.Otp) - || HasApplication(yubiKeyDevice, YubiKeyCapabilities.YubiHsmAuth) - || HasApplication(yubiKeyDevice, YubiKeyCapabilities.Fido2)), // NEW -``` - -### 3. `FidoIntegrationTestBase.cs` — Fixed caller - -```csharp -// Named parameter to resolve ambiguity with new ScpKeyParameters parameter -new Fido2Session(testDevice, persistentPinUvAuthToken: persistentPinUvAuthToken) -``` - -### 4. `Scp03Tests.cs` — FIDO2 integration tests - -- Added `[InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)]` for 5.8+ USB CCID testing -- Added firmware 5.8+ skip condition for USB CCID tests -- Added `Scp03_Fido2Session_MakeCredential_Over_UsbCcid_Succeeds` — full MakeCredential with PIN + touch + attestation over SCP03 -- Added `Scp03_Fido2Session_Pre58_UsbCcid_Skips_Gracefully` — validates correct behavior on both 5.8+ and pre-5.8 keys - -### 5. `Scp11Tests.cs` — FIDO2 integration tests - -- Changed from single NfcSmartCard parameter to theory with both NFC and USB transports -- Added `[InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)]` with firmware 5.8+ skip condition - -### Build & Test Status - -- Solution builds with **0 errors, 0 warnings** -- All **3575 unit tests pass** -- Integration tests **pass over NFC** for both SCP03 and SCP11b (all firmware) -- Integration tests **pass over USB CCID** for SCP03 and SCP11b (firmware 5.8+) -- Full MakeCredential (PIN + touch + attestation) confirmed over SCP03 + USB CCID on 5.8+ - ---- - -## Connection Flows Explained - -### How YubiKey exposes interfaces over USB vs NFC - -Over **USB**, the YubiKey exposes multiple USB interfaces simultaneously: -- **HID FIDO** — for FIDO2/U2F (CTAP2 frames over HID reports) -- **HID Keyboard** — for OTP (keystroke injection) -- **CCID (SmartCard)** — for PIV, OATH, OTP, OpenPGP, YubiHSM Auth, Security Domain - -On **pre-5.8 firmware**, the FIDO2 applet is **only registered on the HID FIDO interface** over USB. It is not registered on CCID. This is a firmware-level decision. - -On **firmware 5.8+**, the FIDO2 applet is **registered on both HID FIDO and CCID** over USB. This means FIDO2 is selectable via SmartCard commands over USB, enabling SCP over USB. - -Over **NFC**, there is only **one interface** — ISO 14443 (SmartCard). All applets are selectable through it, including FIDO2, on all firmware versions. - -### The four FIDO2 connection flows - -#### Flow 1: USB, no SCP — works (all firmware) - -``` -Fido2Session(yubiKey, keyParameters: null) - → ConnectionFactory.CreateConnection(Fido2) - → _hidFidoDevice != null → YES - → return new FidoConnection(_hidFidoDevice) -``` - -Uses HID FIDO interface directly. No SELECT APDU. CTAP2 binary frames go over USB HID reports. This is the normal path — SmartCard/CCID is never involved. - -#### Flow 2: NFC, no SCP — works (all firmware) - -``` -Fido2Session(yubiKey, keyParameters: null) - → ConnectionFactory.CreateConnection(Fido2) - → _hidFidoDevice is null (NFC has no HID) - → _smartCardDevice != null → YES (NFC is always SmartCard) - → return new SmartCardConnection(_smartCardDevice, Fido2) - → SelectApplication() → SELECT FIDO2 AID → OK -``` - -Over NFC, there's no HID, so the SDK falls back to SmartCard. The FIDO2 AID is selectable because NFC exposes all applets. CTAP2 commands are wrapped in ISO 7816 APDUs. - -#### Flow 3: USB, with SCP — firmware-dependent - -**Firmware 5.8+: WORKS** - -``` -Fido2Session(yubiKey, keyParameters: scp03Key) - → ConnectionFactory.CreateScpConnection(Fido2, scp03Key) - → new ScpConnection(_smartCardDevice, Fido2, scp03Key) - → SelectApplication() → SELECT FIDO2 AID over CCID → OK (5.8+ registers FIDO2 on CCID) - → SCP handshake (INITIALIZE UPDATE / EXTERNAL AUTHENTICATE) → OK - → All subsequent CTAP2 APDUs encrypted → OK -``` - -On 5.8+, the FIDO2 AID is registered on both HID and CCID. SCP is a SmartCard-layer protocol, so it routes through CCID. SELECT succeeds, SCP handshake succeeds, and all FIDO2 operations (including MakeCredential with PIN + touch) work through the encrypted channel. - -**Pre-5.8 firmware: FAILS** - -``` -Fido2Session(yubiKey, keyParameters: scp03Key) - → ConnectionFactory.CreateScpConnection(Fido2, scp03Key) - → new ScpConnection(_smartCardDevice, Fido2, scp03Key) - → SelectApplication() → SELECT FIDO2 AID over CCID → 0x6A82 FAIL -``` - -On pre-5.8, the firmware does not register the FIDO2 applet on CCID over USB. SELECT fails with "file not found." This is not an SDK issue — the card simply doesn't have the FIDO2 AID on CCID. - -#### Flow 4: NFC, with SCP — works (all firmware) - -``` -Fido2Session(yubiKey, keyParameters: scp03Key) - → ConnectionFactory.CreateScpConnection(Fido2, scp03Key) - → new ScpConnection(_smartCardDevice, Fido2, scp03Key) - → SelectApplication() → SELECT FIDO2 AID → OK (NFC exposes all applets) - → SCP handshake (INITIALIZE UPDATE / EXTERNAL AUTHENTICATE) → OK - → All subsequent CTAP2 APDUs encrypted → OK -``` - -Over NFC, SmartCard is the only interface, all applets are selectable, and the FIDO2 applet correctly handles GlobalPlatform SCP commands. This is the primary use case: mobile devices communicating with YubiKeys over NFC with SCP channel protection. - -### Summary - -| Flow | SDK transport | Firmware | FIDO2 available? | SCP possible? | Result | -|------|--------------|----------|------------------|---------------|--------| -| 1. USB, no SCP | HID FIDO | All | Yes (HID) | N/A | **Works** | -| 2. NFC, no SCP | SmartCard | All | Yes (NFC exposes all) | N/A | **Works** | -| 3. USB, with SCP | SmartCard (CCID) | Pre-5.8 | No (not on CCID) | Blocked | **Fails** | -| 3. USB, with SCP | SmartCard (CCID) | 5.8+ | Yes (on CCID) | Yes | **Works** | -| 4. NFC, with SCP | SmartCard (NFC) | All | Yes (NFC exposes all) | Yes | **Works** | - -The constraint on pre-5.8: **SCP requires SmartCard. USB FIDO2 is HID-only. They are incompatible transports over USB.** Over NFC, both coexist on a single SmartCard interface. - -On 5.8+: **FIDO2 is available on both HID and CCID over USB.** SCP routes through CCID and succeeds. - ---- - -## Remaining Work - -### Rust FFI path -With FIDO2+SCP validated over NFC (all firmware) and USB CCID (5.8+): -- Wrap `Fido2Session` with `ScpKeyParameters` in NativeAOT exports -- Expose `Connection.SendCommand()` for custom APDUs through the encrypted channel -- Create `[UnmanagedCallersOnly]` entry points for Rust consumption -- On 5.8+ firmware, USB connections are viable — no NFC reader required - ---- - -## Reference Files - -| File | Role | -|------|------| -| `Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs` | Session constructor — accepts `ScpKeyParameters` | -| `Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs` | Feature gate — FIDO2 in SCP03 check | -| `Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs` | SCP connection — works as-is, no changes needed | -| `Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs` | Base class — `SelectApplication()` and `SetPipeline()` | -| `Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs` | Connection routing — FIDO2 prefers HID, SCP always uses SmartCard | -| `yubikit-android/.../fido/ctap/Ctap2Session.java:1650-1661` | Android reference — confirms FIDO2+SCP support | diff --git a/docs/rust-to-netsdk-ffi.md b/docs/rust-to-netsdk-ffi.md deleted file mode 100644 index 869ab2773..000000000 --- a/docs/rust-to-netsdk-ffi.md +++ /dev/null @@ -1,545 +0,0 @@ -# Rust-to-.NET SDK FFI: Technical Handover - -**Date:** 2026-03-17 -**From:** Dennis (SDK team) -**For:** Rust team -**Branch:** `feature/fido2-scp-support` - ---- - -## TL;DR - -The .NET SDK now supports FIDO2 over SCP03/SCP11. On firmware 5.8+, this works over both USB CCID and NFC. On pre-5.8 firmware, NFC is required. To expose this to Rust, we'd compile a NativeAOT shared library (`.dll`/`.so`/`.dylib`) with `[UnmanagedCallersOnly]` C-ABI exports. Rust links against it and calls functions like `fido2_session_open_scp03()` and `fido2_send_command()` — the .NET SDK handles all SCP encryption transparently. - -**However**, this approach ships a 15-30 MB .NET runtime in the native binary, requires per-platform builds, GCHandle lifecycle management, and NativeAOT trimming workarounds. If the Rust team only needs an SCP-encrypted APDU pipe (not the full FIDO2 command set), implementing SCP03 directly in Rust (~500 lines using `aes`/`cmac` crates + `pcsc`) is likely simpler, smaller, and easier to maintain. See the **Feasibility assessment** section for the full tradeoff analysis. - ---- - -## What this enables - -The .NET YubiKey SDK now supports FIDO2 sessions over SCP03 and SCP11 secure channels. This document describes how to expose that capability to Rust via a NativeAOT-compiled shared library. - -The end result: Rust sends cleartext FIDO2/CTAP2 APDUs across an FFI boundary. The .NET SDK handles SCP channel establishment, encryption, and key management transparently. Rust never touches SCP directly. - ---- - -## Architecture - -``` -┌─────────────────────────────────────────────────────┐ -│ Rust Application │ -│ │ -│ extern "C" { │ -│ fn fido2_session_open_scp03(...) -> i32; │ -│ fn fido2_send_command(...) -> i32; │ -│ } │ -└──────────────────┬──────────────────────────────────┘ - │ FFI (C ABI) - │ -┌──────────────────▼──────────────────────────────────┐ -│ Yubico.YubiKey.NativeInterop │ -│ (NativeAOT shared library) │ -│ │ -│ [UnmanagedCallersOnly] static methods │ -│ GCHandle-based opaque handles │ -│ Error codes (no exceptions across FFI) │ -└──────────────────┬──────────────────────────────────┘ - │ .NET project reference - │ -┌──────────────────▼──────────────────────────────────┐ -│ Yubico.YubiKey SDK │ -│ │ -│ Fido2Session(device, ScpKeyParameters) │ -│ ScpConnection → SCP03/SCP11 handshake │ -│ Encrypted APDU transport │ -└─────────────────────────────────────────────────────┘ -``` - ---- - -## Transport support for FIDO2+SCP - -FIDO2 over SCP support depends on firmware version: - -| Transport | Firmware | FIDO2 interface | SCP possible? | -|-----------|----------|----------------|---------------| -| USB, no SCP | All | HID FIDO | N/A (works) | -| USB, with SCP | Pre-5.8 | CCID — FIDO2 AID not on CCID | **No** | -| USB, with SCP | 5.8+ | CCID — FIDO2 AID registered on CCID | **Yes** | -| NFC, no SCP | All | SmartCard | N/A (works) | -| NFC, with SCP | All | SmartCard | **Yes** | - -**Why:** SCP is a SmartCard-layer protocol (ISO 7816 secure messaging) that requires CCID. On pre-5.8 firmware, the YubiKey exposes FIDO2 only on the HID interface over USB — not on CCID. On firmware 5.8+, the FIDO2 applet is registered on both HID and CCID over USB, so SCP works over USB. Over NFC, there is only one interface (ISO 14443 / SmartCard), so all applets — including FIDO2 — are selectable on all firmware versions. - -**Implication for Rust:** On firmware 5.8+, FIDO2+SCP works over USB — no NFC reader required. On pre-5.8 firmware, FIDO2+SCP requires NFC. The Rust application should check the firmware version and transport to determine SCP availability. - ---- - -## .NET NativeInterop project - -### Project setup - -```xml - - - - net8.0 - true - true - Yubico.YubiKey.NativeInterop - yubico_yubikey - - - - - - -``` - -### Publishing per platform - -```bash -# Windows (produces yubico_yubikey.dll) -dotnet publish -r win-x64 -c Release - -# Linux (produces yubico_yubikey.so) -dotnet publish -r linux-x64 -c Release - -# macOS Intel (produces yubico_yubikey.dylib) -dotnet publish -r osx-x64 -c Release - -# macOS Apple Silicon (produces yubico_yubikey.dylib) -dotnet publish -r osx-arm64 -c Release -``` - -Output lands in `bin/Release/net8.0//native/`. - -### NativeAOT prerequisites - -- .NET 8 SDK -- Platform-specific AOT toolchain: - - **Windows:** Visual Studio with C++ workload (for `link.exe`) - - **Linux:** `clang`, `zlib1g-dev` - - **macOS:** Xcode command line tools - ---- - -## FFI surface (C ABI) - -### Design principles - -| Principle | Implementation | -|-----------|---------------| -| No exceptions across FFI | Every function returns `int` error code. `0` = success. | -| Opaque handles | Managed objects are pinned via `GCHandle`, returned as `void*`. Rust never dereferences them. | -| Caller-allocated buffers | Rust allocates output buffers, passes `(ptr, len)`. Avoids cross-allocator issues. | -| No strings | Use byte arrays. UTF-8 where text is needed. | -| Separate functions per SCP variant | Simpler than passing discriminated unions across FFI. | - -### Error codes - -```c -#define YUBIKEY_OK 0 -#define YUBIKEY_ERR_NULL_HANDLE -1 -#define YUBIKEY_ERR_NOT_FOUND -2 -#define YUBIKEY_ERR_CONNECTION -3 -#define YUBIKEY_ERR_COMMAND -4 -#define YUBIKEY_ERR_BUFFER_SMALL -5 -#define YUBIKEY_ERR_INVALID_ARG -6 -``` - -### Proposed API - -#### Device enumeration - -```c -// Returns the number of connected YubiKeys, or 0 on error. -int yubikey_count_devices(void); - -// Opens a handle to the YubiKey at the given index. -// *out_handle receives an opaque device handle. -int yubikey_open_device(int index, void** out_handle); - -// Gets the serial number of the device. -int yubikey_device_serial(void* device_handle, int* out_serial); - -// Releases a device handle. Safe to call with NULL. -void yubikey_close_device(void* device_handle); -``` - -#### FIDO2 session lifecycle - -```c -// Opens a FIDO2 session without SCP (plain HID for USB, SmartCard for NFC). -int fido2_session_open(void* device_handle, void** out_session); - -// Opens a FIDO2 session with SCP03 using default keys. -// Works over NFC (all firmware) and USB CCID (firmware 5.8+). -int fido2_session_open_scp03(void* device_handle, void** out_session); - -// Opens a FIDO2 session with custom SCP03 keys. -// Each key pointer must reference exactly 16 bytes. -// Works over NFC (all firmware) and USB CCID (firmware 5.8+). -int fido2_session_open_scp03_custom( - void* device_handle, - const uint8_t* channel_mac_key, // 16 bytes - const uint8_t* channel_enc_key, // 16 bytes - const uint8_t* data_enc_key, // 16 bytes - void** out_session); - -// Closes and disposes a FIDO2 session. Safe to call with NULL. -void fido2_session_close(void* session_handle); -``` - -#### Commands - -```c -// Sends a raw APDU through the (optionally SCP-encrypted) channel. -// The APDU is cleartext — the SDK encrypts it if SCP is active. -// Response is written to out_buffer. *out_len receives actual length. -int fido2_send_command( - void* session_handle, - const uint8_t* apdu, // input APDU bytes - int apdu_len, - uint8_t* out_buffer, // caller-allocated response buffer - int buffer_len, - int* out_len); // actual response length - -// Gets authenticator info (FIDO2 GetInfo command). -// Writes a UTF-8 comma-separated list of supported versions. -int fido2_get_info_versions( - void* session_handle, - uint8_t* out_buffer, - int buffer_len, - int* out_len); -``` - ---- - -## C# implementation reference - -### Handle management pattern - -```csharp -// Pinning a managed object for FFI -var session = new Fido2Session(device, Scp03KeyParameters.DefaultKey); -var handle = GCHandle.Alloc(session); -*outSession = GCHandle.ToIntPtr(handle); // → void* in Rust - -// Recovering it later -var session = (Fido2Session)GCHandle.FromIntPtr(sessionHandle).Target!; - -// Releasing it -var handle = GCHandle.FromIntPtr(sessionHandle); -if (handle.Target is IDisposable d) d.Dispose(); -handle.Free(); -``` - -### [UnmanagedCallersOnly] method pattern - -```csharp -[UnmanagedCallersOnly(EntryPoint = "fido2_session_open_scp03")] -public static int OpenFido2SessionScp03(IntPtr deviceHandle, IntPtr* outSession) -{ - try - { - if (deviceHandle == IntPtr.Zero) return ERR_NULL_HANDLE; - - var device = (IYubiKeyDevice)GCHandle.FromIntPtr(deviceHandle).Target!; - var session = new Fido2Session(device, Scp03KeyParameters.DefaultKey); - - var sessionHandle = GCHandle.Alloc(session); - *outSession = GCHandle.ToIntPtr(sessionHandle); - return OK; - } - catch - { - return ERR_CONNECTION_FAILED; - } -} -``` - -### Key constraint: AllowUnsafeBlocks - -`[UnmanagedCallersOnly]` methods with pointer parameters (`IntPtr*`, `byte*`) require `true` in the `.csproj`. - ---- - -## Rust consumption - -### Linking - -```toml -# Cargo.toml -[build-dependencies] -# If using bindgen to auto-generate from a C header: -bindgen = "0.71" - -# Or declare the link manually: -# No extra deps needed — just extern "C" declarations -``` - -```rust -// build.rs (if needed) -fn main() { - // Point to the NativeAOT output directory - println!("cargo:rustc-link-search=native=path/to/native/output"); - println!("cargo:rustc-link-lib=dylib=yubico_yubikey"); -} -``` - -### Rust bindings - -```rust -use std::ffi::c_void; -use std::os::raw::c_int; - -const OK: c_int = 0; -const ERR_BUFFER_SMALL: c_int = -5; - -extern "C" { - fn yubikey_count_devices() -> c_int; - fn yubikey_open_device(index: c_int, out_handle: *mut *mut c_void) -> c_int; - fn yubikey_close_device(handle: *mut c_void); - - fn fido2_session_open_scp03( - device: *mut c_void, - out_session: *mut *mut c_void, - ) -> c_int; - fn fido2_session_close(session: *mut c_void); - - fn fido2_send_command( - session: *mut c_void, - apdu: *const u8, - apdu_len: c_int, - out_buf: *mut u8, - buf_len: c_int, - out_len: *mut c_int, - ) -> c_int; - - fn fido2_get_info_versions( - session: *mut c_void, - out_buf: *mut u8, - buf_len: c_int, - out_len: *mut c_int, - ) -> c_int; -} -``` - -### Safe Rust wrapper (suggested) - -```rust -use std::ptr; - -pub struct YubiKeyDevice { - handle: *mut c_void, -} - -impl YubiKeyDevice { - pub fn open(index: i32) -> Result { - let mut handle: *mut c_void = ptr::null_mut(); - let rc = unsafe { yubikey_open_device(index, &mut handle) }; - if rc != OK { return Err(rc); } - Ok(Self { handle }) - } -} - -impl Drop for YubiKeyDevice { - fn drop(&mut self) { - unsafe { yubikey_close_device(self.handle); } - } -} - -pub struct Fido2Session { - handle: *mut c_void, -} - -impl Fido2Session { - /// Opens a FIDO2 session with SCP03 encryption. - /// Works over NFC (all firmware) and USB CCID (firmware 5.8+). - pub fn open_scp03(device: &YubiKeyDevice) -> Result { - let mut handle: *mut c_void = ptr::null_mut(); - let rc = unsafe { fido2_session_open_scp03(device.handle, &mut handle) }; - if rc != OK { return Err(rc); } - Ok(Self { handle }) - } - - /// Sends a cleartext APDU. The .NET SDK encrypts it via SCP if active. - pub fn send_command(&self, apdu: &[u8]) -> Result, i32> { - let mut buf = vec![0u8; 4096]; - let mut out_len: c_int = 0; - let rc = unsafe { - fido2_send_command( - self.handle, - apdu.as_ptr(), - apdu.len() as c_int, - buf.as_mut_ptr(), - buf.len() as c_int, - &mut out_len, - ) - }; - if rc != OK { return Err(rc); } - buf.truncate(out_len as usize); - Ok(buf) - } -} - -impl Drop for Fido2Session { - fn drop(&mut self) { - unsafe { fido2_session_close(self.handle); } - } -} -``` - -### Usage example - -```rust -fn main() -> Result<(), i32> { - let count = unsafe { yubikey_count_devices() }; - println!("Found {} YubiKey(s)", count); - - let device = YubiKeyDevice::open(0)?; - let session = Fido2Session::open_scp03(&device)?; - - // Send a CTAP2 GetInfo command (0x04) through the SCP-encrypted channel - let get_info_cmd = [0x04]; - let response = session.send_command(&get_info_cmd)?; - println!("Response: {} bytes", response.len()); - - Ok(()) // Drop cleans up session and device handles -} -``` - ---- - -## Implementation plan - -### Phase 1: Skeleton (Windows first) - -1. Create `Yubico.YubiKey.NativeInterop` project with NativeAOT config -2. Implement device enumeration exports (`yubikey_count_devices`, `yubikey_open_device`, `yubikey_close_device`) -3. Implement `fido2_session_open` (plain, no SCP) and `fido2_session_close` -4. Publish for `win-x64`, verify Rust can link and call functions -5. Write a minimal Rust test that enumerates devices and opens/closes a session - -### Phase 2: SCP support - -1. Implement `fido2_session_open_scp03` and `fido2_session_open_scp03_custom` -2. Implement `fido2_send_command` (raw APDU passthrough) -3. Test over NFC or USB CCID (5.8+ firmware) with a YubiKey -4. Add SCP11 variants if needed - -### Phase 3: Cross-platform - -1. Publish for `linux-x64` and `osx-arm64` -2. Set up CI to produce all three native libraries -3. Package as a Rust crate with platform-specific lib selection - -### Phase 4: Production hardening - -1. Thread safety audit (one session per thread, or add locking) -2. Comprehensive error codes with `fido2_get_last_error` for detailed messages -3. Logging bridge (route .NET SDK logs to Rust's tracing/log) -4. Memory leak testing (ensure all GCHandles are freed) - ---- - -## Known risks and considerations - -| Risk | Mitigation | -|------|------------| -| **NativeAOT trims unused code** | The SDK uses reflection in some areas. May need `` or `rd.xml` to preserve types. Test early. | -| **NativeAOT binary size** | Expect 15-30 MB for the shared library. The full .NET runtime is embedded. | -| **Thread safety** | `Fido2Session` is not thread-safe. Document that each session handle must be used from one thread at a time. | -| **GCHandle leaks** | If Rust crashes or doesn't call `_close` functions, managed objects leak. Consider a timeout/finalizer strategy. | -| **USB device access** | On Linux, requires udev rules for non-root access. On Windows, requires the correct smart card drivers for NFC readers. | -| **Transport compatibility** | FIDO2+SCP requires CCID: works over NFC (all firmware) and USB CCID (5.8+). On pre-5.8 firmware over USB, Rust code must handle graceful fallback. | - ---- - -## Files to reference - -| File | What it shows | -|------|---------------| -| `Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs` | Constructor accepting `ScpKeyParameters`, XML docs with transport notes | -| `Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs` | How the SCP channel is established | -| `Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs` | How connections are routed (HID vs SmartCard) | -| `Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs` | Feature gate — FIDO2 in SCP03 capability check | -| `docs/findings-and-assumptions.md` | Full investigation of FIDO2+SCP transport constraints | - ---- - -## Feasibility assessment - -### What the FFI path actually buys you - -Through all the NativeAOT machinery, the .NET SDK provides: - -1. **SCP03 handshake** — INITIALIZE UPDATE + EXTERNAL AUTHENTICATE (2 APDUs, AES-128 key derivation) -2. **SCP11 handshake** — EC key agreement, certificate chain validation, session key derivation -3. **APDU encryption/MAC** — AES-CBC encryption + CMAC per the GlobalPlatform SCP spec -4. **SELECT FIDO2 AID** — a fixed 7-byte APDU -5. **Device enumeration** — PC/SC reader discovery - -That's the entire value crossing the FFI boundary. Everything else (CTAP2 commands, credential management, PIN handling) happens in Rust anyway. - -### Cost of the FFI path - -| Cost | Detail | -|------|--------| -| **Binary size** | 15-30 MB native library (embeds .NET runtime + GC + JIT stubs) | -| **Build complexity** | NativeAOT requires MSVC (Windows), clang (Linux), Xcode (macOS) — per platform | -| **Trimming issues** | The SDK uses reflection (logging, DI, serialization). Expect `rd.xml` and `` debugging. | -| **FFI surface maintenance** | Every SDK API change requires updating the interop layer and Rust bindings | -| **Debugging** | Errors are opaque integers. Stack traces don't cross the FFI boundary. | -| **GCHandle lifecycle** | Managed objects pinned via `GCHandle` leak if Rust doesn't call `_close`. No destructor safety net. | -| **Two runtimes** | Process hosts both the .NET GC and Rust's allocator. Memory behavior is less predictable. | - -### Alternative: native Rust SCP implementation - -SCP03 is a well-specified protocol (GlobalPlatform Card Specification, Amendment D): - -| Component | Rust implementation | Complexity | -|-----------|-------------------|------------| -| Key derivation | `cmac` crate (AES-CMAC) | ~30 lines | -| Session encryption | `aes` crate (AES-128-CBC) | ~20 lines | -| MAC generation | `cmac` crate | ~20 lines | -| APDU wrapping | Prepend MAC, encrypt payload | ~40 lines | -| Handshake | INITIALIZE UPDATE + EXTERNAL AUTHENTICATE | ~100 lines | -| NFC communication | `pcsc` crate (PC/SC smart card) | ~50 lines | -| SELECT application | Fixed APDU construction | ~10 lines | -| **Total** | | **~300-500 lines** | - -### Comparison - -| | NativeAOT FFI | Native Rust SCP | -|---|---|---| -| **Time to implement** | 2-3 weeks | 1-2 weeks | -| **Binary size** | 15-30 MB | ~1 MB | -| **Runtime dependencies** | .NET 8 runtime (embedded) | None (static linking) | -| **Debugging** | Painful (cross-runtime) | Normal Rust tooling | -| **Maintenance burden** | Coupled to .NET SDK versions | Self-contained | -| **Platform builds** | 3 separate NativeAOT publishes | `cargo build` (cross-compile) | -| **SCP03 support** | Free (SDK has it) | ~500 lines of Rust | -| **SCP11 support** | Free (SDK has it) | Significant work (~2000 lines: EC key agreement, X.509 cert chains, multiple key slots) | -| **Full FIDO2 command set** | Free (SDK has it) | Must implement from scratch | - -### Recommendation - -**If you need SCP03 only:** Implement in Rust. The protocol is simple, the crypto crates are mature, and you avoid all FFI complexity. The `pcsc` crate handles NFC reader access. - -**If you need SCP11:** Evaluate whether the EC key agreement and certificate handling justifies the FFI overhead. SCP11 is substantially more complex than SCP03. The .NET SDK has a battle-tested implementation. - -**If you need the full FIDO2 command set (credential management, PIN/UV, attestation):** The FFI path makes sense — reimplementing all of FIDO2 in Rust is weeks of work and the SDK already has it. - -**If you only need an encrypted APDU pipe:** You're shipping 30 MB of .NET runtime for something that's 500 lines of Rust. Don't do it. - ---- - -## Questions for the Rust team - -1. **APDU format:** Do you want to send raw ISO 7816 APDUs, or CTAP2 command bytes? The SDK can handle either — determines which `SendCommand` overload to expose. -2. **SCP11 support:** Do you need SCP11b in addition to SCP03? If so, the FFI surface needs a way to pass EC key material (public key, certificates). -3. **Key management:** Will SCP keys be hardcoded, loaded from config, or provisioned at runtime? Affects whether we need `fido2_session_open_scp03_custom` or just `_scp03` with defaults. -4. **Error detail:** Is the integer error code sufficient, or do you want a `fido2_get_last_error(buf, len)` function that returns the .NET exception message as UTF-8? -5. **Concurrency:** Will multiple Rust threads access sessions simultaneously? If so, we need to add locking in the interop layer.