Skip to content

Fast USB interface reclaim #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Yubico.NET.SDK.sln
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sdk Programming Guide", "Sd
Yubico.YubiKey\docs\users-manual\sdk-programming-guide\secure-channel-protocol-3.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\secure-channel-protocol-3.md
Yubico.YubiKey\docs\users-manual\sdk-programming-guide\sensitive-data.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\sensitive-data.md
Yubico.YubiKey\docs\users-manual\sdk-programming-guide\threads.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\threads.md
Yubico.YubiKey\docs\users-manual\sdk-programming-guide\appcompat.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\appcompat.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Apdu", "Apdu", "{BA394A0D-B336-4B6E-83B8-B41FC165D6D9}"
Expand Down
106 changes: 106 additions & 0 deletions Yubico.YubiKey/docs/users-manual/sdk-programming-guide/appcompat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
uid: AppCompat
---

<!-- Copyright 2024 Yubico AB
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->

# Maintaining compatibility

This article describes the various decisions that the SDK makes in order to maintain application and source
compatibility across our versions.

## App-compat strategy for this SDK

The .NET SDK strives to maintain both source and behavioral compatibility across its releases.

**Source compatibility** is the promise that your source code should continue to compile as-is when you update your
application to the latest version of the SDK. This promise extends to "minor" (feature) and "patch" (bug-fix) releases
of the SDK. Additionally, we only make this guarantee for the `Yubico.YubiKey` and the `Yubico.Core` assemblies. Since
`Yubico.NativeShims` and `Yubico.DotnetPolyfills` are meant to be purely for internal use, we *do not* make any
guarantees here.

**Behavioral compatibility** is the promise that your application will behave exactly the same after you have upgraded
the YubiKey SDK version. Maintaining this guarantee is far more difficult and is sometimes simply not possible. However,
we will continue to do our best to maintain behavioral stability across releases. If a behavioral change is
necessitated, such as fixing a bug, we may choose to simply fix the issue. This is far more likely to occur if the bug
prevented the feature from ever working in the first place and no workaround was present. If the change is more nuanced
than that, or it is changing behavior for some other reason, we have a separate mechanism that we've started using so
that these behavior changes may be managed through an opt-in or opt-out decision.

There are two exceptions to these promises:

1. Sometimes a breaking change is unavoidable. Perhaps a new YubiKey feature was released that is simply impossible to
express with the existing shape of the API. Or a bug was discovered, and it simply must be addressed. In these cases,
we will do everything in our power to first mark the affected types or members with the `ObsoleteAttribute` so that
you are alerted the fact that there's an issue with the old usage. The attribute will contain text that will result
in a usage warning when you recompile. This text will be included in the warning message and will point you to the
new API that should be used instead. The old API will remain for several minor releases before we consider it safe to
remove entirely. A major release would remove all obsolete APIs in one go.

2. You will note that the promise is only made for minor and patch releases. For example, upgrading feature releases
(i.e. `1.9.1` to `1.10.0`) or upgrading patch releases (i.e. `1.9.0` to `1.9.1`) have this guarantee. What has been
omitted here is "major" releases (i.e. `1.10.0` to `2.0.0`). Major version releases are our chance to make broader
changes that address design-level issues. It should be expected that there will be source level breaking changes when
a major version is released.

Our SDK does *not* make any promises around **Application Binary Interface (ABI)** stability. This expectation is
generally far less common in the .NET ecosystem to begin with, however there are two very important implications here:

1. You *must* recompile your code against a new version of our SDK. Simply replacing our assemblies with a newer version
is **not** supported and could result in undefined behavior and bugs in your application's behavior.

2. If an enumeration does not have an explicitly defined value, you should assume that the underlying value may change.
While these changes should not result in any changes to behavior (assuming you've recompiled) it does mean that these
values should not be serialized and stored across versions. If you need to persist these values for whatever reason,
it is strongly recommended you create your own stable values to map to, or use another mechanism that does not depend
on the specific compiler-generated enumeration value.

## Managing behavior changes through app-compat switches

Sometimes it's unavoidable that the SDK must make a behavior breaking change. For example: a bug has been addressed that
causes subtle behavior changes that have existed for many releases. Or perhaps an optimization has been made that may
result in different timings that could have an effect on UI applications.

In these cases, we've introduced a new mechanism for adjusting these behaviors through the use of app-compat switches.
These switches use the
[`AppContext.SetSwitch`](https://learn.microsoft.com/en-us/dotnet/api/system.appcontext.setswitch) mechanism exposed by
the .NET Base Class Library.

Whether a behavior change is opt-in or opt-out will be decided on a case-by-case basis. Generally, if we view the change
to be a net positive and have a low risk of observable changes to an application, we will make the change opt-out. That
means, you will get the new behavior by default. Only if the change causes your application problems should you consider
setting the switch to disable that behavior.

For more observable or impactful changes, or changes that would benefit a smaller subset of consumers, we will make the
change opt-in. That is, the existing behaviors will be maintained, and your application must explicitly call `SetSwitch`
with a value of `true`.

This decision is clearly very subjective. Any time a behavior change is made, there is a high likelihood that at least
one consumer will be adversely affected no matter which behavior we choose. That's why we've introduced these switches
in the first place. There will always be a case where someone will need to override out decision. This is your mechanism
to do so.

All of our compatibility switch names are defined in two central classes:

- [YubiKeyCompatSwitches](xref:Yubico.YubiKey.YubiKeyCompatSwitches) - This class holds all the compatibility switches
that affect the behaviors of the `Yubico.YubiKey` assembly.
- [CoreCompatSwitches](xref:Yubico.Core.CoreCompatSwitches) - This class holds all the compatibility switches that
affect the `Yubico.Core` assembly. `Yubico.Core` serves as our platform abstraction layer, so switches here may only
impact a certain operating system or a certain downstream dependency. While not YubiKey specific, it may affect things
like enumeration and eventing of YubiKeys.

Each flag will have a clear explanation of what behavior it affects, what the default is, and what the impact of
overriding the default should be. Use these constants as the value for the `switchName` parameter of
`AppContext.SetSwitch`.
5 changes: 3 additions & 2 deletions Yubico.YubiKey/src/Yubico/YubiKey/FirmwareVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ namespace Yubico.YubiKey
public class FirmwareVersion : IComparable<FirmwareVersion>, IComparable, IEquatable<FirmwareVersion>
{
#region Frequently Used Versions
// Note that these are for internal use. Later, we want to have something
// that allows users to query for specific capabilities instead of versions.
// Note that these are for internal use. It's expected that SDK users should call `.HasFeature` on the
// YubiKey device to check for features instead of FW versions.
internal static readonly FirmwareVersion All = new FirmwareVersion(1, 0, 0);
internal static readonly FirmwareVersion V2_0_0 = new FirmwareVersion(2, 0, 0);
internal static readonly FirmwareVersion V2_1_0 = new FirmwareVersion(2, 1, 0);
Expand All @@ -40,6 +40,7 @@ public class FirmwareVersion : IComparable<FirmwareVersion>, IComparable, IEquat
internal static readonly FirmwareVersion V5_3_0 = new FirmwareVersion(5, 3, 0);
internal static readonly FirmwareVersion V5_4_2 = new FirmwareVersion(5, 4, 2);
internal static readonly FirmwareVersion V5_4_3 = new FirmwareVersion(5, 4, 3);
internal static readonly FirmwareVersion V5_6_0 = new FirmwareVersion(5, 6, 0);
#endregion

public byte Major { get; set; }
Expand Down
29 changes: 29 additions & 0 deletions Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyCompatSwitches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2024 Yubico AB
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace Yubico.YubiKey
{
/// <summary>
/// Compatibility switch names that can be used with `AppContext.SetSwitch` to control breaking behavioral changes
/// within the `Yubico.YubiKey` layer.
/// </summary>
public static class YubiKeyCompatSwitches
{
/// <summary>
/// If set to true, the SDK will ignore whether a YubiKey is capable of faster USB interface switching
/// and always use the 3-second reclaim timeout.
/// </summary>
public const string UseOldReclaimTimeoutBehavior = nameof(UseOldReclaimTimeoutBehavior);
}
}
22 changes: 18 additions & 4 deletions Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,12 @@ private Transport GetTransportIfOnlyDevice()
// failures (i.e. exceptions).
private void WaitForReclaimTimeout(Transport newTransport)
{
// Newer YubiKeys are able to switch interfaces much, much faster. Maybe this is being paranoid, but we
// should still probably wait a few milliseconds for things to stabilize. But definitely not the full
// three seconds! For older keys, we use a value of 3.01 seconds to give us a little wiggle room as the
// YubiKey's measurement for the reclaim timout is likely not as accurate as our system clock.
TimeSpan reclaimTimeout = CanFastReclaim() ? TimeSpan.FromMilliseconds(100) : TimeSpan.FromSeconds(3.01);

// We're only affected by the reclaim timeout if we're switching USB transports.
if (_lastActiveTransport == newTransport)
{
Expand All @@ -1016,13 +1022,10 @@ private void WaitForReclaimTimeout(Transport newTransport)
_lastActiveTransport,
newTransport);

// We use 3.01 seconds to give us a little wiggle room as the YubiKey's measurement
// for the reclaim timeout is likely not as accurate as the system's clock.
var reclaimTimeout = TimeSpan.FromSeconds(3.01);
TimeSpan timeSinceLastActivation = DateTime.Now - GetLastActiveTime();

// If we haven't already waited the duration of the reclaim timeout, we need to do so.
// Otherwise we've already waited and can immediately switch the transport.
// Otherwise, we've already waited and can immediately switch the transport.
if (timeSinceLastActivation < reclaimTimeout)
{
TimeSpan waitNeeded = reclaimTimeout - timeSinceLastActivation;
Expand All @@ -1038,5 +1041,16 @@ private void WaitForReclaimTimeout(Transport newTransport)

_log.LogInformation("Reclaim timeout has lapsed. It is safe to switch USB transports.");
}

private bool CanFastReclaim()
{
if (AppContext.TryGetSwitch(YubiKeyCompatSwitches.UseOldReclaimTimeoutBehavior, out bool useOldBehavior) &&
useOldBehavior)
{
return false;
}

return this.HasFeature(YubiKeyFeature.FastUsbReclaim);
}
}
}
7 changes: 6 additions & 1 deletion Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public enum YubiKeyFeature
/// </summary>
Scp03,

/// <summary>
/// The YubiKey is capable of switching USB interfaces without the lengthy 3-second reclaim timeout.
/// </summary>
FastUsbReclaim,

// OTP application features

/// <summary>
Expand Down Expand Up @@ -236,7 +241,7 @@ public enum YubiKeyFeature
YubiHsmAuthApplication,

/// <summary>
/// Allows temporarily disabling NFC (added in 5.7)
/// Allows temporarily disabling NFC until the next time the YubiKey is powered over USB.
/// </summary>
ManagementNfcRestricted,
}
Expand Down
3 changes: 3 additions & 0 deletions Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ public static bool HasFeature(this IYubiKeyDevice yubiKeyDevice, YubiKeyFeature
|| HasApplication(yubiKeyDevice, YubiKeyCapabilities.Oath)
|| HasApplication(yubiKeyDevice, YubiKeyCapabilities.OpenPgp)),

YubiKeyFeature.FastUsbReclaim =>
yubiKeyDevice.FirmwareVersion >= FirmwareVersion.V5_6_0,

YubiKeyFeature.YubiHsmAuthApplication =>
yubiKeyDevice.FirmwareVersion >= FirmwareVersion.V5_4_3
&& HasApplication(yubiKeyDevice, YubiKeyCapabilities.YubiHsmAuth),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public class ReclaimTimeoutTests
[Fact]
public void SwitchingBetweenTransports_ForcesThreeSecondWait()
{
// Force the old behavior even for newer YubiKeys.
AppContext.SetSwitch(YubiKeyCompatSwitches.UseOldReclaimTimeoutBehavior, true);

using Logger? log = new LoggerConfiguration()
.Enrich.With(new ThreadIdEnricher())
.WriteTo.Console(
Expand All @@ -54,7 +57,7 @@ public void SwitchingBetweenTransports_ForcesThreeSecondWait()
.AddFilter(level => level >= LogLevel.Information));

// TEST ASSUMPTION: This test requires FIDO. On Windows, that means this test case must run elevated (admin).
IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5Fips);
IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5);

// Ensure all interfaces are active
if (testDevice.EnabledUsbCapabilities != YubiKeyCapabilities.All)
Expand Down