Skip to content

fix: code audit fixes across all YubiKey applet modules#455

Open
DennisDyallo wants to merge 34 commits intoyubikit-appletsfrom
yubikey-codeaudit
Open

fix: code audit fixes across all YubiKey applet modules#455
DennisDyallo wants to merge 34 commits intoyubikit-appletsfrom
yubikey-codeaudit

Conversation

@DennisDyallo
Copy link
Copy Markdown
Collaborator

@DennisDyallo DennisDyallo commented Apr 15, 2026

Summary

Comprehensive 5-stage code audit and remediation across all 7 YubiKey applet modules + Core. 27 commits, 87 files changed, net -244 lines.


Stage 1: Bug Fixes & Security (7 commits)

Targeted fixes for correctness bugs, security gaps, and dead code removal.

Module Key Fixes
Piv TLV encoding bug for 128-255 byte certs, buffer zeroing, connection leak, bounds check
Fido2 SHA256 API modernization, auth tag zeroing, DisposeAsync cleanup, dead code
Oath APDU secret buffer zeroing, GeneratedRegex, culture-safe parsing
YubiOtp Connection leak on session failure, NDEF truncation validation
OpenPgp DER P-521 signature encoding bug, private key zeroing, pin truncation guard
SecurityDomain Swapped ValidateCheckSum args (real bug), key zeroing, resource leaks, dead code
YubiHsm PublicKeyLength off-by-one (64→65), wrong error handler

Stage 2: DRY Refactoring & Interface Expansion (7 commits)

Module Key Changes
Piv EnsureProtocol() with [MemberNotNull] (27 sites), merge Encrypt/DecryptBlock, merge auth parsers
Fido2 Fix ExtensionOutput.Decode (silently discarding data), extract CoseKeyWriter, dedup CBOR parsers
Oath Credential.Id → ReadOnlyMemory<byte>, TLV concat helpers, CalculateAsync return type, ArrayBufferWriter
YubiOtp Extract ProcessHmacKey/InitializeKeys to base classes, response validation
OpenPgp Fix X25519 slot algorithm, consolidate 5 KeyRef switches → 1, static dictionary
SecurityDomain Expand ISecurityDomainSession (6→15 methods), extract PutEcKeyAsync, [MemberNotNull]
YubiHsm Extract ThrowOnRetryFailure, TransmitWithRetryCheckAsync (9 sites), immutable test data

Stage 3: Cross-Module Core Extraction (4 commits)

Scope Details
Core utilities BerLength.Write/EncodingSize, SWConstants.ExtractRetryCount/IsVerifyFailWithRetries
Connection leak Fix in 6 remaining modules (Fido2, Oath, OpenPgp, SecurityDomain, YubiHsm, Management)
Consumer updates PIV uses BerLength (8 sites), YubiHsm/OpenPgp use ExtractRetryCount
Dead code cleanup Remove unused EnsureReady() from ApplicationSession

Stage 4: Remaining Findings + All Deferred Items (6 commits)

Module Key Changes
Piv DES inputArr zeroing, BER lengths in BuildAuthResponse, retry extraction (5→SWConstants), List<byte>→ArrayBufferWriter
Fido2 LargeBlob CBOR double-SkipValue parse bug, extract Encapsulate ECDH helper (V1/V2 DRY)
Oath CredentialData IDisposable (zeroes Secret), ValidateAsync/SetKeyAsync → ReadOnlyMemory<byte>, StringComparison
YubiOtp Strict access code validation (no silent truncation), cancellation exception filter
OpenPgp Hash buffer zeroing, Kdf Dispose chain fix, BerLength for ASN.1 INTEGER encoding
SecurityDomain Tlv disposal (5 methods), CA identifier bounds check, remove caller key mutation, DI firmware param

Stage 5: Cross-Module Tlv Disposal (3 commits)

Scope Details
Core Add TlvHelper.EncodeAndDisposeList — encodes then disposes each Tlv in finally
YubiHsm Replace 10 EncodeList sites with EncodeAndDisposeList (sensitive data in buffers)
Piv Dispose Tlv objects in 4 parse methods (copy data out before disposal)

Items Explicitly Skipped (with rationale)

  • Fido2 CBOR construction DRY: 3 different auth message formats, abstraction cost exceeds benefit
  • YubiOtp UpdateConfiguration duplication: 11 trivial 4-line methods, CRTP doesn't compose
  • Oath CredentialData.Secret return type: Would need IMemoryOwner wrapper, over-engineering for one callsite
  • Session init boilerplate extraction: Module-specific firmware detection makes shared helper impractical

Audit Statistics

Metric Count
Total findings identified ~133 (initial) + 23 (re-audit)
Findings fixed ~113
Items skipped (with rationale) 4
Commits 27
Files changed 87
Lines added 1,386
Lines removed 1,630
Net change -244 lines

Test plan

  • Full solution builds with 0 errors, 0 warnings
  • Unit tests pass (8/9 — Fido2 failure is pre-existing testhost infrastructure issue)
  • Integration tests (manual — requires physical YubiKey)
    • PIV: mutual auth (retry count), certificate store (ArrayBufferWriter), bio metadata
    • Fido2: large blob write/read (CBOR fix), PIN operations (Encapsulate DRY)
    • Oath: credential CRUD with IDisposable, validate/setKey with new param types
    • SecurityDomain: EC private key import (no ZeroMemory mutation), SCP11 allowlist, CA identifiers
    • YubiHsm: all credential operations (EncodeAndDisposeList)
    • YubiOtp: NDEF long URIs, access code operations (strict validation)
    • OpenPgp: P-521 signing (BER length), PIN verify (retry count)
    • All modules: session creation failure path (connection leak fix)

🤖 Generated with Claude Code

DennisDyallo and others added 30 commits April 15, 2026 09:42
- Fix TLV length encoding for 128-255 byte certificates (missing 0x81 case)
- Zero inputBuffer before ArrayPool return in DecryptBlock/EncryptBlock
- Add bounds check in ParseVersionResponse for truncated responses
- Zero ModPow intermediate bytes (BigInteger.ToByteArray result)
- Fix connection leak in CreatePivSessionAsync on failure
- Remove empty CheckAlgorithmSupport method
- Delete placeholder tests

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Replace legacy SHA256.Create().ComputeHash() with static SHA256.HashData()
- Zero expected auth tag after FixedTimeEquals in PinUvAuthProtocol V1/V2
- Remove pointless async/await Task.CompletedTask in DisposeAsync
- Remove overly broad catch(Exception) in LargeBlobEntry.TryDecrypt
- Remove dead MutableTemplateInfo and TemplateInfoExtensions file classes
- Modernize null checks to ArgumentNullException.ThrowIfNull()
- Delete placeholder tests

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Zero data buffer containing secret in PutCredentialAsync finally block
- Replace FixedTimeEquals with SequenceEqual for non-secret credential IDs
- Eliminate string allocations in CompareTo via StringComparison.OrdinalIgnoreCase
- Convert TotpIdPattern to [GeneratedRegex] for AOT-friendly source generation
- Add CultureInfo.InvariantCulture to all int.Parse calls
- Use StringComparison.Ordinal for Contains(':') check
- Delete placeholder tests

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Fix connection leak in CreateYubiOtpSessionAsync on failure
- Throw ArgumentException on NDEF content exceeding 54-byte data area
- Add ArgumentNullException.ThrowIfNull for connection parameter
- Extract static readonly FirmwareVersion constants
- Remove unused System.Buffers import
- Delete placeholder test

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Fix DER SEQUENCE length encoding for P-521 signatures (seqLength >= 128)
- Zero private key material in PrivateKeyTemplate.ToBytes() intermediates
- Add upper bound validation (<=255) in SetPinAttemptsAsync to prevent truncation
- Fix XML doc for VerifyPinAsync extended parameter (was backwards)
- Remove unused DigestInfo headers (Sha224, Sha512_224, Sha512_256)
- Delete unused CardholderRelatedData class
- Replace FixedTimeEquals with SequenceEqual for non-secret OID comparison
- Remove redundant (Pw) casts in VerifyPwAsync
- Delete placeholder tests

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ead code

- Fix swapped ValidateCheckSum arguments in PutKeyAsync(ECPrivateKey)
- Fix hardcoded "ExceptionMessages.ChecksumError" string literal
- Zero private key material (parameters.D) after encryption
- Add using var for DisposableTlvList in GetCertificatesAsync/GetCaIdentifiersAsync
- Add CancellationToken to ClearAllowListAsync
- Remove dead code: ParseCertificateBundle, ParseCaIdentifiers, EncodeControlReference, EncodeKeyReference
- Rename StoreAllowlistAsync → StoreAllowListAsync for consistent PascalCase
- Modernize to collection expressions and stackalloc

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…e salt

- Fix PublicKeyLength from 64 to 65 for EcP256YubicoAuthentication
- Fix ChangeCredentialPasswordAsync to use ThrowOnCredentialPasswordFailure
- Change Pbkdf2Salt from mutable byte[] to ReadOnlySpan<byte> property
- Add public key length validation in GetPublicKeyAsync
- Remove unreachable MinLabelLength check
- Remove stale TODO comment
- Delete placeholder tests

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Extract EnsureProtocol() with [MemberNotNull] replacing 27 inline null checks
- Merge DecryptBlock/EncryptBlock into single CryptoBlock method
- Merge ParseWitnessResponse/ParseChallengeResponse into ParseAuthResponse
- Extract EncodeObjectId helper from GetObjectAsync/PutObjectAsync

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…edup parsers

- Fix ExtensionOutput.Decode to delegate to DecodeWithRawData (was silently
  discarding all extension data)
- Add mutual exclusion guard for largeBlob/largeBlobAssertion in ExtensionBuilder
- Extract shared CoseKeyWriter from ClientPin and HmacSecretInput
- Replace duplicate CBOR parsers in CredentialManagementModels with canonical
  Parse methods from PublicKeyCredentialDescriptor/UserEntity

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…erWriter

- Change Credential.Id from byte[] to ReadOnlyMemory<byte> with defensive copy
- Extract ConcatTlvs helpers replacing 5 manual TLV concatenation sites
- Change CalculateAsync return type to Task<ReadOnlyMemory<byte>>
- Replace MemoryStream+ToArray with ArrayBufferWriter in CollectResponseData

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…n, response check

- Extract ProcessHmacKey into SlotConfiguration base class (was duplicated in
  HmacSha1SlotConfiguration and HotpSlotConfiguration)
- Extract InitializeKeys into KeyboardSlotConfiguration (was duplicated in
  YubiOtpSlotConfiguration and StaticTicketSlotConfiguration)
- Add response length validation in OtpHidBackend.WriteUpdateAsync

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…static dict

- Fix EcAttributes.Create to always use ECDH algorithm ID for X25519
- Consolidate 5 KeyRef switch dispatchers into single GetKeyRefData tuple method
- Extract static AlgoAttributeToKeyRef dictionary (was rebuilt per-call)
- Replace .AsMemory().ToArray() with .AsSpan().ToArray() in 4 files

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…rNotNull

- Expand ISecurityDomainSession from 6 to 15 methods (all public methods now
  on the interface for DI/mocking)
- Extract shared PutEcKeyAsync helper from ECPublicKey/ECPrivateKey overloads
- Add [MemberNotNull] to EnsureInitializedProtocol, remove null-forgiving operators

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
… test data

- Extract ThrowOnRetryFailure shared by management key and credential password handlers
- Extract TransmitWithRetryCheckAsync applied to 9 call sites
- Change test DefaultManagementKey from mutable byte[] to ReadOnlyMemory<byte>

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Add BerLength.Write/EncodingSize for correct BER-TLV length encoding
  (eliminates error-prone hand-rolled encoding across modules)
- Add SWConstants.ExtractRetryCount/IsVerifyFailWithRetries for 0x63Cx
  retry count extraction (duplicated in PIV, YubiHsm, OpenPgp)
- Add EnsureReady() to ApplicationSession base class combining
  ThrowIfDisposed + IsInitialized checks

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Fix connection resource leak when session creation throws after
ConnectAsync succeeds. Affects: Fido2, Oath, OpenPgp, SecurityDomain,
YubiHsm, Management. PIV and YubiOtp were already fixed in stage 1.

Each module now wraps CreateAsync in try/catch that disposes the
connection on failure, matching the PIV/YubiOtp pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- PIV: Replace 8 hand-rolled BER-TLV length encoding sites with
  BerLength.Write/EncodingSize (Certificates, Crypto, DataObjects)
- YubiHsm: Delegate ExtractRetries to SWConstants.ExtractRetryCount
- OpenPgp: Replace inline retry extraction with SWConstants.ExtractRetryCount
  (also fixes subtle bug: old mask 0xFF00 matched 0x6300-0x63FF, new mask
  0xFFF0 correctly matches only 0x63C0-0x63CF)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
EnsureReady() was added but never adopted by any module — each session
has its own [MemberNotNull]-annotated protocol guard that the compiler
requires for nullable analysis. Removing to avoid dead code.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Zero inputArr in DesBlockOperation finally block (security)
- Replace single-byte length casts in BuildAuthResponse with BerLength.Write
- Add TODO for GetBioMetadataAsync positional parsing (needs TLV verification)
- Replace 5 inline retry count extractions with SWConstants.ExtractRetryCount
- Delegate PivPinUtilities.GetRetriesFromStatusWord to SWConstants
- Replace List<byte> with ArrayBufferWriter in StoreCertificateAsync/PutObjectAsync

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Fix double SkipValue in ParseDecryptedBlob (corrupted parse on non-empty
  byte string keys — conditional skip based on whether key was consumed)
- Extract shared ECDH P-256 key agreement into PinUvAuthHelpers, eliminating
  ~40 lines of duplicated code between PinUvAuthProtocolV1/V2.Encapsulate

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…params

- Make CredentialData implement IDisposable, zeroing Secret on Dispose
- Zero intermediate HmacShortenKey buffer when different from input
- Update 4 caller sites to use 'using' (CLI commands, examples)
- Change ValidateAsync/SetKeyAsync params from byte[] to ReadOnlyMemory<byte>
- Fix path.Contains(':') missing StringComparison.Ordinal

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ilter

- Replace silent access code truncation with strict size validation
- Add exception filter to prevent swallowing OperationCanceledException

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…NTEGER

- Zero FormatEcSignPayload hash buffer after sign/authenticate APDU
- Fix KdfIterSaltedS2k.Dispose() to call base.Dispose()
- Replace single-byte length in EncodeAsn1Integer with BerLength.Write
- Fix KdfNone.ToBytes() .AsMemory().ToArray() → .AsSpan().ToArray()

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…n, DI param

- Fix GetCaIdentifiersAsync bounds check (Length >= 2 instead of !IsEmpty)
- Dispose Tlv objects in 5 methods (StoreAllowList, StoreCaIssuer,
  StoreCertificates, GetCertificates, and nested constructions)
- Remove ZeroMemory(parameters.D) that mutated caller's ECPrivateKey
- Add firmwareVersion parameter to SecurityDomainSessionFactory delegate

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Encodes a collection of TLV objects then disposes each in a finally
block. Prevents sensitive data from lingering in undisposed Tlv buffers
when Tlvs are created inline for EncodeList calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Replace TlvHelper.EncodeList with EncodeAndDisposeList at all call sites
where Tlvs are created inline. Prevents sensitive data (management keys,
credential passwords, private keys) from lingering in undisposed buffers.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add using to Tlv objects in ParseAuthResponse, ParseCryptoResponse,
UnwrapDataObjectResponse, and ParsePublicKey. Copies data out before
disposal to avoid use-after-dispose.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…shared core

Restore clear encrypt/decrypt semantics at call sites while keeping
shared buffer management in CryptoBlockCore. Parameters renamed to
plaintext/ciphertext for self-documenting data flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Plans/todo-backlog-workplan.md: 19 Jira issues (YESDK-1559–1577) prioritized
- Plans/foamy-swimming-summit.md: Stage 4 implementation plan with deferred analysis
- Plans/handoff.md: Updated with full 5-stage session summary

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
DennisDyallo and others added 4 commits April 15, 2026 13:17
ModPow returned the raw BigInteger byte array directly when its length
matched the output size, but the finally block then zeroed that same
array via CryptographicOperations.ZeroMemory. Since arrays are reference
types, callers received a zeroed buffer, breaking all RSA decrypt ops.

Clone with .AsSpan().ToArray() before returning so the finally block
safely zeros only the BigInteger's internal allocation.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
… format

Replace 36 old-format [Trait("RequiresUserPresence", "true")] with
[Trait(TestCategories.Category, TestCategories.RequiresUserPresence)]
across 17 files. The old format was not caught by the --smoke filter
(Category!=RequiresUserPresence), causing 39 false failures.

Add missing trait to 8 test methods that require user presence but
had no trait at all: ToggleAlwaysUv, SetMinPinLength,
GetFingerprintSensorInfo, GetCredentialsMetadata, and 4
EncryptedMetadata tests.

Remove duplicate trait on FidoPinManagementTests.ChangePin.
Update comment in FidoSessionSimpleTests to reference new format.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
HidYubiKey.DeviceId used VendorId:ProductId:Usage which is identical
across multiple YubiKeys, causing one key's HID interfaces to overwrite
the other in the device cache. Include ReaderName (unique device path)
in DeviceId, matching how PcscYubiKey already uses ReaderName.

ClientPin.GetPinUvAuthTokenUsingPinAsync always sent CTAP2.1 subcommand
0x09 (getPinUvAuthTokenUsingPinWithPermissions), failing on CTAP2.0
devices. Check pinUvAuthToken option from GetInfo and fall back to
legacy getPinToken (subcommand 0x05) when unsupported, matching
python-fido2's ClientPin.get_pin_token() logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant