feat(fido2): WebAuthn previewSign extension#468
Conversation
1cf9541 to
9960876
Compare
Test Results: Windows 2 files 2 suites 40s ⏱️ Results for commit be57c2c. ♻️ This comment has been updated with latest results. |
Test Results: Ubuntu 2 files 2 suites 1m 8s ⏱️ Results for commit 216c956. ♻️ This comment has been updated with latest results. |
Test Results: MacOS 4 files 4 suites 54s ⏱️ Results for commit 216c956. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
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
previewSignextension 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.
3a99633 to
9c68ea2
Compare
1d36eaf to
dd829ec
Compare
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>
… 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>
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.
There was a problem hiding this comment.
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
MakeCredentialDataconstructor validated that the attestation statement is well-formed (packed format, contains alg/sig, optional x5c, no extra keys) and threwCtap2DataExceptionviaReadAttestationwhen it wasn't. With this refactor, validation only happens insideAttestationObject.ParsePackedAttestationStatement, whose return value is discarded (_ = ParsePackedAttestationStatement(...)in AttestationObject.cs:216). A "packed" response with a missingsig, an unknown extra key, or too many entries will now silently produce aMakeCredentialDatawithAttestationAlgorithm == ES256(the fallback) andAttestationStatement == 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 = 6is declared here, butMakeCredentialData.csdeclares its ownprivate const int KeyUnsignedExtensionOutputs = 6;and uses that instead. Either consume the constant exported here fromMakeCredentialData, 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;
| 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); | ||
| } |
| 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>
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>
There was a problem hiding this comment.
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,
bytesReadcame fromCoseKey.Createand represented the exact byte length consumed by the COSE parser. Now it is derived fromCborReader.ReadEncodedValue().Length, which is computed against the slice starting atoffset. IfEncodedAuthenticatorData[offset..]contains trailing extension bytes (ED flag set),ReadEncodedValuereturns only the COSE key portion — which is the intended behavior — but please verify thatcoseKeyReaderadvancing through an indefinite-length map still produces aLengthequal 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 thatVerifyDataindeed 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 toVerifyData, this is fine; otherwise the docstring or the call needs to be corrected.
// Copyright 2025 Yubico AB
| var unsignedMap = map.ReadMap<string>(KeyUnsignedExtensionOutputs); | ||
| UnsignedExtensionOutputs = PreviewSignExtension.ParseUnsignedExtensionOutputs(unsignedMap.Encoded); |
| /// 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); | ||
|
|
| catch | ||
| { | ||
| // Swallow — if enumeration throws, downstream test skip path | ||
| // (DeviceNotFoundException) handles user feedback. | ||
| } |
| /// <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; |
| // 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, | ||
| }; |
| // 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.
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 againstpython-fido2, the Rustcnh-authenticator-rs-extensionreference, and the upstreamYubico/5-8-hello-worldquickstart.ARKG-P256 (RFC draft
bradleylundberg-cfrg-arkg-09) implemented inYubico.Core.Cryptography.ArkgPrimitivesOpenSslvia OpenSSL P/Invoke throughYubico.NativeShims. No BouncyCastle dependency.Stacking: depends on #472. Merge #472 first — this branch consumes its new
Native_EC_POINT_is_on_curveexport, theYubico.NativeShims 1.16.1-prerelease.20260428.1version bump, and the relocatedHkdfUtilities.Verification
Yubico.YubiKey.UnitTestsYubico.Core.UnitTestsArkgP256Tests+ArkgPrimitivesTests)PreviewSignTests(3 tests)/t:DocFXBuildwarnings-as-errorsCI note
submit-nugetwill fail untilYubico.NativeShims 1.16.1-prerelease.20260428.1is 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.