Skip to content

feat(applets): merge yubikit-applets into yubikit#446

Open
DennisDyallo wants to merge 296 commits into
yubikitfrom
yubikit-applets
Open

feat(applets): merge yubikit-applets into yubikit#446
DennisDyallo wants to merge 296 commits into
yubikitfrom
yubikit-applets

Conversation

@DennisDyallo

Copy link
Copy Markdown
Collaborator

Summary

  • Adds all six YubiKey applet implementations (PIV, OATH, OpenPGP, FIDO2, YubiOTP, HsmAuth) and their CLI tools to the yubikit integration branch
  • Includes 39+ bug fixes found during integration testing across firmware 5.4.3 and 5.8.0-alpha
  • All primary workflows verified working: PIV 44/44, OATH 8/8, OpenPGP 28/28, FIDO2 full (HID excluded — macOS OS constraint)

Key fixes in this branch (recent sessions)

  • core: reject BER-TLV 0x80 indefinite length encoding
  • openpgp: fallback for malformed GetAlgorithmInformation TLV from FW 5.4.3 (matches ykman padding approach)
  • openpgp: relax reset-state test assertions — 5.4.3 does not clear fingerprint DOs on TERMINATE+ACTIVATE
  • hsmauth: correct ChangeCredentialPasswordAdmin TLV field ordering to [Label, MgmtKey, NewPw]
  • piv: bypass .NET TripleDES weak key rejection for PIV default management key
  • otp: use HidOtp transport for HMAC-SHA1 challenge-response tests
  • core: IsSupported() handles 0.0.1 sentinel firmware correctly

Known limitations

  • YubiOTP HMAC-SHA1 HID timeout on FW 5.4.3 at OtpHidProtocol.cs:211 (works on alpha firmware)
  • HsmAuth ChangeCredentialPassword not testable on alpha (INS 0x0B not implemented, SW=0x6D00)
  • FIDO2 HID tests excluded — macOS CTAP daemon holds exclusive access

Test plan

  • Unit tests: 9/9 projects passing, 0 failures
  • PIV integration: 44/44 on FW 5.4.3
  • OATH integration: 8/8 on FW 5.4.3
  • OpenPGP integration: 28/28 on FW 5.4.3
  • YubiOTP integration: 5/7 (2 HID timeout failures on 5.4.3, by-design on CCID)
  • Build: 0 errors, 0 warnings on dotnet build Yubico.YubiKit.sln

🤖 Generated with Claude Code

DennisDyallo and others added 30 commits April 2, 2026 11:52
…rmware

- OtpBackend.ReadConfigAsync: add bounds check before CheckCrc to prevent
  IndexOutOfRangeException when alpha firmware response length exceeds buffer
- SlotConfiguration.IsSupportedBy: honour Major==0 sentinel firmware (matches
  ApplicationSession.IsSupported) so PutConfigurationAsync works on alpha
- YubiOtpSession.InitializeAsync: always recreate SmartCard backend after
  SELECT so _lastProgSeq reflects actual device state, not hardcoded 0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After ykman config reset, alpha firmware stops exposing serial numbers
via the Management API. This blocked all integration tests.

- Add AllowUnknownSerials option to IAllowListProvider/AllowList/
  AppSettingsAllowListProvider, read from appsettings.json
- YubiKeyTestInfrastructure: authorize devices without serial when
  AllowUnknownSerials=true
- YubiOTP integration tests: pin GetSerial and SwapSlots to SmartCard
  to avoid HidOtp 1023ms timeout on alpha firmware flash programming
- GetSerial assertion relaxed to >= 0 (serial may be hidden)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…cumented

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DevTeam review identified ~2600 LOC duplicated across 5 CLI tools.
4-phase extraction plan with difficulty ratings, inconsistencies to
normalize, and proposed Yubico.YubiKit.Cli.Shared project structure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Shared (#12)

New shared project consolidating ~2600 LOC of duplicated CLI patterns:
- DeviceSelectorBase: abstract base for device discovery/selection
- OutputHelpers: Spectre.Console formatting (WriteSuccess/Error/Warning/etc.)
- ConfirmationPrompts: dangerous/destructive operation confirmations
- PinPrompt: masked PIN input
- ArgumentParser: flag/option/positional parsing
- CommandHelper: YubiKeyManager lifecycle + CTS boilerplate
- ConnectionTypeFormatter/FormFactorFormatter: enum display formatting

All 5 CLIs migrated (ManagementTool, OathTool, FidoTool, OpenPgpTool, HsmAuthTool):
- Each CLI gets a sealed DeviceSelectorBase subclass for transport filtering
- OutputHelpers delegate to shared library (except OathTool: plain-text by design)
- OathTool retains custom pipe-safe output intentionally

Reviewer fixes applied:
- Non-interactive multi-device guard (returns null vs crashing on Prompt)
- Logger.LogDebug replaces silent console debug in GetDeviceInfoAsync
- WriteTouchPrompt renamed to PromptForTouch for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Mark CLI shared infrastructure (#12) Phases 1-3 as done in both plan docs
- Add handoff document for session continuity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Brings in full PIV application implementation (~13,850 lines):
- PivSession with authentication, certificates, crypto, key pairs, metadata, bio
- IPivSession interface, domain types (PivAlgorithm, PivSlot, PivPinPolicy, etc.)
- PivTool CLI example application
- Unit and integration tests
- Module CLAUDE.md and README.md

Conflict resolution: kept yubikit-applets' async DisposeAsync pattern
(proper DisposeAsyncCore + Dispose(false)) over yubikit-piv's simpler
synchronous version.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
… default

- WithYubiKeyAttribute now implements ITraitAttribute, automatically
  adding RequiresHardware and Integration category traits to all
  integration tests that use [WithYubiKey]
- build.cs test target now runs only unit tests by default
- New --integration flag requires --project to prevent accidentally
  running all integration tests at once
- FilterBullseyeArgs now properly distinguishes flags from value options

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…bugs

- Fix GetArgument/HasFlag to use captured top-level args instead of
  Environment.GetCommandLineArgs() which shadowed the variable
- Fix FilterBullseyeArgs call to strip --package-version, --nuget-feed-name,
  --nuget-feed-path (previously leaked to Bullseye as unknown args)
- Extract PrintColored, PrintProjectList, PrintNoProjectsFound helpers
- Extract RunTestProjects and PrintTestSummary to eliminate duplication
  between test and coverage targets
- Extract FilterToProject for --project filtering logic
- Remove unused allTestProjects intermediate variable
- Simplify UsesMicrosoftTestingPlatformRunner to single return expression
- Update BUILD.md: document --integration option, add Code Coverage section
  noting Microsoft.Testing.Platform exclusion
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…do2, and Piv

- OtpHidProtocol: Fix assertion to expect 6-byte status response (not 0)
  for empty payload commands, matching actual protocol behavior
- YubiKeyDeviceManager: Populate cache before starting monitoring since
  StartMonitoring is event-driven and does not trigger an initial scan
- LargeBlobArray.Deserialize: Fix minimum size check from HashSize+2 to
  HashSize+1 since a valid empty CBOR array (0x80) is only 1 byte
- PivSession: Use reflection to set FirmwareVersion to 4.0.0 instead of
  relying on default 0.0.0, which is treated as alpha/beta (latest) and
  bypasses the firmware version gate

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…Shared (Phase 4)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The test used a 32-byte key expecting ArgumentException, but
Authenticate now accepts both 32-byte (PIN token) and 64-byte
(shared secret) keys. Changed to 16 bytes which is genuinely invalid.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Cross-referenced with ykman 5.8.0 FIDO reset flow:
- Query LongTouchForReset from AuthenticatorInfo to show correct
  touch message (10s hold for 5.8+/0.0.0 keys, tap for older)
- Add TransportsForReset pre-check in interactive menu
- Fix --force to skip reinsertion flow entirely (matching ykman)
- Add ResetPreflightInfo record and GetPreflightInfoAsync method
- Add PinAuthBlocked error mapping, align error messages with ykman

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
clean target must now be specified explicitly; dotnet build.cs clean build for full clean build
OATH: Fix touch property encoding to use raw bytes [tag, value] instead
of TLV encoding [tag, length, value], matching ykman's struct.pack.

YubiOTP: Fix ConfigFlag values (StrongPw1, StrongPw2, ManUpdate) to
match ykman constants. Add missing ChalHmac, ChalYubico, OathHotp8
flags. Add default ext flags (SerialApiVisible, AllowUpdate) to base
SlotConfiguration. Add default tkt/ext flags (AppendCr, FastTrigger)
to KeyboardSlotConfiguration. Add ChalHmac|HmacLt64 to HMAC-SHA1
config. Add OathFixedModhex2 to HOTP config. Fix Use8Digits to use
OathHotp8 instead of OathFixedModhex1. Remove incorrect StaticTicket
from StaticPassword config.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ation

Remove DependsOn("build") from test and coverage targets and remove
--no-build flags so dotnet handles incremental building naturally.
Running `dotnet build.cs test` no longer forces a full rebuild — it
only recompiles if sources changed, matching dotnet.exe behavior.
Explicit rebuild via `dotnet build.cs clean build test`.

Add TranslateToMtpFilter() to convert VSTest --filter expressions to
xUnit v3 MTP native options (--filter-method, --filter-trait, etc.).
Add --minimum-expected-tests 0 to prevent failure when no tests match.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Replace == null / != null with is null / is not null across all modules
- Remove all #region / #endregion directives (91 files, 399 lines removed)
- Replace SequenceEqual with CryptographicOperations.FixedTimeEquals in
  security-sensitive contexts (ATR, FIDO nonce, RP ID hash, large blob
  hash, OATH credential ID, OpenPGP OID)
- Replace Array.Empty<byte>() with [] where target type supports it

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add --smoke flag to skip Slow and RequiresUserPresence tests for fast
integration runs. Extract DiscoverProjects() and FilterProjectsByName()
to eliminate repeated project discovery logic. Add --project support to
coverage target. Update BUILD.md, CLAUDE.md, and TESTING.md with
integration test strategy. Mark RSA 3072/4096 PIV tests as [Slow].

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Convert `new byte[] { ... }` to `[...]` and `Array.Empty<T>()` to `[]`
where C# 14 collection expressions are supported by the target type.
Skipped instances where `var` inference, `Memory<byte>`, attribute args,
or method chaining prevent collection expression usage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The stub class in Core was unreferenced — no code in Core throws,
catches, or uses it. The full implementation lives in
Fido2/Ctap/CtapException.cs where it belongs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The extended parameter's mapping to P2 values was reversed vs ykman
canonical. extended=false should send P2=0x81 (signature-only) and
extended=true should send P2=0x82 (decrypt/authenticate/attest).
Fix tests to use correct extended values for each operation type.
Integration: 27/28 pass (AttestKey remains alpha firmware gap).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Convert.ToHexString accepts ReadOnlySpan<byte> natively on net10
- Collection expression spread operator supports ReadOnlySpan<byte>
- Tlv.ToString: replace BitConverter.ToString().Replace() with
  Convert.ToHexString() (cleaner + zero-alloc)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AttestKeyAsync incorrectly parsed the GET_ATTESTATION APDU response as
a certificate. Per ykman canonical, GET_ATTESTATION writes the cert to
the slot — read it back with GetCertificateAsync. Also fix
SelectCertificateSlotAsync index mapping (should be 3-keyRef: SIG=2,
DEC=1, AUT=0) and non-standard FW<=5.4.3 byte placement (prepend
before TLV, not inside). Integration: 28/28 pass.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Alpha/beta firmware reports 0.0.1 sentinel from applet SELECT, causing
raw FirmwareVersion comparisons to fail. IsSupported() handles Major==0
by assuming all features available. Fixes PIV GetPinAttemptsAsync
returning wrong retry count and several other feature gates across
OATH, OpenPGP, and PIV modules.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
HMAC-SHA1 challenge-response is not supported over USB CCID (SmartCard),
matching ykman's not_usb_ccid condition. Changed ConnectionType from
SmartCard to HidOtp and removed incorrect RequiresUserPresence trait
since the test doesn't enable touch-triggered mode.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
DennisDyallo and others added 22 commits April 24, 2026 14:08
…hase 9.7)

Phase 9.7 enforces the strict no-duplication rule: zero duplicated code or
behavior in WebAuthn; Fido2 owns anything both projects need.

Shipped 12 of 19 architectural violations identified by the SoC audit
(Plans/phase-9.7-soc-consolidation.md). Group C (4 attestation items)
deferred to Phase 9.8 (Plans/phase-9.8-attestation-typed-variants.md) due
to a Fido2 public-API-freeze conflict that requires explicit maintainer
sign-off to resolve cleanly.

Groups complete:
- A (6): extension Input/Output type families consolidated to Fido2
  (CredBlob, MinPinLength, LargeBlob, Prf). Phase 9.6 32-byte CredBlob
  validation absorbed into A1 and now closed.
- B (3): WebAuthn identity types deleted; consumers use Fido2's
  PublicKeyCredentialDescriptor / RpEntity / UserEntity.
- D (2): COSE typed model promoted to Yubico.YubiKit.Fido2.Cose
  (new public additions: CoseKey, CoseAlgorithm).
- E (1): AAGUID big-endian/mixed-endian helper unified at
  Yubico.YubiKit.Fido2.Cbor.AaguidConverter (internal).
- F (2): PreviewSign output decoders moved into Fido2 alongside the
  encoders; WebAuthn adapter no longer reads CBOR.
- G (2): dead using-alias removed; private ByteArrayComparer duplicate
  in PrfAdapter deleted.

Constraints honored:
- Fido2 public API surface: only additions, zero breaking changes.
- All 10 test projects pass (Fido2 357/0, WebAuthn 90/0).
- Build clean (0 errors).
- Fido CLI builds and consumes Fido2 unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ierarchy (Phase 9.8 Group C)

Replaces Fido2's flat sealed-class AttestationStatement with a typed
discriminated-union hierarchy promoted from WebAuthn. Closes Group C of
the SoC consolidation deferred from Phase 9.7.

This is a BREAKING CHANGE to Fido2's public API — explicitly authorized
by Dennis after consumer-surface audit confirmed zero external callers.

Before:
  public sealed class AttestationStatement {
      ReadOnlyMemory<byte>? Signature; IReadOnlyList<...>? X5c;
      ReadOnlyMemory<byte>? EcdaaKeyId; int? Algorithm;
      ReadOnlyMemory<byte> RawData; bool IsNone;
  }

After:
  public abstract record AttestationStatement(AttestationFormat Format, ReadOnlyMemory<byte> RawCbor)
  public sealed record PackedAttestationStatement   : AttestationStatement
  public sealed record FidoU2FAttestationStatement  : AttestationStatement
  public sealed record AppleAttestationStatement    : AttestationStatement
  public sealed record NoneAttestationStatement     : AttestationStatement
  public sealed record UnknownAttestationStatement  : AttestationStatement

MakeCredentialResponse.AttestationStatement property keeps its name and
declared type. Consumers reading .Signature/.X5c/.Algorithm directly need
to pattern-match on the variant.

Items shipped:
- C1 internal decoder dispatches by fmt string to typed variants
- C2 typed hierarchy promoted to Yubico.YubiKit.Fido2.Credentials (public)
- AttestationFormat enum promoted alongside
- WebAuthn's local copies removed (git mv preserves history)

Items intentionally deferred:
- C3/C4 envelope writer/decoder helpers — WebAuthn already has working
  implementations at WebAuthnAttestationObject.{EncodeAttestationObject,Decode};
  extracting them to Fido2 internal helpers can be a follow-up if other
  consumers ever need them.

Constraint compliance:
- Pre-mapped consumer migration: all 5 sites updated (Fido2 internal
  decoder, 4 test methods)
- Fido CLI: zero changes needed (confirmed by grep — Fido CLI examples
  do not consume .AttestationStatement.* fields)
- All 10 test projects pass (Fido2 357/0, WebAuthn 90/0)
- Build clean (0 errors)
- Test fixture in CredentialResponseTests updated to produce valid
  packed attStmt (alg + sig per CTAP 2.1 §8.2) when format=="packed"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reduce root CLAUDE.md from 1395→586 lines (40KB→22KB) by extracting deep-dive
content to JIT-loadable docs while preserving every behavioral mandate.

- Quick Reference rules retained, each with a JIT pointer that names the
  trigger context (e.g. "load when allocating buffers")
- Type Selection (273 lines) and Test Philosophy (121 lines) preserved
  verbatim — they catch the most expensive AI-agent failure modes
- New JIT docs (MEMORY-MANAGEMENT.md, CRYPTO-APIS.md, CSHARP-PATTERNS.md)
  open with a `READ WHEN <triggers>` frontmatter mirroring the PAI skill
  discoverability pattern, so agents pull them in by intent rather than
  enumeration
- Build/test/security deep-dives deleted in favor of existing skill files
  (`domain-build`, `domain-test`, `domain-security-guidelines`,
  `domain-secure-credential-prompt`)
- Verified: 29/29 semantic mandates retained across the new file set;
  all docs/ and .claude/skills/ pointers resolve

Includes the trim plan (Plans/do-you-see-a-whimsical-spring.md) which
documents the per-section treatment matrix and a Tier 2 A/B agent harness
spec for measuring real behavioral impact in a follow-up session. Baseline
CLAUDE.md snapshot at Plans/eval/baseline/ enables one-revert recovery.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…not 8

The CTAP MakeCredential response field for unsignedExtensionOutputs is key 6
per CTAP 2.2 / WebAuthn L3, aligned with yubikit-swift, yubikit-android, and
yubikit-python. v2 .NET was the lone outlier reading at key 8 (an early CTAP
v4 draft value), causing silent data loss against real YubiKey 5.8+ firmware:
MakeCredentialResponse.UnsignedExtensionOutputs was always null, and
PreviewSignAdapter.ParseRegistrationOutput silently fell back to the authData
path with AttestationObject: null.

Includes a regression unit test pinning the new behavior: a response carrying
the legacy key 8 must not populate UnsignedExtensionOutputs.

Cross-source evidence:
- yubikit-swift release/1.3.0: key 6
- yubikit-swift commit 339a7792: explicit Java + Python alignment
- 5-8-hello-world README: key 6
- v1 .NET PoC (add-preview-sign): key 6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… CredProtect L2

CTAP 2.x: a regular pinUvAuthToken must be regenerated for each authenticator
invocation. PPUAT (Per-credential Persistent UV Auth Token) is the read-only
exception, not used here. The CredProtect L2 test was reusing the same token
across two GetAssertion calls (line 120 and the prior line 129), which fails
with "PIN authentication failed" on real YubiKey firmware on the second call.

Fix: mint a fresh pinUvAuthToken and recompute the auth param immediately
before the second GetAssertion call, with dedicated variable names so the
per-call freshness is visible to readers.

Verified by isolated hardware run on a freshly Reset YubiKey: the test now
passes end-to-end in 34s with focused user touches. Prior runs failed at
either line 120 (touch timeout under multi-test cascade) or line 129 (token
reuse). Both root causes are addressed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…USB-only devices

The three FidoNfcTests methods were failing (not skipping) on USB-connected
NFC-capable YubiKeys. The [WithYubiKey(RequireNfc=true)] filter matches the
device's NFC capability, not its current connection — so a USB-A YubiKey 5 NFC
passes the filter, then SDK throws NotSupportedException when trying to open a
SmartCard FIDO2 session over USB CCID (intentionally blocked).

Mirrors the established pattern in FidoTransportTests.MakeCredential_OverNfcSmartCard:
1. Pre-runtime check on state.ConnectionType to skip cleanly
2. catch (NotSupportedException) → Skip.If(true, ...) for the runtime case

[SkippableTheory] alone only converts SkipException to Skipped, not arbitrary
SDK exceptions — explicit catch is required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements client-layer excludeList pre-flight in Yubico.YubiKit.WebAuthn,
mirroring yubikit-android's Ctap2Client.makeCredential + Utils.filterCreds
architecture (Ctap2Client.java:520-564, Utils.java:860-938).

When excludeCredentials is non-empty, WebAuthnClient now:
1. Acquires a pinUvAuthToken with MakeCredential | GetAssertion permissions
2. Chunks the exclude list by info.MaxCredentialCountInList
3. Probes each chunk via GetAssertion(up=false) to find the first matching cred
4. Calls MakeCredential with at-most-1 entry in excludeList

This preserves CTAP 2.1 semantics on YubiKey firmware that evaluates request
processing limits before excludeList matching for large lists. Without
pre-flight, a 17-entry exclude list returned LimitExceeded instead of the
expected CredentialExcluded.

Architectural rule preserved: the pre-flight is a WebAuthn client-orchestration
concern, NOT a low-level CTAP concern. src/Fido2/ is unchanged; only
WebAuthn module gains the new logic.

Also maps CTAP CredentialExcluded → WebAuthnClientErrorCode.InvalidState per
WebAuthn L2 §5.1.3.

Test layering changes:
- Deletes src/Fido2/tests/.../FidoExcludeListStressTests.cs (test exercised
  behavior that requires the WebAuthn pre-flight layer; cannot pass at the
  Fido2-direct layer alone)
- Adds src/WebAuthn/tests/.../WebAuthnExcludeListStressTests.cs covering the
  same scenario at the correct architectural layer
- Adds 4 unit tests for ExcludeListPreflight (empty, single-match, no-match,
  chunked iteration)
- Test uses separate FidoSessions for setup vs WebAuthnClient body to avoid
  PIN/UV protocol state contamination from connection reuse
- Documents the operator-Reset precondition mirroring yubikit-android's
  FidoTestState.withCtap2() contract

KNOWN ISSUE: On YubiKey 5.8.0, after pre-flight's GetAssertion(up=false)
consumes the token's GetAssertion permission, the subsequent MakeCredential
returns PinAuthInvalid. Per CTAP 2.1 §6.5.5.7, authenticators MAY consume
permissions on use. Java parity at the WebAuthn layer requires re-acquiring
the pinUvAuthToken between pre-flight and MakeCredential when excludeList is
non-empty. Tracked for next session — see Plans/handoff.md.

Empirically validated end-to-end on freshly-Reset YubiKey 5.8.0:
- 17 RKs created via WebAuthnClient.MakeCredentialAsync ✓
- Pre-flight GetAssertion(up=false) executes without consuming a touch ✓
- Device returned CredentialExcluded → WebAuthnClient surfaces typed error ✓
- Final InvalidState mapping pending the token-rotation fix above

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nd MakeCredential

Resolves the KNOWN ISSUE documented in commit f62c7c4: on YubiKey 5.8.0 the
pre-flight's GetAssertion(up=false) consumes the GetAssertion permission from
the pinUvAuthToken (per CTAP 2.1 §6.5.5.7, "authenticators MAY consume
permissions on use"). The subsequent MakeCredential then fails with
PinAuthInvalid because the same token can no longer authorize ANY operation,
even one (MakeCredential) whose permission slot is logically untouched.

Fix: when ExcludeListPreflight has run, dispose the original token and mint a
fresh one scoped to MakeCredential-only permissions before BuildMakeCredentialRequest
computes the pinUvAuthParam. tokenCopy zeroed in finally for hygiene.

Java parity: yubikit-android Ctap2Client.java:472-474 mints the token with
permissions = MC | (excludeCredentials.isEmpty() ? 0 : GA) — same single-token
shape as ours. Whether Java hits the same firmware behavior on 5.8.0 is
untested; either way, this is the correct WebAuthn-layer fix per the commit
message that introduced the preflight.

Empirically validated:
- WebAuthnExcludeListStressTests: FAIL → PASS in 34s on freshly-Reset YK5.8.0
- 17 RKs created, preflight matches one of them, final excluded MakeCredential
  surfaces InvalidState as the test expects
- Build clean, all unit tests still green

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…error mapping)

CodeAudit pass on WebAuthn + adjacent Fido2 surface flagged 4 critical issues
post-dc2ed141. All addressed in this commit; build clean (0 errors), WebAuthn
unit tests green.

1. PinUvAuthTokenSession.cs: add finalizer fallback. The class owns a private
   byte[] clone of a sensitive token but had no finalizer, so a leaked
   instance never zeroed before GC. CLAUDE.md mandates IDisposable + defensive
   zeroing for owned sensitive byte[]. Now: Dispose() suppresses finalize;
   finalizer zeroes if Dispose was never called.

2. CredentialMatcher.cs: stop swallowing CtapStatus.NotAllowed (0x30) as
   "no credentials." NotAllowed is the generic CTAP "device denied the
   operation" status (user cancel, policy reject) — semantically distinct
   from "no matching credential." Folding them together sent users down the
   wrong recovery UX path. Now propagates so the upstream mapper translates
   to WebAuthnClientErrorCode.NotAllowed.

3. WebAuthnClient.MakeCredentialCoreAsync: explicit torn-state guard between
   the post-preflight token Dispose() and the re-mint AcquirePinUvTokenWithRetryAsync.
   If the re-mint throws, tokenSession is now null instead of referencing a
   disposed instance — the outer finally is a clean no-op.

4. WebAuthnClient: introduce private MapCtapStatusToWebAuthnError + add
   general catch arms in both MakeCredentialCoreAsync and GetAssertionCoreAsync.
   Previously, only previewSign-requested registrations had typed CTAP→WebAuthn
   mapping; every other CTAP error leaked as raw CtapException, contradicting
   the WebAuthn module rule "never expose raw CTAP status codes to high-level
   API consumers." Mapping covers PinAuth*/PinBlocked/NotAllowed/OperationDenied
   → NotAllowed; KeyStoreFull/LimitExceeded/Timeout → Constraint; Unsupported*
   → NotSupported; PinNotSet/UpRequired → Security; NoCredentials/InvalidCredential
   → InvalidState; everything else → Unknown with the inner CtapException
   preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…odeAudit Critical fixes

Captures: WebAuthnExcludeListStressTests went FAIL → PASS via dc2ed14;
4 Critical CodeAudit findings landed in a0070db; all 6 prior+session
commits pushed to origin; lessons captured (CTAP 2.1 §6.5.5.7 permission
consumption, three-agent triangulation pattern, vslsp-driven audit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…atusChannel

CodeAudit HIGH finding (StatusChannel.cs:125-137): CreateUvRequest had zero
callers (verified via vslsp find_usages — definition site only). The
_uvResponseTcs field was set inside CreateUvRequest and read inside the same
closure but never observed from outside. The interactive UV flow doesn't go
through the channel-mediated TCS pattern at all — WebAuthnClient auto-responds
WebAuthnStatusRequestingUv directly (lines 108, 176, 401, 475).

Removes ~20 LOC of unreachable interactive-protocol code. The
WebAuthnStatusRequestingUv record is retained — it has 9 real usages.

Build clean, WebAuthn unit tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…resolution

Follow-up to 37ff02f which added catch (NotSupportedException) → Skip.If(true, ...)
guards to all 3 FidoNfcTests methods but did not add the Xunit using directive
that Skip lives in. The file appears to have resolved Skip via transitive imports
in some build configurations but not others; explicit import matches the
FidoTransportTests reference pattern verbatim.

DevTeam Ship verification:
- Engineer agent correctly identified that the catch guards were already in place
  (avoiding a redundant edit) and pinpointed the missing import as the real gap
- Build clean (dotnet toolchain.cs build) — 0 errors
- All 358 Fido2 unit tests pass
- All WebAuthn unit tests pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ead public API

Closes 3 of the remaining 7 HIGH-severity CodeAudit findings. Build clean,
WebAuthn unit tests green.

1. Add WebAuthnClientErrorCode.Cancelled and OperationCanceledException catch
   arm to both producer Task.Run lambdas in WebAuthnClient (~lines 240, 320).
   Cancellation was previously surfaced as WebAuthnClientErrorCode.Unknown
   wrapping a TaskCanceledException via the general Exception catch — consumers
   could not distinguish "I cancelled" from "device errored." OCE arm precedes
   the general Exception arm to avoid being shadowed.

2. Remove BackendMakeCredentialRequest.EnterpriseAttestation (declared on the
   internal request record but never populated by WebAuthnClient and never read
   by FidoSessionWebAuthnBackend). Public API surface that advertised a feature
   wired nowhere.

3. Remove IWebAuthnBackend.GetUvRetriesAsync, IWebAuthnBackend.GetPinRetriesAsync,
   their FidoSessionWebAuthnBackend implementations, and the now-orphan
   PinRetriesResult record. The interface members were declared and implemented
   but never called from any client code path; consumers using them would
   depend on a code path with no integration coverage. Better to add them back
   later as part of a deliberate PIN-failure-flow story than to ship untested
   surface.

CodeAudit reference: post-dc2ed141 audit findings #2, #3, #7 (HIGH severity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…4 Criticals + 4 HIGH)

Captures full session arc: WebAuthnExcludeListStressTests went FAIL → PASS via
dc2ed14; 4 Critical CodeAudit findings landed in a0070db; 3 HIGH findings
landed in 489c853 + 2b1b085; NFC test using-import fix in f547fca. All
pushed to origin.

Documents the 3 remaining HIGH findings (Tier B ExtensionPipeline + Tier C
WebAuthnClient.cs split absorbing both DRY findings) plus 6 MEDIUM + 6 LOW
audit items as named follow-ups with file:line and effort estimates so the
next session can pick them up cold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2b00

YK 5.8.0-beta firmware accepts only Esp256SplitArkgPlaceholder (-65539,
"ARKG-P256-ESP256") as the request alg for previewSign. Esp256 (-9) names
the *output signature* alg only and is rejected at protocol-decode time
if sent on the wire. This bug was independently caught by python-fido2,
cnh-authenticator-rs, and the Yubico.NET.SDK-Legacy preview-sign branch
(commit fe82b00 — fix on identical hardware, identical firmware).

Three test files updated:

1. src/Fido2/tests/.../FidoPreviewSignTests.cs
   - Registration uses algorithms: [-65539] (was [-9])
   - Output assertion expects -65539 (device echoes the negotiated request
     alg at extension key 3, not the internal sig alg)
   - Comments rewritten to spell out the -9 vs -65539 trap so future
     readers cannot reintroduce the bug

2. src/Fido2/tests/.../PreviewSignCborTests.cs
   - Sample COSE_Sign_Args bytes use alg=-65539 (was -9)
   - Encoder is a passthrough — the value is documentation, not behaviour,
     but documenting the wrong constant is what produced bug #1 in Legacy

3. src/WebAuthn/tests/.../PreviewSignTests.cs
   - Comment on the still-skipped FullCeremony test corrected; explains
     auth still requires additional_args (COSE_Sign_Args) builder, which
     is Phase 10 work — see Plans/phase-10-arkg-sign-args-builder-prd.md

Verification on YK 5.8.0-beta, macOS arm64:
  Yubico.YubiKit.Fido2.UnitTests --filter "*PreviewSign*": 4/4 PASS
  Yubico.YubiKit.WebAuthn.UnitTests --filter "*PreviewSign*": 9/9 PASS
  Yubico.YubiKit.Fido2.IntegrationTests
    MakeCredential_WithPreviewSignExtension_ReturnsGeneratedSigningKey:
    PASS (was FAIL — wrong alg + wrong assertion)

Diagnosis path: two parallel Engineer agents (python-fido2 forensics +
Legacy SDK forensics) independently converged on the same root cause in
one round. Detailed report: /tmp/arkg-forensics/LEGACY_PREVIEWSIGN_FORENSICS.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (Phase 10 §3)

Replaces the opaque `ReadOnlyMemory<byte>? AdditionalArgs` field on
`PreviewSignSigningParams` with a typed, layered, Fido2-canonical builder
for the CTAP v4 `COSE_Sign_Args` map. Makes the `-9` vs `-65539` algorithm
bug class unrepresentable at the type level and ships the surface needed
for the FullCeremony hardware test.

Files changed (8):
- src/Fido2/src/Cose/CoseAlgorithm.cs            (+ ArkgP256 alias = -65539)
- src/Fido2/src/Extensions/CoseSignArgs.cs       (NEW: closed union)
- src/Fido2/src/Extensions/PreviewSignExtension.cs
                                  (PreviewSignSigningParams: AdditionalArgs
                                   → CoseSignArgs; new EncodeCoseSignArgs)
- src/WebAuthn/src/Extensions/PreviewSign/PreviewSignSigningParams.cs
                                  (re-exports Fido2 type, drops ad-hoc CBOR
                                   validity check — type system enforces it)
- src/WebAuthn/src/Extensions/Adapters/PreviewSignAdapter.cs
                                  (passes typed value through unchanged)
- src/Fido2/tests/.../PreviewSignCborTests.cs    (typed builder; 8 new tests)
- src/WebAuthn/tests/.../PreviewSignSigningParamsTests.cs (NEW; 6 tests)
- src/WebAuthn/tests/.../PreviewSignTests.cs     (FullCeremony rewired; still
                                                  Skip.If — awaits Dennis HW)

Public API delta (BREAKING — preview-stage, no external consumers):
  - PreviewSignSigningParams.AdditionalArgs : ReadOnlyMemory<byte>?
  + PreviewSignSigningParams.CoseSignArgs    : CoseSignArgs?
  + abstract record class CoseSignArgs { abstract int Algorithm; static
      ArkgP256(ROM<byte>, ROM<byte>) }
  + sealed   record class ArkgP256SignArgs : CoseSignArgs { ROM<byte>
      KeyHandle; ROM<byte> Context }
  + static   readonly CoseAlgorithm CoseAlgorithm.ArkgP256

Breaking-change justification: the codebase is preview-stage on
webauthn/phase-9.2-rust-port; the only consumers of `AdditionalArgs` were
in this repo (one unit test, one skipped integration test). Keeping both
fields would create a "two ways to do it" trap that re-admits the
`-9` vs `-65539` bug Dennis just fixed in 6ecbae3. The typed API is the
mitigation; leaving the escape hatch defeats it (PRD §6).

Constants (do NOT confuse — covered in XML doc + test asserts):
  -65539  CoseAlgorithm.ArkgP256 / Esp256SplitArkgPlaceholder
          → wire signing-op alg, COSE_Sign_Args key 3 (this PRD)
  -65700  ARKG_P256_PLACEHOLDER.ALGORITHM (python-fido2)
          → seed-key COSE-key alg, different layer (NOT used here)

Validation (binary):
  ✅ dotnet toolchain.cs build
       Build succeeded. 0 Error(s), 1 pre-existing test-sdk warning.
  ✅ Fido2.UnitTests *PreviewSign*  17/17 passed (was 4 — added 13 new)
  ✅ Fido2.UnitTests full suite    371/371 passed (regression check)
  ✅ WebAuthn.UnitTests *PreviewSign* 15/15 passed (was 9 — added 6 new)
  ✅ WebAuthn.UnitTests full suite  100/100 passed (regression check)
  ✅ WebAuthn.IntegrationTests build clean (FullCeremony rewired,
       Skip.If(true) awaiting Dennis manual HW verification)

Spec / forensics references:
- Plans/phase-10-arkg-sign-args-builder-prd.md (binding spec)
- Yubico.NET.SDK-Legacy commit fe82b00 — EncodeArkgSignArgs in
  GetAssertionParameters.cs:402-499 (legacy reference shape)
- python-fido2/tests/test_arkg.py:36-73 (deterministic vectors;
  CTX = "ARKG-P256.test vectors", 22 bytes)
- python-fido2/fido2/cose.py:389-460 (COSE_Sign_Args layer)
- /tmp/arkg-forensics/LEGACY_PREVIEWSIGN_FORENSICS.md (wire format §3.4)

Encoder byte-shape (test-asserted byte-for-byte, 126 bytes total):
  A3                                # map(3)
    03 3A 0001 0002                 #   3 : -65539
    20 58 51 <81-byte KH>           #  -1 : bstr len 81
    21 58 20 <32-byte CTX>          #  -2 : bstr len 32

Memory hygiene: KeyHandle/Context are ReadOnlyMemory<byte> passthroughs
(no internal clone) — caller owns and zeros after the request lands on
the wire (documented in XML doc; per repo CLAUDE.md "Security" guidance
that ROM passthrough is safe in record types).

PRD §4 had a 125-byte arithmetic typo — corrected to 126 in the test
sanity assertion. Byte-for-byte structural assertion is the binding
contract.

Hardware FullCeremony test pending Dennis manual verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Sign

Root cause: Hardware-failing test `Registration_WithPreviewSign_ReturnsGeneratedSigningKey`
on YubiKey 5.8.0-beta crashed with `System.InvalidOperationException: Cannot perform the
requested operation, the next CBOR data item is of major type '0'` at
WebAuthnAttestationObject.cs:79.

The inner attestation object embedded in `unsignedExtensionOutputs["previewSign"][7]` is
CTAP-shaped — its keys are integers (`{1:fmt, 2:authData, 3:attStmt}`), NOT WebAuthn
text strings (`{"fmt","authData","attStmt"}`). The Fido2 decoder was returning the raw
inner CBOR bytes verbatim and the WebAuthn adapter was feeding them straight into
`WebAuthnAttestationObject.Decode`, which expects the WebAuthn text-keyed shape — hence
"major type 0" (unsigned int 1 for `fmt`) where it expected major type 3 (text string).

This matches the on-the-wire layout documented in the legacy SDK
(Yubico.NET.SDK-Legacy/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/PreviewSignExtension.cs:144-147,
249-282) and was masked in the unit-test suite by `BuildAttestationObject` synthesizing a
WebAuthn-shaped (text-keyed) attestation object — the test exercised a shape the device
never emits. Phase 10 §3's typed `CoseSignArgs` builder is unaffected and remains green.

Fix:
- `Fido2.Extensions.PreviewSignCbor.DecodeUnsignedRegistrationOutput` now returns a typed
  `InnerAttestationObject(string Fmt, ReadOnlyMemory<byte> AuthData,
  ReadOnlyMemory<byte> AttStmtRawCbor)` decoded from the CTAP-shaped inner map. Per the
  layering rule, canonical CBOR decode lives in Fido2.
- `WebAuthn.Extensions.Adapters.PreviewSignAdapter.ParseRegistrationOutput` consumes the
  decoded components and rebuilds the spec attestation object via the existing
  `WebAuthnAttestationObject.Create(authenticatorData, statement)` factory. WebAuthn owns
  the wrap to the spec shape; no CBOR is decoded here.
- `PreviewSignAdapterTests.BuildAttestationObject` rewritten to emit the CTAP-shaped
  inner map so the unit test reflects what the YubiKey actually returns. This becomes the
  regression test for the parser bug.

Breaking change (internal): `DecodeUnsignedRegistrationOutput` return type changed from
`ReadOnlyMemory<byte>` to `PreviewSignCbor.InnerAttestationObject`. The only caller in
the codebase is `PreviewSignAdapter`; preview-stage internal API per branch convention.

Verification:
- Build: `dotnet toolchain.cs build` → Succeeded.
- Fido2 unit tests: 371/371 pass (PreviewSignExtension change covered).
- WebAuthn unit tests: 100/100 pass (regression test now reflects device truth).
- Hardware integration test will be re-run by Dennis on YubiKey 5.8.0-beta.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…wSign hardware-path advance

- Adds binding PRD for Phase 10 §3 typed CoseSignArgs builder (was untracked
  through commits 6ecbae3/adcff793/0fbeb9c9; locked-decisions block in §7
  records all 9 design choices Dennis approved this session)
- Rewrites Plans/handoff.md to reflect 3-commit afternoon wave:
    6ecbae3 previewSign -9 -> -65539 algo fix (Fido2 HW test PASS)
    adcff79 typed CoseSignArgs builder (471 unit tests green)
    0fbeb9c WebAuthn CTAP-shaped attestation parser fix
- Surfaces newly-discovered ARKG-P256 CoseKey decoder gap as Open
  Follow-up A (Phase 10 §4 candidate); WebAuthn FullCeremony hardware
  test now blocked on this + Yubico.Core ARKG port

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ign key holders

Port Yubico.Core ARKG-P256 cryptographic primitives (525-LOC OpenSSL P/Invoke,
7 ZeroMemory sites, 3 KAT vectors) from Legacy SDK. Add typed CoseArkgP256SeedKey
decoder, PreviewSignGeneratedKey/PreviewSignDerivedKey with offline ARKG derivation
and ECDSA signature verification. Hardware-verified registration + ARKG seed-key
extraction + offline derivation on YK 5.8.0-beta.

FullCeremony GetAssertion+previewSign+ARKG remains skipped — CBOR encoding and ARKG
derivation proven byte-identical to python-fido2 (cross-verified), but firmware
rejects with 0x7F. Root cause narrowed to macOS HID transport layer; needs targeted
FidoHidProtocol deep-dive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves Audit Gate 3 finding H-3 by adding CTAP error mapping for
previewSign extension during authentication. Maps authenticator errors
(UnsupportedAlgorithm, InvalidCredential, MissingParameter, etc.) to
typed WebAuthnClientError enums instead of surfacing raw CtapException.

Matches existing MakeCredential pattern. Removes TODO comment (line 715).

Verified: All 100 WebAuthn + 377 Fido2 unit tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DennisDyallo and others added 7 commits May 14, 2026 01:38
The ARKG-P256 seed-key wire format per draft-bradleylundberg-cfrg-arkg-10
and python-fido2 (cose.py:428-433) is:
  -1 = pkBl  (blinding public key)
  -2 = pkKem (KEM public key)

Modern's Decode and Encode had these inverted, causing the offline ARKG
key handle to be encapsulated against the wrong KEM key. Firmware could
not decapsulate, returning CTAP2_ERR_OTHER (0x7F) at GetAssertion time.

The morning's "byte-identical wire" cross-check missed this because the
CBOR bytes are identical end-to-end - only the labels attached to the
two nested COSE keys were swapped. The existing
Decode_WithValidArkgSeedKey_ReturnsCorrectVariant test embedded the same
inversion in its producer/consumer expectations, so CI green proved
nothing.

Adds Decode_pkBlAtMinus1_pkKemAtMinus2_PerSpec - a spec-contract
regression guard that uses distinguishable byte patterns
(BL=0xAA, KEM=0xBB) at CBOR keys -1/-2 and asserts the decoder routes
them to BlPublicKey and KemPublicKey respectively. Corrects the existing
test to use the spec-correct wire layout.

Hardware verified: FullCeremony_RegisterDeriveSignVerify_RoundTrip
passes end-to-end on YubiKey 5.8.0-beta (12s, 2 user-presence touches).

The integration test changes also fold in test-correctness fixes
hardware-validated during phase D forensics:
- omit second PIN-token acquisition (firmware previewSign state)
- options.UserPresence = false to mirror python-fido2 reference
- registration flags = 0 (no UP requirement on derived signing key)
- non-resident credential (no rk option)
- pass raw message to VerifySignature (avoids double-hash via VerifyData)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Handoff captures the full diagnostic chain (legacy ↔ python-fido2 ↔ modern
parser diff), the swap-fix application via TDD, and the hardware-validated
end-to-end green for FullCeremony.

The strolling-star plan is the focused fix plan that replaced the parked
wire-chatter cleanup plan; it stays for traceability of the root-cause
narrative across future agents reading the PR history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prerelease 1.16.1-prerelease.20260428.1 is not on NuGet's official feed;
CI resolves 1.16.1 instead, triggering NU1603 (warning-as-error).
Pin to the stable release that is actually available.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

2 participants