Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c197f25
test(perf): add YubiKit performance benchmarks
DennisDyallo Jun 15, 2026
4b0e958
perf(otp): reduce HID write polling latency
DennisDyallo Jun 16, 2026
0a741bd
fix(core): harden SmartCard listener recovery
DennisDyallo Jun 16, 2026
10c2c04
test(core): add OTP polling resilience guard
DennisDyallo Jun 16, 2026
eb2ac82
test(core): add runtime resilience static scanner
DennisDyallo Jun 16, 2026
21b8501
docs: document runtime resilience workflow
DennisDyallo Jun 16, 2026
dad4326
fix(core): dispose SmartCard listener contexts on stop
DennisDyallo Jun 16, 2026
52d4d11
test(core): add runtime resilience fast runner
DennisDyallo Jun 16, 2026
b24077c
docs: defer runtime diagnostics project
DennisDyallo Jun 16, 2026
0363a3d
docs: defer runtime audit skill
DennisDyallo Jun 16, 2026
58ad07c
ci: add runtime resilience check
DennisDyallo Jun 16, 2026
58c4ee9
docs: fold runtime resilience learnings forward
DennisDyallo Jun 16, 2026
2a4c67b
docs(core): require resilience runner for loop changes
DennisDyallo Jun 16, 2026
b933d2a
feat(core): add Windows HID transport
DennisDyallo Jun 23, 2026
db41b6a
fix(core): document Windows HID access failures
DennisDyallo Jun 23, 2026
32158c9
fix(fido2): align CTAP status values with CTAP 2.2
DennisDyallo Jun 23, 2026
85d6f0b
fix(core): use full PIV application identifier
DennisDyallo Jun 23, 2026
23bdbfb
docs(fido2): clarify CTAP status coverage
DennisDyallo Jun 23, 2026
d265372
chore: code style
DennisDyallo Jun 24, 2026
de5f0ed
fix(core): audit and fix Windows HID tests
DennisDyallo Jun 24, 2026
51ac5b2
refactor: code style and warnings
DennisDyallo Jun 24, 2026
630c697
Delete src/Core/tests/Yubico.YubiKit.Core.UnitTests/Sessions/Applicat…
DennisDyallo Jun 24, 2026
e8141ce
Merge pull request #501 from Yubico/yubikit-piv-9-byte-aid
DennisDyallo Jun 26, 2026
a0b78a6
refactor(fido2): remove PuvathRequired backwards compat alias
github-actions[bot] Jun 26, 2026
f9f649e
Merge pull request #505 from Yubico/yubikit-ctap-status-22-alignment
DennisDyallo Jun 29, 2026
fd7d983
fix(core): use ArgumentException for HID report buffer length validation
github-actions[bot] Jun 29, 2026
c2cae84
Merge pull request #496 from Yubico/yubikit-hid-windows
DennisDyallo Jun 30, 2026
82b32a4
fix(core): preserve SmartCard context on stuck listener
DennisDyallo Jun 30, 2026
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
5 changes: 4 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true

# Code-block preferences
csharp_prefer_braces = false:when_multiline
csharp_prefer_braces = when_multiline:none
csharp_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_diagnostic.IDE0046.severity = suggestion
dotnet_diagnostic.IDE0072.severity = suggestion
csharp_prefer_simple_using_statement = true
csharp_style_namespace_declarations = file_scoped
csharp_style_prefer_method_group_conversion = true
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ jobs:
- name: Run unit tests
run: dotnet toolchain.cs test

- name: Run runtime resilience checks
run: dotnet toolchain.cs -- resilience --fast

- name: Pack NuGet packages
run: dotnet toolchain.cs pack --package-version 2.0.0-preview.${{ github.run_number }}

Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Do not run `dotnet build` directly. Use `dotnet toolchain.cs build` or `dotnet toolchain.cs build --project Piv`.
- 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.
- Focus tests with both module and filter when possible: `dotnet toolchain.cs test --project Fido2 --filter "Method~Sign"`.
- When touching Core runtime loops, polling paths, recovery logic, or listener lifecycle cleanup, also run `dotnet toolchain.cs -- resilience --fast`.
- `--integration` requires `--project`: `dotnet toolchain.cs -- test --integration --project Piv --smoke`.
- `--smoke` skips `Slow` and `RequiresUserPresence`; agents should not run touch/insert/remove tests unless a human explicitly coordinates hardware.
- Use `dotnet toolchain.cs -- --help` when arguments act strangely; `--help` and some options need the `--` separator.
Expand All @@ -30,6 +31,7 @@
- Platform interop belongs under `src/Core/src/PlatformInterop/{Windows,MacOS,Linux,Desktop}` with safe handles and `SdkPlatformInfo` platform selection.
- 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+.
- `TlvBuilder` and `DisposableTlvList` must be disposed.
- 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.

## Hardware And Integration Tests
- 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.
Expand All @@ -52,6 +54,7 @@
- Do not add validation-only tests that just prove `ArgumentNullException.ThrowIfNull` or framework type checks work.
- Do not add skipped placeholder tests. If behavior cannot be unit-tested, document the limitation and point to an integration path.
- Use fake connections, queued APDU responses, and byte/vector assertions for protocol behavior; use integration tests only for real hardware behavior.
- Runtime resilience tests should use fakes/seams and must not self-skip because PC/SC, HID, or YubiKey hardware is unavailable.

## Git And Release Notes
- The workflow file currently triggers on `yubikit` and `yubikit-applets`; verify the active plan/branch before assuming a PR base.
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@
<PackageVersion Include="Spectre.Console" Version="0.49.1" />
<PackageVersion Include="Spectre.Console.Cli" Version="0.49.1" />
<!-- Build Tools -->
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<PackageVersion Include="Bullseye" Version="5.0.0" />
<PackageVersion Include="SimpleExec" Version="12.0.0" />
</ItemGroup>
</Project>
</Project>
241 changes: 241 additions & 0 deletions benchmarks/Yubico.YubiKit.PerformanceBenchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Exporters.Csv;
using BenchmarkDotNet.Exporters.Json;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Validators;
using Yubico.YubiKit.Core.Abstractions;
using Yubico.YubiKit.Core.Devices;
using Yubico.YubiKit.Core.Protocols.Fido.Hid;
using Yubico.YubiKit.Core.Protocols.SmartCard.Apdu;
using Yubico.YubiKit.Core.Sessions;
using Yubico.YubiKit.Core.Transports.Hid;
using Yubico.YubiKit.Core.Transports.SmartCard;
using Yubico.YubiKit.Management;

var config = YubiKitBenchmarkConfig.Create();
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);

internal sealed class YubiKitBenchmarkConfig : ManualConfig
{
private YubiKitBenchmarkConfig()
{
var runId = DateTimeOffset.UtcNow.ToString("yyyyMMdd-HHmmss");
ArtifactsPath = Path.Combine(FindRepoRoot(), "artifacts", "benchmarks", runId);

AddJob(Job.ShortRun
.WithRuntime(CoreRuntime.Core10_0)
.WithId("net10-short"));

AddColumnProvider(DefaultColumnProviders.Instance);
AddDiagnoser(MemoryDiagnoser.Default);
AddDiagnoser(ThreadingDiagnoser.Default);
AddDiagnoser(ExceptionDiagnoser.Default);
AddDiagnoser(new EventPipeProfiler(EventPipeProfile.CpuSampling));
AddExporter(MarkdownExporter.GitHub);
AddExporter(HtmlExporter.Default);
AddExporter(CsvExporter.Default);
AddExporter(CsvMeasurementsExporter.Default);
AddExporter(JsonExporter.Full);
AddExporter(RPlotExporter.Default);
AddValidator(JitOptimizationsValidator.FailOnError);
AddLogger(DefaultConfig.Instance.GetLoggers().ToArray());
}

public static IConfig Create() => new YubiKitBenchmarkConfig();

private static string FindRepoRoot()
{
var current = AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(current))
{
if (Directory.Exists(Path.Combine(current, ".git")))
return current;

current = Directory.GetParent(current)?.FullName;
}

return Directory.GetCurrentDirectory();
}
}

public abstract class YubiKeyHardwareBenchmarkBase
{
protected IYubiKey Device { get; private set; } = null!;

protected void SetupDevice(ConnectionType requiredConnection)
{
Device = FindDevice(requiredConnection).GetAwaiter().GetResult();
}

[GlobalCleanup]
public void Cleanup() => YubiKeyManager.Shutdown();

private static async Task<IYubiKey> FindDevice(ConnectionType requiredConnection)
{
var devices = await YubiKeyManager.FindAllAsync(ConnectionType.All, forceRescan: true)
.ConfigureAwait(false);

return devices.FirstOrDefault(device => device.SupportsConnection(requiredConnection))
?? throw new InvalidOperationException(
$"No connected YubiKey exposes {requiredConnection}. Connect the YubiKey 5.8 device before running benchmarks.");
}
}

[MemoryDiagnoser]
[ThreadingDiagnoser]
[ExceptionDiagnoser]
public class YubiKeyDiscoveryBenchmarks
{
[GlobalSetup]
public void Setup()
{
_ = YubiKeyManager.FindAllAsync(ConnectionType.All, forceRescan: true)
.GetAwaiter()
.GetResult();
}

[GlobalCleanup]
public void Cleanup() => YubiKeyManager.Shutdown();

[Benchmark(Baseline = true)]
public async Task<int> CachedFindAll()
{
var devices = await YubiKeyManager.FindAllAsync(ConnectionType.All, forceRescan: false)
.ConfigureAwait(false);
return devices.Count;
}

[Benchmark]
public async Task<int> ForcedRescanFindAll()
{
var devices = await YubiKeyManager.FindAllAsync(ConnectionType.All, forceRescan: true)
.ConfigureAwait(false);
return devices.Count;
}

[Benchmark]
public async Task<int> ColdFindAll()
{
await YubiKeyManager.ShutdownAsync().ConfigureAwait(false);
var devices = await YubiKeyManager.FindAllAsync(ConnectionType.All, forceRescan: false)
.ConfigureAwait(false);
return devices.Count;
}
}

[MemoryDiagnoser]
[ThreadingDiagnoser]
[ExceptionDiagnoser]
public class SmartCardManagementBenchmarks : YubiKeyHardwareBenchmarkBase
{
[GlobalSetup]
public void Setup() => SetupDevice(ConnectionType.SmartCard);

[Benchmark(Baseline = true)]
public async Task<bool> ConnectSmartCard()
{
await using var connection = await Device.ConnectAsync<ISmartCardConnection>().ConfigureAwait(false);
return connection.SupportsExtendedApdu();
}

[Benchmark]
public async Task<int> SelectManagementOverSmartCard()
{
var connection = await Device.ConnectAsync<ISmartCardConnection>().ConfigureAwait(false);
using var protocol = PcscProtocolFactory<ISmartCardConnection>.Create().Create(connection);
var response = await protocol.SelectAsync(ApplicationIds.Management).ConfigureAwait(false);
return response.Length;
}

[Benchmark]
public async Task<int> GetDeviceInfoOverSmartCard()
{
await using var session = await Device.CreateManagementSessionAsync(
preferredConnection: ConnectionType.SmartCard)
.ConfigureAwait(false);

var info = await session.GetDeviceInfoAsync().ConfigureAwait(false);
return info.SerialNumber ?? 0;
}
}

[MemoryDiagnoser]
[ThreadingDiagnoser]
[ExceptionDiagnoser]
public class FidoHidManagementBenchmarks : YubiKeyHardwareBenchmarkBase
{
[GlobalSetup]
public void Setup() => SetupDevice(ConnectionType.HidFido);

[Benchmark(Baseline = true)]
public async Task<int> ConnectFidoHid()
{
await using var connection = await Device.ConnectAsync<IFidoHidConnection>().ConfigureAwait(false);
return connection.PacketSize;
}

[Benchmark]
public async Task<int> CreateManagementSessionOverFidoHid()
{
await using var session = await Device.CreateManagementSessionAsync(
preferredConnection: ConnectionType.HidFido)
.ConfigureAwait(false);

return session.FirmwareVersion.Major;
}

[Benchmark]
public async Task<int> GetDeviceInfoOverFidoHid()
{
await using var session = await Device.CreateManagementSessionAsync(
preferredConnection: ConnectionType.HidFido)
.ConfigureAwait(false);

var info = await session.GetDeviceInfoAsync().ConfigureAwait(false);
return info.SerialNumber ?? 0;
}
}

[MemoryDiagnoser]
[ThreadingDiagnoser]
[ExceptionDiagnoser]
public class OtpHidManagementBenchmarks : YubiKeyHardwareBenchmarkBase
{
[GlobalSetup]
public void Setup() => SetupDevice(ConnectionType.HidOtp);

[Benchmark(Baseline = true)]
public async Task<int> ConnectOtpHid()
{
await using var connection = await Device.ConnectAsync<IOtpHidConnection>().ConfigureAwait(false);
return connection.FeatureReportSize;
}

[Benchmark]
public async Task<int> CreateManagementSessionOverOtpHid()
{
await using var session = await Device.CreateManagementSessionAsync(
preferredConnection: ConnectionType.HidOtp)
.ConfigureAwait(false);

return session.FirmwareVersion.Major;
}

[Benchmark]
public async Task<int> GetDeviceInfoOverOtpHid()
{
await using var session = await Device.CreateManagementSessionAsync(
preferredConnection: ConnectionType.HidOtp)
.ConfigureAwait(false);

var info = await session.GetDeviceInfoAsync().ConfigureAwait(false);
return info.SerialNumber ?? 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
Comment thread
DennisDyallo marked this conversation as resolved.
<RootNamespace>Yubico.YubiKit.PerformanceBenchmarks</RootNamespace>
<AssemblyName>Yubico.YubiKit.PerformanceBenchmarks</AssemblyName>
<IsPackable>false</IsPackable>
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../../src/Core/src/Yubico.YubiKit.Core.csproj" />
<ProjectReference Include="../../src/Management/src/Yubico.YubiKit.Management.csproj" />
</ItemGroup>

</Project>
16 changes: 9 additions & 7 deletions docs/architecture/physical-device-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ by `AvailableConnections`. This document explains the model, how to discover and
metadata lives, how each applet picks a transport, and how to migrate code written against the old
per-interface-handle model.

> **Platform note:** HID interface enumeration is implemented on macOS and Linux. On Windows, HID discovery
> is not yet implemented, so a YubiKey currently surfaces only its PC/SC (CCID) interface there. See
> **Platform note:** HID interface enumeration is implemented on macOS, Linux, and Windows. On Windows,
> opening HID report handles can fail with access denied if another process holds the interface or if the
> environment requires an elevated process. See
> [Platform Support For HID Discovery](#platform-support-for-hid-discovery).

See also: [event-driven device discovery](./event-driven-device-discovery.md) and the
Expand Down Expand Up @@ -71,11 +72,12 @@ distinct keys, so one physical key can surface as more than one row.

### Platform Support For HID Discovery

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

## Opening A Connection

Expand Down
31 changes: 31 additions & 0 deletions docs/learnings/runtime-resilience/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Runtime Resilience Learnings

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.

## Phases

- [Phase 1: SmartCard Listener Recovery](phase-01-smartcard-listener.md)
- [Phase 2: OTP HID Ready-To-Write Polling](phase-02-otp-polling.md)
- [Phase 3: Static Runtime-Resilience Scanner](phase-03-static-scanner.md)
- [Phase 4: SmartCard Context Leak Invariant](phase-04-smartcard-context-leak.md)
- [Phase 5: Minimal Fast Runner](phase-05-minimal-fast-runner.md)
- [Phase 6: Diagnostics Project Deferred](phase-06-diagnostics-project-deferred.md)
- [Phase 7: Audit Skill Deferred](phase-07-audit-skill-deferred.md)

## Closeout Rule

Each future phase should close in this order:

1. Implement the smallest useful slice.
2. Verify with focused and broader project gates.
3. Run cross-vendor review when the change affects runtime behavior or proof quality.
4. Write the phase learning document.
5. Commit the phase before starting the next one.

## What The Next Work Should Do

- Use `dotnet toolchain.cs -- resilience --fast` as the default runtime-resilience verification command.
- Start from a concrete bug seed or missing invariant, not from a desire to add infrastructure.
- Prefer no-hardware proof first: fake seams, deterministic timing/resource-release invariants, or seeded scanner fixtures.
- Use BenchmarkDotNet for discovery and evidence, then convert proven foreground regressions into cheap gates.
- Keep the diagnostics project and audit skill deferred until the workflow grows beyond the current single-command runner.
Loading
Loading