Skip to content

feat(fido2): WebAuthn previewSign extension#468

Open
DennisDyallo wants to merge 42 commits into
developfrom
feature/webauthn-preview-sign
Open

feat(fido2): WebAuthn previewSign extension#468
DennisDyallo wants to merge 42 commits into
developfrom
feature/webauthn-preview-sign

Conversation

@DennisDyallo
Copy link
Copy Markdown
Collaborator

@DennisDyallo DennisDyallo commented Apr 28, 2026

Summary

Adds the previewSign WebAuthn extension to the v1 SDK (ARKG-P256 key generation, offline derivation, sign-by-credential, offline signature verification) targeting YubiKey 5.8.0-beta firmware. Ported from yubikit-swift release/1.3.0, cross-verified against python-fido2, the Rust cnh-authenticator-rs-extension reference, and the upstream Yubico/5-8-hello-world quickstart.

ARKG-P256 (RFC draft bradleylundberg-cfrg-arkg-09) implemented in Yubico.Core.Cryptography.ArkgPrimitivesOpenSsl via OpenSSL P/Invoke through Yubico.NativeShims. No BouncyCastle dependency.

Stacking: depends on #472. Merge #472 first — this branch consumes its new Native_EC_POINT_is_on_curve export, the Yubico.NativeShims 1.16.1-prerelease.20260428.1 version bump, and the relocated HkdfUtilities.

Verification

Suite Result
Yubico.YubiKey.UnitTests 3588 PASS
Yubico.Core.UnitTests 488 PASS / 21 SKIP (Windows-only on macOS)
ARKG KAT vectors (ArkgP256Tests + ArkgPrimitivesTests) 8 PASS — byte-for-byte vs Rust hid-test reference
Hardware integration: PreviewSignTests (3 tests) ✅ on YubiKey 5.8.0-beta (5C NFC)
DocFX /t:DocFXBuild warnings-as-errors ✅ clean

CI note

submit-nuget will fail until Yubico.NativeShims 1.16.1-prerelease.20260428.1 is reachable from the public NuGet feed (currently published only to Yubico's internal feed). All other CI checks green across Windows/Linux/macOS.

In-branch design docs

Plans/preview-sign-v1-port-plan.md (design), codeaudit-previewsign-port.md (vslsp + crypto audit), python-fido2-reference-findings.md (cross-impl archaeology), previewsign-followups.md (deferred cleanup with suggested PR sequence). Untracked — intentionally kept out of this PR's tree.

@DennisDyallo DennisDyallo force-pushed the feature/webauthn-preview-sign branch 2 times, most recently from 1cf9541 to 9960876 Compare April 28, 2026 11:58
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Test Results: Windows

    2 files      2 suites   40s ⏱️
4 171 tests 4 149 ✅ 22 💤 0 ❌
4 173 runs  4 151 ✅ 22 💤 0 ❌

Results for commit be57c2c.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Test Results: Ubuntu

    2 files      2 suites   1m 8s ⏱️
4 163 tests 4 141 ✅ 22 💤 0 ❌
4 165 runs  4 143 ✅ 22 💤 0 ❌

Results for commit 216c956.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Test Results: MacOS

    4 files      4 suites   54s ⏱️
4 145 tests 4 142 ✅ 3 💤 0 ❌
4 147 runs  4 144 ✅ 3 💤 0 ❌

Results for commit 216c956.

♻️ This comment has been updated with latest results.

@DennisDyallo DennisDyallo requested a review from Copilot April 28, 2026 12:09
Comment thread Yubico.Core/src/Yubico.Core.csproj Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for the WebAuthn previewSign extension to the FIDO2 surface area of Yubico.NET.SDK, including ARKG-P256 offline public-key derivation and sign-by-credential plumbing, plus associated cryptographic/native support and tests.

Changes:

  • Introduces previewSign extension APIs for MakeCredential (generate key) and GetAssertion (sign by derived credential), plus parsing of signed/unsigned extension outputs.
  • Adds ARKG-P256 primitives (OpenSSL-backed via NativeShims) and wires them through CryptographyProviders.
  • Adds unit/integration tests for previewSign + ARKG, and a test-only cache warmup workaround for a macOS device-listener race.

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs Updates allow-list path documentation for macOS/Linux.
Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/DeviceListenerCacheWarmup.cs Adds test utility to wait for initial device enumeration to populate.
Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/PreviewSignExtensionTests.cs Unit tests for previewSign CBOR encoding/decoding and regression scenarios.
Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Arkg/ArkgP256Tests.cs Known-answer tests for ARKG-P256 derivation behavior.
Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/HkdfUtilitiesTests.cs Updates HKDF tests to reference the moved HKDF utility.
Yubico.YubiKey/tests/integration/Yubico/YubiKey/Fido2/PreviewSignTests.cs Adds end-to-end integration tests for previewSign generate/derive/sign/verify.
Yubico.YubiKey/src/Yubico/YubiKey/Fido2/PreviewSignGeneratedKey.cs New public type representing generated key material and offline derivation entrypoint.
Yubico.YubiKey/src/Yubico/YubiKey/Fido2/PreviewSignExtension.cs Implements CBOR encoder/decoder for previewSign extension input/output.
Yubico.YubiKey/src/Yubico/YubiKey/Fido2/PreviewSignDerivedKey.cs New public type representing derived key material and signature verification helper.
Yubico.YubiKey/src/Yubico/YubiKey/Fido2/MakeCredentialParameters.cs Adds API to request previewSign key generation during registration.
Yubico.YubiKey/src/Yubico/YubiKey/Fido2/MakeCredentialData.cs Adds parsing of CTAP unsigned extension outputs and accessor for generated key.
Yubico.YubiKey/src/Yubico/YubiKey/Fido2/GetAssertionParameters.cs Adds API to request previewSign signing-by-credential during assertions.
Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Extensions.cs Adds previewSign extension identifier constant.
Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Cose/CoseAlgorithmIdentifier.cs Adds previewSign-related COSE algorithm identifiers (Esp256, ArkgP256Esp256).
Yubico.YubiKey/src/Yubico/YubiKey/Fido2/AuthenticatorInfo.cs Updates imports to use the relocated HKDF utility.
Yubico.YubiKey/src/Yubico/YubiKey/Fido2/AuthenticatorData.cs Adds accessor to extract previewSign signature from signed extension outputs.
Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Arkg/ArkgP256.cs Adds ARKG-P256 wrapper to route derivation through Core primitives.
Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/CryptographyProviders.cs Adds ArkgPrimitivesCreator delegate to allow swapping ARKG primitive implementation.
Yubico.NativeShims/ssl.ecpoint.c Exposes EC_POINT_is_on_curve via NativeShims for on-curve validation.
Yubico.NativeShims/exports.msvc Exports new Native_EC_POINT_is_on_curve symbol on Windows (MSVC).
Yubico.NativeShims/exports.llvm Exports new symbol for LLVM toolchains.
Yubico.NativeShims/exports.gnu Exports new symbol for GNU toolchains.
Yubico.Core/tests/unit/Yubico/Core/Cryptography/ArkgPrimitivesTests.cs Adds Core-level tests for on-curve validation and ECDH behavior.
Yubico.Core/src/Yubico/PlatformInterop/Desktop/Cryptography/EcPoint.Interop.cs Adds P/Invoke wrapper for Native_EC_POINT_is_on_curve.
Yubico.Core/src/Yubico/Core/Cryptography/IArkgPrimitives.cs Introduces interface contract for ARKG primitives (validation, ECDH, derive).
Yubico.Core/src/Yubico/Core/Cryptography/HkdfUtilities.cs Moves/renames HKDF utility into Core and updates implementation details.
Yubico.Core/src/Yubico/Core/Cryptography/ArkgPrimitivesOpenSsl.cs Implements ARKG-P256 primitives and derivation using OpenSSL via NativeShims.
Yubico.Core/src/Yubico/Core/Cryptography/ArkgPrimitives.cs Adds factory for default ARKG primitives implementation.
Yubico.Core/src/Yubico.Core.csproj Updates NativeShims package dependency version for the new export.
Comments suppressed due to low confidence (2)

Yubico.Core/src/Yubico/Core/Cryptography/HkdfUtilities.cs:74

  • HkdfExtract creates temporary byte[] copies of both the salt and the input keying material via ToArray(). Since IKM can be secret material, these temporary arrays should be zeroed after ComputeHash to avoid extending the lifetime of sensitive data in memory.
    Yubico.Core/src/Yubico/Core/Cryptography/HkdfUtilities.cs:87
  • HkdfExpand currently calls pseudoRandomKey.ToArray() to build the HMAC instance, which creates an extra copy of the PRK. Since DeriveKey already zeroes the original PRK, this additional copy will remain in memory unless explicitly cleared. Consider avoiding the extra copy (e.g., accept byte[] and pass it through) and/or ensuring any extra key buffer is zeroed when no longer needed.

Comment thread Yubico.YubiKey/src/Yubico/YubiKey/Fido2/GetAssertionParameters.cs
Comment thread Yubico.YubiKey/src/Yubico/YubiKey/Fido2/PreviewSignGeneratedKey.cs Outdated
@DennisDyallo DennisDyallo force-pushed the feature/webauthn-preview-sign branch 8 times, most recently from 3a99633 to 9c68ea2 Compare April 29, 2026 14:07
@DennisDyallo DennisDyallo changed the title Feature/webauthn preview sign feat(fido2): WebAuthn previewSign extension (ARKG-P256) Apr 29, 2026
@DennisDyallo DennisDyallo force-pushed the feature/webauthn-preview-sign branch 3 times, most recently from 1d36eaf to dd829ec Compare April 29, 2026 15:00
@DennisDyallo DennisDyallo changed the base branch from develop to feature/nativeshims-tests April 29, 2026 15:02
DennisDyallo and others added 7 commits April 29, 2026 17:03
Phase 1 of the previewSign port establishes the test-first behavioral
spec. All public API stubs throw NotImplementedException; tests compile
and fail at runtime. Subsequent phases turn red tests green:
  Phase 2 — NativeShims native additions
  Phase 3 — Managed P/Invoke + ArkgPrimitivesOpenSsl
  Phase 4 — ARKG-P256 algorithm port (from Yubico Rust reference)
  Phase 5 — Fido2 extension wiring (CBOR encoders, parsers)
  Phase 6 — Doc pass + final green-test verification

Includes two required regression tests:
  - ParseGenerateKeyFromUnsignedExtensions_KeyAt6 — locks CTAP key 6
    (matches yubikit-swift release/1.3.0, Java, Python; v2 .NET uses 8
    and is the outlier — separate bug report queued)
  - AddPreviewSignByCredential_ThrowsWhenExtensionUnsupported — guards
    against a missing-validation bug found in the upstream PoC

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the OpenSSL-backed ARKG primitives needed by the previewSign WebAuthn
extension: SEC1 on-curve validation via the new EcPointIsOnCurve P/Invoke
wrapper, and ECDH shared-secret computation that rejects invalid-curve
inputs before any scalar multiplication. Wires a CryptographyProviders
factory delegate following the EcdhPrimitivesCreator pattern.

IArkgPrimitives is exposed publicly to mirror the existing IEcdhPrimitives
contract; making it internal would require a Yubico.YubiKey
InternalsVisibleTo entry that collides with the Nullable polyfill.

Phase 3 of the previewSign port. Phase 4 (ArkgP256 algorithm) and Phase 5
(extension wiring) tests remain RED with NotImplementedException.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements draft-bradleylundberg-cfrg-arkg-09 derivation entirely in
managed code, with point arithmetic via Yubico.NativeShims and scalar
reduction via System.Numerics.BigInteger. The full algorithm body lives
in ArkgPrimitivesOpenSsl.Derive (Yubico.Core) so it can use the internal
OpenSSL P/Invoke surface that is not visible from Yubico.YubiKey;
ArkgP256.cs (Yubico.YubiKey) becomes a thin wrapper that routes through
CryptographyProviders.ArkgPrimitivesCreator.

Adds three KAT vectors generated from cnh-authenticator-rs-extension's
arkg.rs reference. Vector A reproduces the embedded Rust unit-test
expected output byte-for-byte; vectors B/C exercise distinct-IKM and
distinct-ctx paths with bit-exact expected derived public keys.

Phase 4 of the previewSign port. Phase 5 (extension wiring) tests
remain RED with NotImplementedException.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces all Phase 1 stubs with real implementations:
- New PreviewSignExtension (internal) — CBOR encoder/decoder using
  per-context enums (MakeCredentialKey {3,4,7}, GetAssertionKey {2,6,7})
  to keep the deliberately reused integers readable.
- AddPreviewSignGenerateKeyExtension and AddPreviewSignByCredentialExtension
  both call IsExtensionSupported (the second was Alan's bug — guarded by a
  named regression test).
- MakeCredentialData parses UnsignedExtensionOutputs at CTAP response
  key 6 (NOT 8 — verified against yubikit-swift release/1.3.0; explicit
  regression test asserts the value).
- AuthenticatorData.GetPreviewSignSignature pulls the DER-encoded ECDSA
  signature from authData.extensions["previewSign"] map key 6.
- PreviewSignGeneratedKey.DerivePublicKey routes to ArkgP256.
- PreviewSignDerivedKey.VerifySignature uses the existing EcdsaVerify
  helper for portability across netstandard2.0/2.1/net472.

Flags rule: requireUv -> 0b101 (RequireUserVerification), else 0b001
(RequireUserPresence). Derived from the AddExtension call argument, never
a user knob.

Phase 5 of the previewSign port. All 9 PreviewSignExtensionTests pass,
including ParseGenerateKeyFromUnsignedExtensions_KeyAt6 and
AddPreviewSignByCredential_ThrowsWhenExtensionUnsupported.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added comprehensive XML documentation for all public APIs in the
previewSign WebAuthn extension implementation:

- Enhanced CoseAlgorithmIdentifier.Esp256/ArkgP256Esp256 docs to
  clarify usage distinction (Esp256 for signing, ArkgP256Esp256
  for key generation requests)
- Expanded IArkgPrimitives.Derive with algorithm details and
  cross-references to usage contexts
- Added class and method remarks to PreviewSignGeneratedKey and
  PreviewSignDerivedKey with workflow guidance
- Enhanced MakeCredentialParameters.AddPreviewSignGenerateKeyExtension
  and GetAssertionParameters.AddPreviewSignByCredentialExtension
  with usage flows and cross-references
- Documented ArkgPrimitives.Create factory method

All docs follow existing SDK patterns from CredBlob extension.
DocFX build clean (0 warnings, 0 errors).
Test suites green (Core: 488 PASS, YubiKey: 3587 PASS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the 3 Phase 1 NotImplementedException stubs in
PreviewSignTests.cs with real test bodies exercising the public
previewSign API surface landed in Phases 3-6:

- MakeCredentialWithPreviewSign_ReturnsGeneratedKey: registers a
  credential with previewSign and validates the returned
  PreviewSignGeneratedKey shape (KeyHandle non-empty,
  BlindingPublicKey + KemPublicKey are 65-byte SEC1 uncompressed
  P-256 points starting with 0x04).

- FullCeremony_RegisterDeriveSignVerify_RoundTrip: full Step A->B->C->D
  ceremony — register with previewSign, derive public key offline with
  deterministic ikm/ctx, sign a fixed message with the derived
  credential, verify the signature offline.

- MakeCredentialWithUnsupportedAlgorithm_Fails: registers with ES256
  instead of ArkgP256Esp256, asserts hardware rejects it. Pivoted from
  the original GetAssertion-with-unsupported-alg framing because the
  production AddPreviewSignGenerateKeyExtension does no client-side
  algorithm filtering, so device rejection is the meaningful test.

Tests use [SkippableFact(typeof(DeviceNotFoundException))] +
Skip.IfNot(IsExtensionSupported(Extensions.PreviewSign)) so they skip
cleanly without a YubiKey 5.8.0-beta+ present. Helpers
(GetMakeCredentialParams / GetGetAssertionParams / GetPinToken) follow
the HmacSecretTests integration pattern.

Reviewed via /DevTeam Ship — Engineer wrote, Reviewer approved
CLEAN PASS on logic correctness without execution. Tests can only
be run by a human with physical YK + user touch.

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

The previous implementation inherited SimpleIntegrationTestConnection,
which calls IntegrationTestDeviceEnumeration.GetTestDevice() EAGERLY
in its constructor. On macOS the YubiKeyDeviceListener populates its
device cache asynchronously, so FindByTransport(All) returned empty
before the cache was populated, throwing DeviceNotFoundException and
silently skipping every test even when a supported YubiKey was
plugged in and present in the allow-list.

Refactor to inherit FidoSessionIntegrationTestBase (the same base
class used by MakeCredentialBlobTests, HmacSecretTests, MinPinLenTests).
That base exposes Device / Session / Connection as LAZY properties
accessed only when the test method runs, by which time the device
listener has populated its cache.

Side benefits of the rebase:
- No private constructor needed; base handles PIN setup via TestPin1
- Removes 3 helper methods; base provides MakeCredentialParameters /
  GetAssertionParameters templates and a TestKeyCollector
- Higher-level Session.MakeCredential / Session.GetAssertions API
  instead of Connection.SendCommand(...)
- File moved from Fido2/Commands/ to Fido2/ to match the location
  convention of other FidoSessionIntegrationTestBase-using tests

Hardware behavior unchanged. Skip semantics preserved: still uses
[SkippableFact(typeof(DeviceNotFoundException))] +
Skip.IfNot(Session.AuthenticatorInfo.IsExtensionSupported(...))
on every test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DennisDyallo and others added 12 commits May 13, 2026 16:31
… duplication

- Create AttestationObject class (~/250 lines) implementing ICborEncode
  with dual constructors and parseAttestationStatement flag for format-agnostic parsing
- Refactor MakeCredentialData constructor to use AttestationObject, eliminating
  ~60 lines of duplicate CBOR parsing code and ReadAttestation() method
- Refactor PreviewSignExtension.DecodeGeneratedKey() to use AttestationObject,
  eliminating duplicate parsing logic and ParseInnerAttestationObject() method
- Fix CBOR reader state contract violation in ExtractRawAuthDataFromAttestationObject()
  by ensuring ReadEndMap() is always called after ReadStartMap()
- Add encoding validation in AttestationObject.CborEncode() to fail-fast
  when EncodedAttestationStatement is missing

Benefits:
- Eliminates ~80 lines of duplicate code across three files
- Provides reusable typed AttestationObject for future extensions
- Maintains 100% backward compatibility with existing MakeCredentialData API
- Fixes CBOR reader state management issues in PreviewSign extension
- Adds defensive validation preventing invalid CBOR generation

All 3,577 unit tests pass. Build succeeds with 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Enhance empty constructor documentation with clear warning that
  properties must be set before calling CborEncode()
- Strengthen CborEncode() validation to check Format, AuthenticatorData,
  and EncodedAttestationStatement (was only checking EncodedAttestationStatement)
- Improve error message to guide developers toward the decoding constructor
  or programmatic setup
- Clarify in remarks that EncodedAttestationStatement is always populated
  even when parseAttestationStatement=false

These changes prevent runtime InvalidOperationException by validating
preconditions early and guiding developers away from common misuse patterns.

All 3,577 unit tests pass. Build succeeds with 0 warnings, 0 errors.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add AttestationObjectTests.cs with 7 unit tests covering:
  - Packed format parsing: Format, AuthenticatorData, Algorithm, Signature
  - Structure-only parsing (parseAttestationStatement=false): parsed fields are null
  - SDK guarantee: EncodedAttestationStatement always populated regardless of flag
  - Guard: CborEncode() throws InvalidOperationException on uninitialized instance
  - Round-trip: bytesRead == input length

- Mark empty constructor [Obsolete] + [EditorBrowsable(Never)] since:
  - Zero production callers exist (confirmed by grep)
  - ICborEncode does not require parameterless constructor
  - AttestationObject is decode-only (authenticator generates it, SDK only reads it)
  - Hides from IntelliSense and emits compiler warning on new call sites
  - Preserves existing CborEncode() InvalidOperationException guard

Test count: 3577 → 3584 (+7). Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ytesRead

The out int bytesRead parameter exists for sequential CBOR buffer parsing
(e.g. CoseKey.Create used in AuthenticatorData). AttestationObject is never
parsed mid-stream — it always occupies the full response payload — so forcing
callers to declare and discard bytesRead with out _ is pure noise.

Add: AttestationObject(ReadOnlyMemory<byte>, bool parseAttestationStatement = true)
     chains to the full out int overload internally
Keep: full out int bytesRead overload as escape hatch for future sequential use
Migrate: MakeCredentialData now uses the clean overload
Update: AttestationObjectTests migrated; bytesRead-specific test preserved

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n docs

- CoseKey.Create: collapse EC2 algorithm cases into OR pattern match;
  add ArkgP256Esp256 to EC2 group — ARKG keys share the same EC2 COSE
  structure (kty=2, x/y coordinates) as Esp256/ES256

- AttestationObject: replace CborMap<int> with raw CborReader in constructor
  so empty attStmt maps ("none" format) and non-"packed" formats parse
  without CborMap's eager key-type detection throwing; only create
  CborMap<string> for "packed" attStmt parsing

- AttestationObject: unknown formats now handled gracefully — typed
  properties (AttestationAlgorithm, AttestationStatement, Certificates)
  stay null; EncodedAttestationStatement always populated

- Remove stale "no longer needed / backward compat" doc language — this
  code is not shipped, there are no breaking changes

- Add AttestationObjectTests.Parse_UnknownFormat test verifying "none"
  format parses cleanly with null typed fields and populated raw bytes

- Update CborEncode round-trip test to verify well-formed instance encodes

All 3585 unit tests pass. Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…dings

Teach AuthenticatorData to optionally skip CoseKey parsing via
parseCredentialPublicKey flag, allowing AttestationObject to handle
ARKG composite keys that aren't standard COSE keys. This eliminates
~70 lines of duplicate CBOR/binary parsing workarounds in
PreviewSignExtension.

Changes:
- AuthenticatorData: add EncodedCredentialPublicKey property and
  parseCredentialPublicKey parameter (default true, backward compatible)
- AttestationObject: rename parseAttestationStatement to parseFullDetails,
  fix bytesRead to use actual consumed bytes via CborReader.BytesRemaining
- MakeCredentialData: expose typed AttestationObject as public property
- PreviewSignExtension: use AttestationObject instead of raw byte parsing,
  delete ExtractRawAuthDataFromAttestationObject and ParseAuthDataForArkgSeed
- PreviewSignGeneratedKey: change AttestationObject from ReadOnlyMemory<byte>
  to typed AttestationObject
- Add 5 new unit tests for new code paths (3590 total, 0 warnings)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Fix documentation issues found during PR review:
- Replace stale parseAttestationStatement references with parseFullDetails
  in AttestationObject property docs and test comments
- Remove internal repo path references from ArkgP256 and ArkgP256Tests
- Fix PreviewSignDerivedKey doc to reference individual properties instead
  of "pass this object" (AddPreviewSignExtension takes separate params)
- Clarify Esp256 is not IANA-registered in CoseAlgorithmIdentifier
- Clarify VerifySignature hashes internally (pass raw data, not hash)
- Remove internal jargon ("Fix H1") from inline comment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop superfluous guard-clause tests and merge overlapping tests:
- AttestationObjectTests: 13→7 tests (merge full-parse and structure-only
  assertions, drop trivial not-null checks)
- ArkgPrimitivesTests: merge malformed-input tests into [Theory],
  drop null-guard and format-parsing tests
- PreviewSignExtensionTests: merge flags tests into [Theory],
  remove dead BuildAuthenticatorInfoWithPreviewSign method
- PreviewSignGeneratedKey: clean up init accessors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Buffer.BlockCopy with Span.CopyTo/Slice operations and
convert internal helper method parameters from byte[] to
ReadOnlySpan<byte>. P/Invoke boundaries keep byte[] where required
by DllImport. Interface signatures unchanged for compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Migrate IArkgPrimitives + ArkgPrimitivesOpenSsl inputs to ReadOnlySpan<byte>
- Add ReadOnlySpan<byte> overloads to DerivePublicKey and VerifySignature;
  byte[] versions delegate to Span overloads (no breaking change)
- Fix EcdsaVerify resource leak in VerifySignature (missing using)
- Add cross-impl citations (python-fido2, yubikit-swift, Rust, JS, IETF draft)
  on 6 load-bearing constant sites
- Swap Esp256 → ArkgP256Esp256 in 3 encoder unit test sites;
  update expected CBOR bytes accordingly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 6 comments.

Comment thread Yubico.YubiKey/src/Yubico/YubiKey/Fido2/MakeCredentialData.cs
Comment thread Yubico.YubiKey/src/Yubico/YubiKey/Fido2/MakeCredentialData.cs
Comment thread Yubico.YubiKey/src/Yubico/YubiKey/Fido2/PreviewSignExtension.cs Outdated
Comment thread Yubico.YubiKey/src/Yubico/YubiKey/Fido2/AttestationObject.cs Outdated
Comment thread Yubico.YubiKey/tests/integration/Yubico/YubiKey/Fido2/PreviewSignTests.cs Outdated
Comment thread Yubico.Core/src/Yubico/Core/Cryptography/ArkgPrimitivesOpenSsl.cs Outdated
Ship the previewSign wire/API surface while keeping relying-party ARKG derivation and signature verification out of the production YubiKey assembly.

PreviewSign remains available to downstream consumers through the request/response helpers and generated-key accessors. RP-side ARKG derivation and signature verification now live in test utilities and integration/unit tests, where they are needed for validation only.

Changes:

- Keep Core ARKG primitives internal

- Remove the production ArkgP256 wrapper from Yubico.YubiKey

- Remove CryptographyProviders.ArkgPrimitivesCreator

- Avoid production InternalsVisibleTo from Yubico.Core to Yubico.YubiKey

- Grant Core internals only to test and test-utility assemblies

- Update previewSign test helpers and ARKG KATs to call Core ARKG primitives from test assemblies
Reject malformed previewSign generated-key outputs before surfacing them as usable key material. The generated-key decoder now requires the outer ARKG-P256 algorithm marker and validates the nested ARKG COSE key shape, including EC2/P-256/ESP256 metadata for both public-key components.

Also fail fast on invalid caller input for previewSign requests: empty generation algorithm lists, unsupported option flags, and non-SHA-256-length to-be-signed digests.

Tests cover missing/unsupported generated-key algorithms, malformed ARKG COSE key metadata, invalid generation inputs, invalid flags, and bad sign digest length.
@DennisDyallo DennisDyallo requested a review from Copilot May 19, 2026 09:14
@DennisDyallo DennisDyallo added the enhancement New feature or request label May 19, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 25 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (2)

Yubico.YubiKey/src/Yubico/YubiKey/Fido2/MakeCredentialData.cs:217

  • The previous MakeCredentialData constructor validated that the attestation statement is well-formed (packed format, contains alg/sig, optional x5c, no extra keys) and threw Ctap2DataException via ReadAttestation when it wasn't. With this refactor, validation only happens inside AttestationObject.ParsePackedAttestationStatement, whose return value is discarded (_ = ParsePackedAttestationStatement(...) in AttestationObject.cs:216). A "packed" response with a missing sig, an unknown extra key, or too many entries will now silently produce a MakeCredentialData with AttestationAlgorithm == ES256 (the fallback) and AttestationStatement == Empty, instead of throwing. This is a behavior regression for malformed-input handling.
                // Backward compatibility: expose properties from AttestationObject
                Format = AttestationObject.Format;
                AuthenticatorData = AttestationObject.AuthenticatorData;
                AttestationAlgorithm = AttestationObject.AttestationAlgorithm ?? CoseAlgorithmIdentifier.ES256;
                AttestationStatement = AttestationObject.AttestationStatement ?? ReadOnlyMemory<byte>.Empty;
                EncodedAttestationStatement = AttestationObject.EncodedAttestationStatement;
                AttestationCertificates = AttestationObject.AttestationCertificates;

Yubico.YubiKey/src/Yubico/YubiKey/Fido2/PreviewSignExtension.cs:53

  • PreviewSignExtension.CtapUnsignedExtensionOutputsKey = 6 is declared here, but MakeCredentialData.cs declares its own private const int KeyUnsignedExtensionOutputs = 6; and uses that instead. Either consume the constant exported here from MakeCredentialData, or remove the unused constant.
        // Cross-ref: yubikit-swift, python-fido2, Rust, JS.
        /// <summary>CTAP key on the MakeCredential RESPONSE map carrying unsigned extension outputs.</summary>
        internal const int CtapUnsignedExtensionOutputsKey = 6;
        private const int CoseKeyTypeArkgP256 = -65537;
        private const int CoseAlgorithmArkgP256 = -65700;

Comment thread Yubico.YubiKey/src/Yubico/YubiKey/Fido2/MakeCredentialData.cs
Comment thread Yubico.YubiKey/src/Yubico/YubiKey/Fido2/PreviewSignExtension.cs Outdated
Comment on lines +282 to +300
if (alg != CoseAlgorithmIdentifier.ArkgP256Esp256)
{
return null;
}

var attestationObj = new AttestationObject(attestationObject, parseFullDetails: false);
if (attestationObj.AuthenticatorData.CredentialId is null ||
attestationObj.AuthenticatorData.EncodedCredentialPublicKey is null)
{
return null;
}

byte[] keyHandle = attestationObj.AuthenticatorData.CredentialId.Id.ToArray();

(byte[] pkBl, byte[] pkKem) = ParseArkgCoseKey(
attestationObj.AuthenticatorData.EncodedCredentialPublicKey.Value.ToArray());

return new PreviewSignGeneratedKey(keyHandle, pkBl, pkKem, alg, attestationObj);
}
Comment on lines +65 to +69
catch
{
// Swallow — if enumeration throws, downstream test skip path
// (DeviceNotFoundException) handles user feedback.
}
Addresses audit findings for AttestationObject and MakeCredentialData:

1. Add CborMap<TKey>.ReadEncodedValue() helper to preserve byte-identity
   when extracting CBOR values that must be re-emitted verbatim (e.g.,
   signed attestation statements). Eliminates double-parse in
   BuildAttestationObjectSubset.

2. Add required-keys guard in BuildAttestationObjectSubset to throw
   Ctap2DataException early if keys 1, 2, or 3 are missing from CTAP
   response, instead of later KeyNotFoundException.

3. Broaden AttestationObject constructor catch blocks to wrap both
   InvalidOperationException and FormatException (from CBOR type
   mismatches) as Ctap2DataException, maintaining documented exception
   contract.

4. Add comprehensive test coverage for all error paths:
   - CborMap.ReadEncodedValue byte-identity and missing-key cases
   - MakeCredentialData with missing required keys (fmt, authData, attStmt)
   - AttestationObject with malformed packed attestation (wrong alg type)

All 273 Fido2 unit tests pass. No regressions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@DennisDyallo DennisDyallo requested a review from Copilot May 19, 2026 18:12
Addresses Copilot review comments on PR #468:

- ArkgPrimitivesOpenSsl: relax ctx Guard from "size > 0" to
  "size <= 64", permitting empty context (ctxPrime[0] = 0).
- PreviewSignExtension: remove [Flags] attribute from
  PreviewSignOptions; UP (1) and UV (5) are mutually exclusive
  authentication MODES, not orthogonal bit flags.
- PreviewSignTests: replace new Random(42).NextBytes() with
  RandomNumberGenerator.Fill() for cryptographic IKM material.
- PreviewSignDerivedKey: fix DocFX cref to point at the extension
  method PreviewSignGeneratedKeyExtensions.DerivePublicKey(...)
  instead of a non-existent instance method.
- Tests: add Derive_EmptyContext_Succeeds and
  PreviewSignOptions_NotFlagsEnum_DoesNotSupportBitwiseOr
  anti-regression coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated 9 comments.

Comments suppressed due to low confidence (2)

Yubico.YubiKey/src/Yubico/YubiKey/Fido2/AuthenticatorData.cs:1

  • Previously, bytesRead came from CoseKey.Create and represented the exact byte length consumed by the COSE parser. Now it is derived from CborReader.ReadEncodedValue().Length, which is computed against the slice starting at offset. If EncodedAuthenticatorData[offset..] contains trailing extension bytes (ED flag set), ReadEncodedValue returns only the COSE key portion — which is the intended behavior — but please verify that coseKeyReader advancing through an indefinite-length map still produces a Length equal to the canonical bytes consumed (rather than the source slice length). A small unit test covering an authData with both AT and ED bits would harden this.
// Copyright 2025 Yubico AB

Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/Fido2/PreviewSignDerivedKey.cs:1

  • The XML doc says the method 'computes the SHA-256 hash internally before verifying', but the implementation delegates to EcdsaVerify.VerifyData(..., isStandardSignature: true). Please confirm that VerifyData indeed hashes the input message (not pre-hashed) — if it expects a pre-hashed digest, callers following the docstring will get a false negative. If hashing is indeed internal to VerifyData, this is fine; otherwise the docstring or the call needs to be corrected.
// Copyright 2025 Yubico AB

Comment on lines +236 to +237
var unsignedMap = map.ReadMap<string>(KeyUnsignedExtensionOutputs);
UnsignedExtensionOutputs = PreviewSignExtension.ParseUnsignedExtensionOutputs(unsignedMap.Encoded);
Comment on lines +65 to +69
/// Do NOT pass this value as the <c>alg</c> field of the previewSign
/// COSE_Sign_Args map; that field requests an ARKG-derived signing
/// operation and must be <see cref="ArkgP256Esp256"/> (-65539).
/// </remarks>
Esp256 = -9,
/// </summary>
public static PreviewSignGeneratedKey? DecodeGeneratedKey(ReadOnlyMemory<byte> previewSignValue) =>
DecodeGeneratedKey(previewSignValue, fallbackAlgorithm: null);

Comment on lines +65 to +69
catch
{
// Swallow — if enumeration throws, downstream test skip path
// (DeviceNotFoundException) handles user feedback.
}
Comment on lines +103 to +120
/// <param name="arkgKeyHandle">The ARKG key handle.</param>
/// <param name="context">The context string used during key derivation.</param>
/// <returns>CBOR-encoded COSE_Sign_Args map {3: alg, -1: arkg_kh, -2: ctx}.</returns>
/// <remarks>
/// The alg field identifies the SIGN-ARGS request as ARKG-derived (-65539), not the
/// raw signing algorithm. Rust hid-test, python-fido2, and the JS test page all pass
/// -65539 here; firmware rejects other values.
/// </remarks>
public static byte[] EncodeArkgSignArgs(
ReadOnlyMemory<byte> arkgKeyHandle,
ReadOnlyMemory<byte> context)
{
var cbor = new CborWriter(CborConformanceMode.Ctap2Canonical, convertIndefiniteLengthEncodings: true);
cbor.WriteStartMap(3);

// Cross-ref: python-fido2 cose.py:391 (-65539), Rust hid-test main.rs:28, JS test-page index.html:555
cbor.WriteInt32(3);
cbor.WriteInt32((int)Cose.CoseAlgorithmIdentifier.ArkgP256Esp256);
{
// Cross-ref: yubikit-swift, python-fido2, Rust, JS.
/// <summary>CTAP key on the MakeCredential RESPONSE map carrying unsigned extension outputs.</summary>
internal const int CtapUnsignedExtensionOutputsKey = 6;
Comment on lines +829 to +842
// Fixed P-256 generator — valid SEC1 point used as a placeholder
// for tests that only need a syntactically valid PublicKey.
return new byte[]
{
0x04,
0x6B, 0x17, 0xD1, 0xF2, 0xE1, 0x2C, 0x42, 0x47,
0xF8, 0xBC, 0xE6, 0xE5, 0x63, 0xA4, 0x40, 0xF2,
0x77, 0x03, 0x7D, 0x81, 0x2D, 0xEB, 0x33, 0xA0,
0xF4, 0xA1, 0x39, 0x45, 0xD8, 0x98, 0xC2, 0x96,
0x4F, 0xE3, 0x42, 0xE2, 0xFE, 0x1A, 0x7F, 0x9B,
0x8E, 0xE7, 0xEB, 0x4A, 0x7C, 0x0F, 0x9E, 0x16,
0x2B, 0xCE, 0x33, 0x57, 0x6B, 0x31, 0x5E, 0xCE,
0xCB, 0xB6, 0x40, 0x68, 0x37, 0xBF, 0x51, 0xF5,
};
Comment thread Yubico.YubiKey/src/Yubico/YubiKey/Fido2/MakeCredentialData.cs
Comment on lines +199 to +200
// bytesRead must reflect actual consumed bytes, not input length
bytesRead = cborEncoding.Length - reader.BytesRemaining;
- DecodeGeneratedKey now throws Ctap2DataException for unsupported alg
  instead of returning null, matching the exception-based failure mode
  of ParseArkgCoseKey.
- Remove duplicate KeyUnsignedExtensionOutputs const in MakeCredentialData
  in favor of PreviewSignExtension.CtapUnsignedExtensionOutputsKey.
- Add XML doc to DecodeGeneratedKeyAlgorithm.
- Drop per-call .ToArray() in PreviewSignGeneratedKeyExtensions.DerivePublicKey
  by passing ReadOnlyMemory.Span directly to ArkgPrimitives.Derive.
@github-actions
Copy link
Copy Markdown
Contributor

Code Coverage

Package Line Rate Branch Rate Complexity Health
Yubico.Core 56% 49% 1642
Yubico.YubiKey 51% 47% 7350
Summary 52% (13592 / 26330) 47% (3345 / 7128) 8992

Minimum allowed line rate is 40%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Development

Successfully merging this pull request may close these issues.

2 participants