Skip to content

Commit 41c554e

Browse files
authored
Merge pull request #493 from Yubico/yubikit-performance
perf/resilience: reduce OTP polling latency and add runtime resilience gates
2 parents 10c1661 + 82b32a4 commit 41c554e

49 files changed

Lines changed: 2438 additions & 478 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,10 @@ csharp_style_prefer_readonly_struct = true
129129
csharp_style_prefer_readonly_struct_member = true
130130

131131
# Code-block preferences
132-
csharp_prefer_braces = false:when_multiline
132+
csharp_prefer_braces = when_multiline:none
133+
csharp_style_prefer_conditional_expression_over_return = true:suggestion
134+
dotnet_diagnostic.IDE0046.severity = suggestion
135+
dotnet_diagnostic.IDE0072.severity = suggestion
133136
csharp_prefer_simple_using_statement = true
134137
csharp_style_namespace_declarations = file_scoped
135138
csharp_style_prefer_method_group_conversion = true

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ jobs:
5555
- name: Run unit tests
5656
run: dotnet toolchain.cs test
5757

58+
- name: Run runtime resilience checks
59+
run: dotnet toolchain.cs -- resilience --fast
60+
5861
- name: Pack NuGet packages
5962
run: dotnet toolchain.cs pack --package-version 2.0.0-preview.${{ github.run_number }}
6063

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Do not run `dotnet build` directly. Use `dotnet toolchain.cs build` or `dotnet toolchain.cs build --project Piv`.
1111
- Do not run `dotnet test` directly. Unit tests mix xUnit v3/Microsoft Testing Platform and xUnit v2; `dotnet toolchain.cs test` detects and invokes the right runner.
1212
- Focus tests with both module and filter when possible: `dotnet toolchain.cs test --project Fido2 --filter "Method~Sign"`.
13+
- When touching Core runtime loops, polling paths, recovery logic, or listener lifecycle cleanup, also run `dotnet toolchain.cs -- resilience --fast`.
1314
- `--integration` requires `--project`: `dotnet toolchain.cs -- test --integration --project Piv --smoke`.
1415
- `--smoke` skips `Slow` and `RequiresUserPresence`; agents should not run touch/insert/remove tests unless a human explicitly coordinates hardware.
1516
- Use `dotnet toolchain.cs -- --help` when arguments act strangely; `--help` and some options need the `--` separator.
@@ -30,6 +31,7 @@
3031
- Platform interop belongs under `src/Core/src/PlatformInterop/{Windows,MacOS,Linux,Desktop}` with safe handles and `SdkPlatformInfo` platform selection.
3132
- FIDO2 over USB primarily uses HID FIDO. SmartCard/CCID FIDO2 is supported over NFC and over USB when the FIDO2 AID is exposed; USB SmartCard FIDO2 requires firmware 5.8.0+.
3233
- `TlvBuilder` and `DisposableTlvList` must be disposed.
34+
- Listener/native retry loops must block, back off, exit, or throttle on every failure path; add no-hardware fault-injection tests for persistent native errors.
3335

3436
## Hardware And Integration Tests
3537
- Integration tests require authorized YubiKey hardware. The shared test infrastructure reads `YubiKeyTests:AllowedSerialNumbers` from `appsettings.json`; an empty/missing allow list can hard-fail with `Environment.Exit(-1)`, and unauthorized devices are filtered out.
@@ -52,6 +54,7 @@
5254
- Do not add validation-only tests that just prove `ArgumentNullException.ThrowIfNull` or framework type checks work.
5355
- Do not add skipped placeholder tests. If behavior cannot be unit-tested, document the limitation and point to an integration path.
5456
- Use fake connections, queued APDU responses, and byte/vector assertions for protocol behavior; use integration tests only for real hardware behavior.
57+
- Runtime resilience tests should use fakes/seams and must not self-skip because PC/SC, HID, or YubiKey hardware is unavailable.
5558

5659
## Git And Release Notes
5760
- The workflow file currently triggers on `yubikit` and `yubikit-applets`; verify the active plan/branch before assuming a PR base.

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@
3939
<PackageVersion Include="Spectre.Console" Version="0.49.1" />
4040
<PackageVersion Include="Spectre.Console.Cli" Version="0.49.1" />
4141
<!-- Build Tools -->
42+
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
4243
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
4344
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
4445
<PackageVersion Include="Bullseye" Version="5.0.0" />
4546
<PackageVersion Include="SimpleExec" Version="12.0.0" />
4647
</ItemGroup>
47-
</Project>
48+
</Project>
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Columns;
3+
using BenchmarkDotNet.Configs;
4+
using BenchmarkDotNet.Diagnosers;
5+
using BenchmarkDotNet.Environments;
6+
using BenchmarkDotNet.Exporters;
7+
using BenchmarkDotNet.Exporters.Csv;
8+
using BenchmarkDotNet.Exporters.Json;
9+
using BenchmarkDotNet.Jobs;
10+
using BenchmarkDotNet.Reports;
11+
using BenchmarkDotNet.Running;
12+
using BenchmarkDotNet.Validators;
13+
using Yubico.YubiKit.Core.Abstractions;
14+
using Yubico.YubiKit.Core.Devices;
15+
using Yubico.YubiKit.Core.Protocols.Fido.Hid;
16+
using Yubico.YubiKit.Core.Protocols.SmartCard.Apdu;
17+
using Yubico.YubiKit.Core.Sessions;
18+
using Yubico.YubiKit.Core.Transports.Hid;
19+
using Yubico.YubiKit.Core.Transports.SmartCard;
20+
using Yubico.YubiKit.Management;
21+
22+
var config = YubiKitBenchmarkConfig.Create();
23+
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
24+
25+
internal sealed class YubiKitBenchmarkConfig : ManualConfig
26+
{
27+
private YubiKitBenchmarkConfig()
28+
{
29+
var runId = DateTimeOffset.UtcNow.ToString("yyyyMMdd-HHmmss");
30+
ArtifactsPath = Path.Combine(FindRepoRoot(), "artifacts", "benchmarks", runId);
31+
32+
AddJob(Job.ShortRun
33+
.WithRuntime(CoreRuntime.Core10_0)
34+
.WithId("net10-short"));
35+
36+
AddColumnProvider(DefaultColumnProviders.Instance);
37+
AddDiagnoser(MemoryDiagnoser.Default);
38+
AddDiagnoser(ThreadingDiagnoser.Default);
39+
AddDiagnoser(ExceptionDiagnoser.Default);
40+
AddDiagnoser(new EventPipeProfiler(EventPipeProfile.CpuSampling));
41+
AddExporter(MarkdownExporter.GitHub);
42+
AddExporter(HtmlExporter.Default);
43+
AddExporter(CsvExporter.Default);
44+
AddExporter(CsvMeasurementsExporter.Default);
45+
AddExporter(JsonExporter.Full);
46+
AddExporter(RPlotExporter.Default);
47+
AddValidator(JitOptimizationsValidator.FailOnError);
48+
AddLogger(DefaultConfig.Instance.GetLoggers().ToArray());
49+
}
50+
51+
public static IConfig Create() => new YubiKitBenchmarkConfig();
52+
53+
private static string FindRepoRoot()
54+
{
55+
var current = AppContext.BaseDirectory;
56+
while (!string.IsNullOrEmpty(current))
57+
{
58+
if (Directory.Exists(Path.Combine(current, ".git")))
59+
return current;
60+
61+
current = Directory.GetParent(current)?.FullName;
62+
}
63+
64+
return Directory.GetCurrentDirectory();
65+
}
66+
}
67+
68+
public abstract class YubiKeyHardwareBenchmarkBase
69+
{
70+
protected IYubiKey Device { get; private set; } = null!;
71+
72+
protected void SetupDevice(ConnectionType requiredConnection)
73+
{
74+
Device = FindDevice(requiredConnection).GetAwaiter().GetResult();
75+
}
76+
77+
[GlobalCleanup]
78+
public void Cleanup() => YubiKeyManager.Shutdown();
79+
80+
private static async Task<IYubiKey> FindDevice(ConnectionType requiredConnection)
81+
{
82+
var devices = await YubiKeyManager.FindAllAsync(ConnectionType.All, forceRescan: true)
83+
.ConfigureAwait(false);
84+
85+
return devices.FirstOrDefault(device => device.SupportsConnection(requiredConnection))
86+
?? throw new InvalidOperationException(
87+
$"No connected YubiKey exposes {requiredConnection}. Connect the YubiKey 5.8 device before running benchmarks.");
88+
}
89+
}
90+
91+
[MemoryDiagnoser]
92+
[ThreadingDiagnoser]
93+
[ExceptionDiagnoser]
94+
public class YubiKeyDiscoveryBenchmarks
95+
{
96+
[GlobalSetup]
97+
public void Setup()
98+
{
99+
_ = YubiKeyManager.FindAllAsync(ConnectionType.All, forceRescan: true)
100+
.GetAwaiter()
101+
.GetResult();
102+
}
103+
104+
[GlobalCleanup]
105+
public void Cleanup() => YubiKeyManager.Shutdown();
106+
107+
[Benchmark(Baseline = true)]
108+
public async Task<int> CachedFindAll()
109+
{
110+
var devices = await YubiKeyManager.FindAllAsync(ConnectionType.All, forceRescan: false)
111+
.ConfigureAwait(false);
112+
return devices.Count;
113+
}
114+
115+
[Benchmark]
116+
public async Task<int> ForcedRescanFindAll()
117+
{
118+
var devices = await YubiKeyManager.FindAllAsync(ConnectionType.All, forceRescan: true)
119+
.ConfigureAwait(false);
120+
return devices.Count;
121+
}
122+
123+
[Benchmark]
124+
public async Task<int> ColdFindAll()
125+
{
126+
await YubiKeyManager.ShutdownAsync().ConfigureAwait(false);
127+
var devices = await YubiKeyManager.FindAllAsync(ConnectionType.All, forceRescan: false)
128+
.ConfigureAwait(false);
129+
return devices.Count;
130+
}
131+
}
132+
133+
[MemoryDiagnoser]
134+
[ThreadingDiagnoser]
135+
[ExceptionDiagnoser]
136+
public class SmartCardManagementBenchmarks : YubiKeyHardwareBenchmarkBase
137+
{
138+
[GlobalSetup]
139+
public void Setup() => SetupDevice(ConnectionType.SmartCard);
140+
141+
[Benchmark(Baseline = true)]
142+
public async Task<bool> ConnectSmartCard()
143+
{
144+
await using var connection = await Device.ConnectAsync<ISmartCardConnection>().ConfigureAwait(false);
145+
return connection.SupportsExtendedApdu();
146+
}
147+
148+
[Benchmark]
149+
public async Task<int> SelectManagementOverSmartCard()
150+
{
151+
var connection = await Device.ConnectAsync<ISmartCardConnection>().ConfigureAwait(false);
152+
using var protocol = PcscProtocolFactory<ISmartCardConnection>.Create().Create(connection);
153+
var response = await protocol.SelectAsync(ApplicationIds.Management).ConfigureAwait(false);
154+
return response.Length;
155+
}
156+
157+
[Benchmark]
158+
public async Task<int> GetDeviceInfoOverSmartCard()
159+
{
160+
await using var session = await Device.CreateManagementSessionAsync(
161+
preferredConnection: ConnectionType.SmartCard)
162+
.ConfigureAwait(false);
163+
164+
var info = await session.GetDeviceInfoAsync().ConfigureAwait(false);
165+
return info.SerialNumber ?? 0;
166+
}
167+
}
168+
169+
[MemoryDiagnoser]
170+
[ThreadingDiagnoser]
171+
[ExceptionDiagnoser]
172+
public class FidoHidManagementBenchmarks : YubiKeyHardwareBenchmarkBase
173+
{
174+
[GlobalSetup]
175+
public void Setup() => SetupDevice(ConnectionType.HidFido);
176+
177+
[Benchmark(Baseline = true)]
178+
public async Task<int> ConnectFidoHid()
179+
{
180+
await using var connection = await Device.ConnectAsync<IFidoHidConnection>().ConfigureAwait(false);
181+
return connection.PacketSize;
182+
}
183+
184+
[Benchmark]
185+
public async Task<int> CreateManagementSessionOverFidoHid()
186+
{
187+
await using var session = await Device.CreateManagementSessionAsync(
188+
preferredConnection: ConnectionType.HidFido)
189+
.ConfigureAwait(false);
190+
191+
return session.FirmwareVersion.Major;
192+
}
193+
194+
[Benchmark]
195+
public async Task<int> GetDeviceInfoOverFidoHid()
196+
{
197+
await using var session = await Device.CreateManagementSessionAsync(
198+
preferredConnection: ConnectionType.HidFido)
199+
.ConfigureAwait(false);
200+
201+
var info = await session.GetDeviceInfoAsync().ConfigureAwait(false);
202+
return info.SerialNumber ?? 0;
203+
}
204+
}
205+
206+
[MemoryDiagnoser]
207+
[ThreadingDiagnoser]
208+
[ExceptionDiagnoser]
209+
public class OtpHidManagementBenchmarks : YubiKeyHardwareBenchmarkBase
210+
{
211+
[GlobalSetup]
212+
public void Setup() => SetupDevice(ConnectionType.HidOtp);
213+
214+
[Benchmark(Baseline = true)]
215+
public async Task<int> ConnectOtpHid()
216+
{
217+
await using var connection = await Device.ConnectAsync<IOtpHidConnection>().ConfigureAwait(false);
218+
return connection.FeatureReportSize;
219+
}
220+
221+
[Benchmark]
222+
public async Task<int> CreateManagementSessionOverOtpHid()
223+
{
224+
await using var session = await Device.CreateManagementSessionAsync(
225+
preferredConnection: ConnectionType.HidOtp)
226+
.ConfigureAwait(false);
227+
228+
return session.FirmwareVersion.Major;
229+
}
230+
231+
[Benchmark]
232+
public async Task<int> GetDeviceInfoOverOtpHid()
233+
{
234+
await using var session = await Device.CreateManagementSessionAsync(
235+
preferredConnection: ConnectionType.HidOtp)
236+
.ConfigureAwait(false);
237+
238+
var info = await session.GetDeviceInfoAsync().ConfigureAwait(false);
239+
return info.SerialNumber ?? 0;
240+
}
241+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<RootNamespace>Yubico.YubiKit.PerformanceBenchmarks</RootNamespace>
7+
<AssemblyName>Yubico.YubiKit.PerformanceBenchmarks</AssemblyName>
8+
<IsPackable>false</IsPackable>
9+
<PlatformTarget>AnyCPU</PlatformTarget>
10+
<DebugType>pdbonly</DebugType>
11+
<DebugSymbols>true</DebugSymbols>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="BenchmarkDotNet" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<ProjectReference Include="../../src/Core/src/Yubico.YubiKit.Core.csproj" />
20+
<ProjectReference Include="../../src/Management/src/Yubico.YubiKit.Management.csproj" />
21+
</ItemGroup>
22+
23+
</Project>

docs/architecture/physical-device-model.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ by `AvailableConnections`. This document explains the model, how to discover and
77
metadata lives, how each applet picks a transport, and how to migrate code written against the old
88
per-interface-handle model.
99

10-
> **Platform note:** HID interface enumeration is implemented on macOS and Linux. On Windows, HID discovery
11-
> is not yet implemented, so a YubiKey currently surfaces only its PC/SC (CCID) interface there. See
10+
> **Platform note:** HID interface enumeration is implemented on macOS, Linux, and Windows. On Windows,
11+
> opening HID report handles can fail with access denied if another process holds the interface or if the
12+
> environment requires an elevated process. See
1213
> [Platform Support For HID Discovery](#platform-support-for-hid-discovery).
1314
1415
See also: [event-driven device discovery](./event-driven-device-discovery.md) and the
@@ -71,11 +72,12 @@ distinct keys, so one physical key can surface as more than one row.
7172

7273
### Platform Support For HID Discovery
7374

74-
HID interface enumeration (HID FIDO, HID OTP) is implemented on **macOS and Linux**. On **Windows** HID
75-
enumeration is not yet implemented, so today a YubiKey is discovered through its PC/SC (CCID) interface only:
76-
`AvailableConnections` will not include `HidFido`/`HidOtp`, HID connection filters return no devices, and a
77-
composite USB key cannot merge HID interfaces it cannot see. The PC/SC SmartCard path works on all
78-
platforms. This is a known platform residual tracked outside the composite-device model itself.
75+
HID interface enumeration (HID FIDO, HID OTP) is implemented on **macOS, Linux, and Windows**. On Windows,
76+
discovery uses ConfigMgr interface metadata and does not need to open report handles just to identify YubiKey
77+
HID interfaces. Opening a HID report connection can still fail with `UnauthorizedAccessException` when Windows
78+
denies access to the interface, for example because another process holds it exclusively or because the
79+
current environment requires the process to run elevated as Administrator. The PC/SC SmartCard path works on
80+
all platforms and is unaffected by HID report-handle access.
7981

8082
## Opening A Connection
8183

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Runtime Resilience Learnings
2+
3+
This folder records phase closeouts for the runtime resilience workstream. Each phase captures what we found, how we got better at finding it, what worked, what did not, what review changed, and what remains risky.
4+
5+
## Phases
6+
7+
- [Phase 1: SmartCard Listener Recovery](phase-01-smartcard-listener.md)
8+
- [Phase 2: OTP HID Ready-To-Write Polling](phase-02-otp-polling.md)
9+
- [Phase 3: Static Runtime-Resilience Scanner](phase-03-static-scanner.md)
10+
- [Phase 4: SmartCard Context Leak Invariant](phase-04-smartcard-context-leak.md)
11+
- [Phase 5: Minimal Fast Runner](phase-05-minimal-fast-runner.md)
12+
- [Phase 6: Diagnostics Project Deferred](phase-06-diagnostics-project-deferred.md)
13+
- [Phase 7: Audit Skill Deferred](phase-07-audit-skill-deferred.md)
14+
15+
## Closeout Rule
16+
17+
Each future phase should close in this order:
18+
19+
1. Implement the smallest useful slice.
20+
2. Verify with focused and broader project gates.
21+
3. Run cross-vendor review when the change affects runtime behavior or proof quality.
22+
4. Write the phase learning document.
23+
5. Commit the phase before starting the next one.
24+
25+
## What The Next Work Should Do
26+
27+
- Use `dotnet toolchain.cs -- resilience --fast` as the default runtime-resilience verification command.
28+
- Start from a concrete bug seed or missing invariant, not from a desire to add infrastructure.
29+
- Prefer no-hardware proof first: fake seams, deterministic timing/resource-release invariants, or seeded scanner fixtures.
30+
- Use BenchmarkDotNet for discovery and evidence, then convert proven foreground regressions into cheap gates.
31+
- Keep the diagnostics project and audit skill deferred until the workflow grows beyond the current single-command runner.

0 commit comments

Comments
 (0)