Skip to content

Very experimental, untested, here be dragons#722

Draft
dainnilsson wants to merge 261 commits intomainfrom
experiment/rust
Draft

Very experimental, untested, here be dragons#722
dainnilsson wants to merge 261 commits intomainfrom
experiment/rust

Conversation

@dainnilsson
Copy link
Copy Markdown
Member

No description provided.

dainnilsson and others added 30 commits March 21, 2026 02:19
Move OATH credential ID formatting/parsing, HMAC-SHA1, PBKDF2 key
derivation, TOTP/HOTP challenge generation, OTP code formatting,
and base32 key parsing from Python to Rust in yubikey-mgmt crate.

Python wrapper functions in yubikit/oath.py delegate to Rust via
PyO3 bindings, maintaining the existing API for callers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement core Rust infrastructure:
- SmartCardConnection trait with send_and_receive + transport
- SmartCardProtocol with command/response chaining, touch workaround,
  and SCP03 secure messaging integration
- Version, Aid, Sw types and SmartCardError enum
- PcscConnection now implements SmartCardConnection trait

Add complete OathSession in Rust (yubikey-mgmt crate):
- Session lifecycle: new, reset, validate, set_key, unset_key
- Credential operations: put, list, rename, delete
- Code calculation: calculate, calculate_all, calculate_code
- Credential, Code, CredentialData types

A pure Rust program can now enumerate PC/SC readers, open a
PcscConnection, and use OathSession to manage OATH credentials.

Python wrappers unchanged — still using their own SmartCardProtocol.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement complete YubiOTP protocol in yubikey-mgmt crate:

Types: Slot, ConfigSlot, TktFlag/CfgFlag/ExtFlag, NdefType, ConfigState.

Slot configurations: YubiOTP, HMAC-SHA1, HOTP, static password,
static ticket, and update configs with builder pattern for flags.

OTP HID protocol: OtpProtocol with 70-byte frame send/receive over
HidConnection, sequence tracking, keepalive, and cancellation.

Two session types:
- YubiOtpSession<C: SmartCardConnection> for SmartCard backend
- YubiOtpOtpSession for HID OTP backend

Both support: get_serial, get_config_state, put/update/delete
configuration, swap_slots, set_scan_map, set_ndef_configuration,
and calculate_hmac_sha1.

26 Rust unit tests covering config building, CRC, NDEF, flags.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement ManagementSession in yubikey-mgmt crate:

Types: Capability (bitflags), FormFactor, DeviceFlag, ReleaseType,
UsbInterface, Mode, VersionQualifier.

Data structures: DeviceConfig with TLV serialization, DeviceInfo
with full parse() handling edge cases (v4.2.4 workaround, YK4
USB_ENABLED skip, FIPS/SKY flags, version qualifier override).

ManagementSession<C: SmartCardConnection> with read_device_info
(paginated), write_device_config, set_mode, device_reset, and
NEO v3 fallback logic.

20 Rust unit tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement HsmAuthSession in yubikey-mgmt crate with EC P-256 support:

Types: Algorithm, Credential, SessionKeys, HsmAuthError.

Full session: put/delete/list credentials, symmetric and asymmetric
key operations, PBKDF2 credential password derivation, session key
calculation, management key operations, PIN retry handling.

EC P-256 via p256 crate for asymmetric credential key pairs with
SEC1 uncompressed point encoding.

10 Rust unit tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement SecurityDomainSession in yubikey-mgmt crate:

Types: KeyType, Curve (with OIDs), ScpKid, KeyRef, StaticKeys.

Full session: get_data, get_key_information, get_card_recognition_data,
get_supported_ca_identifiers, get_certificate_bundle, reset (with
SCP03 default key restore + SCP11b generation), store_data,
store_certificate_bundle, store_allowlist, store_ca_issuer,
delete_key, generate_ec_key, put_key (static/EC private/public).

Certificates handled as raw DER bytes for flexibility.

17 Rust unit tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Complete PivSession<C: SmartCardConnection> implementation with:
- All enums: KeyType, ManagementKeyType, Slot, ObjectId, PinPolicy, TouchPolicy
- Management key authentication (TDES/AES ECB)
- PIN/PUK management with retry tracking
- Crypto operations: sign, decrypt, calculate_secret
- Key management: generate, import, attest, move, delete
- Certificate management with gzip compression support
- Object data storage
- Version-based feature support checking
- Metadata queries (pin, puk, management key, slot, bio)
- 18 unit tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Complete OpenPgpSession<C: SmartCardConnection> implementation with:
- All enums: KeyRef, Uif, PinPolicy, Pw, Do, HashAlgorithm, KeyStatus
- Algorithm attributes: RsaAttributes, EcAttributes with OID support
- Full Data Object (DO) system for read/write operations
- PIN management: verify, change, reset, unverify, set_pin_attempts
- KDF support: KdfNone and KdfIterSaltedS2k (RFC 4880 S2K)
- Key generation (RSA/EC), import with PrivateKeyTemplate, deletion
- Crypto operations: sign, decrypt, authenticate with PKCS#1v1.5 padding
- Certificate management: get, put, delete, attest
- UIF (touch) configuration
- Algorithm information queries
- Factory reset
- ApplicationRelatedData parsing with full discretionary data objects
- 31 unit tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create Python-accessible wrappers for all Rust session types via
_ykman_native.sessions module:
- OathSession, PivSession, OpenPgpSession
- HsmAuthSession, ManagementSession, SecurityDomainSession

Uses PySmartCardConnection bridge that implements Rust's
SmartCardConnection trait by calling back to Python's
send_and_receive method. This allows Rust sessions to work
with any Python connection type transparently.

All sessions tested working against physical YubiKey via
ScardSmartCardConnection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- OathSession now delegates to Rust via PyO3 for non-SCP sessions
- Falls back to Python SmartCardProtocol for SCP-encrypted sessions
- Fix smartcard_err() to raise proper Python ApduError, NotSupportedError,
  and BadResponseError instead of generic RuntimeError
- Add set_version() to Rust OathSession for development device version override
- Apply _override_version.patch() for dev devices reporting version 0.0.1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- HsmAuthSession now delegates to Rust via PyO3 for non-SCP sessions
- Falls back to Python SmartCardProtocol for SCP-encrypted sessions
- Add set_version() to Rust HsmAuthSession for dev device version override
- Fix hsmauth_err() to raise InvalidPinError from yubikit.core
- Convert EC key types at Python/Rust boundary (SEC1 bytes <-> cryptography objects)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- SmartCard+no-SCP path delegates to Rust via PyO3
- OTP and CTAP backends remain Python-only
- Add set_version() and read_device_info_unchecked() to Rust ManagementSession
- Fix parse_version_string() to handle legacy firmware strings
- Add _device_info_from_native() helper for DeviceInfo conversion
- Update diagnostics to handle native session

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add set_version() to Rust PivSession for dev device version override
- Fix piv_err() to properly map InvalidPin and NotSupported errors
- Delegate all 31 public methods with proper type conversions
- Handle DER bytes <-> cryptography objects for certs and keys
- Gracefully fall back to Python path for unknown object IDs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add set_version() to Rust OpenPgpSession for dev device version override
- Fix openpgp_err() to properly map InvalidPin and NotSupported errors
- Delegate all 30+ public methods with proper type conversions
- Handle crypto objects (keys, certs) via DER bytes at Python/Rust boundary
- Helper functions for hash algorithm and private key conversion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add version field and set_version() to Rust SecurityDomainSession
- Add version getter/setter to PyO3 wrapper
- Fix get_key_information() TLV parsing bug (was re-parsing already-unpacked values)
- Delegate all public methods except put_key (requires SCP processor)
- Handle KeyRef as (kid, kvn) tuples, certs as DER bytes at boundary

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create PyO3 wrapper for both SmartCard and OTP HID backends
- Add yubiotp_err() for proper error mapping
- Add set_version() to both Rust session types
- Make write_config() and send_and_receive() public for PyO3 access
- Delegate SmartCard path to Rust, keep OTP HID as Python fallback
- Register YubiOtpSession classes in sessions submodule

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- New device module with list_devices(), read_info(), get_name()
- YubiKeyDevice type with open_smartcard()/open_otp() methods
- Full product name logic ported from Python (NEO, YK4, YK5, Bio, FIPS, etc.)
- Fix read_config() to use modern path for dev devices (version 0.0.1)
- Three examples: list_devices, device_info, oath_list
- 15 unit tests for product name logic

A pure Rust program can now:
  list_devices() -> open_smartcard() -> OathSession::new() -> list_credentials()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add apply_device_info_fixups() for FIPS detection and NFC workarounds
- Fix read_config() to use v3-only check (NEO) instead of < 4.0.0
- Add PyO3 device submodule with read_info() and get_name() functions
- Make device_info_to_dict() public for cross-module reuse

Python can now call _ykman_native.device.read_info(reader_name) to get
DeviceInfo directly from Rust, bypassing the Python Management session.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The version_qualifier (release type, iteration) was not included in the
device_info_to_dict() output, causing Python to always create a FINAL
qualifier. This prevented _override_version from being applied for
non-final (alpha/beta) firmware, making version-gated features fail.

- Add version_qualifier dict to device_info_to_dict() in PyO3
- Parse version_qualifier in _device_info_from_native() in Python
- Fixes 10 test failures in OATH and HsmAuth device tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rust DeviceInfo parsing converted auto_eject_timeout=0 to None, while
Python always returns it as an int (0). This caused different _key()
fingerprints in _PidGroup, preventing the OTP HID interface from being
matched to the same physical device resolved via SmartCard.

Fix: always return Some(value) for auto_eject_timeout and
challenge_response_timeout, matching Python's behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- PIV: re-query management key metadata after version override in set_version()
- PIV: map BadResponseError properly in piv_err() instead of generic RuntimeError
- TLV: validate parsed length doesn't exceed data bounds (prevents panics on invalid data)
- SecurityDomain: return full DER bytes (tag+length+value) from get_certificate_bundle()
- OpenPGP: fix generate_ec_key to pass dotted OID string instead of enum name
- OpenPGP: fix put_key to set algorithm attributes before import (matching Python path)
- OpenPGP: fix RSA import to use standard format for YK5+ (not CRT)
- OpenPGP: fix X25519 private key byte order reversal in native prep
- OpenPGP: fix get_challenge to pass Le (expected response length) in APDU
- SmartCardProtocol: add send_apdu_with_le() for APDUs needing explicit Le

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The device fingerprint key uses str() on the supported_capabilities dict,
which is order-dependent. Rust's HashMap iteration order is non-deterministic,
so the OTP HID and SmartCard paths could produce different dict orderings,
causing device resolution to fail with 'Failed to connect to the device'.

Fix by sorting transport entries (USB before NFC) before building Python dicts,
and deriving Ord on Transport enum for clean sorting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When rapidly switching USB interfaces, the previously-resolved device
handle may fail to open because the USB interface isn't ready yet.
Previously this propagated as an uncaught exception. Now we catch the
failure, move the device back to unresolved, and let the normal
resolution logic (with retries) handle it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename the main Rust crate to yubikit-rs to better reflect its purpose
as a Rust implementation of the YubiKit library. Updates all Cargo.toml
references, workspace members, and use statements across both the core
crate and the PyO3 wrapper crate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create core_types.rs with Version and Transport (moved from iso7816.rs)
- Create otp_protocol.rs with OtpProtocol, YubiOtpError, and HID framing
  helpers (moved from yubiotp.rs)
- Re-export moved types from original modules for backwards compatibility
- iso7816.rs now focuses on APDU/SmartCard protocol types
- yubiotp.rs now focuses on YubiOTP session logic

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add ManagementOtpSession struct that wraps OtpProtocol for management
operations (read_config, write_config, read_device_info, set_mode) over
HID OTP connections. Extract shared config parsing into helper functions
used by both SmartCard and OTP management sessions.

The Rust ManagementOtpSession is exposed via PyO3 for direct use, while
the Python ManagementSession continues using the Python OtpProtocol
backend to avoid connection ownership issues.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add init_scp03() and init_scp11() to SmartCardProtocol in Rust
- SCP03: INITIALIZE UPDATE + EXTERNAL AUTHENTICATE handshake
- SCP11: ECDH key agreement (P-256), X9.63 KDF, receipt verification
- Support SCP11a, SCP11b, and SCP11c variants
- Fix empty data encryption bug (counter desync)
- Fix KCV computation (was using wrong IV)
- Fix SCP11c params byte (0x03 not 0x11)
- Fix key_agreement_data to include card ephemeral key TLV
- Fix store_allowlist to handle arbitrary-length serial numbers
- Update all 7 PyO3 session constructors to accept SCP params
- Python sessions always use native Rust SCP (no fallback)
- Delete _init_python() methods and ScpProcessor fallback paths
- All 401 device tests pass (375 + 11 SD + 15 SCP APDU)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Integration tests (crates/yubikit-rs/tests/device_tests.rs):
- 18 tests covering all session types: Management, OATH, PIV, OpenPGP,
  YubiOTP, HSM Auth, and Security Domain
- Requires YUBIKEY_SERIAL env var to prevent accidental runs
- Skips tests based on device capabilities
- Tests include: session open, version read, CRUD operations (OATH),
  PIN verification (PIV), application data read (OpenPGP), key info (SD)

CLI example (crates/yubikit-rs/examples/ykinfo.rs):
- Lists connected YubiKeys with device info
- --serial filter to target a specific device
- --all flag to query each application session
- Demonstrates the full yubikit-rs API: device enumeration, connection
  opening, and session creation for all supported application types

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove try/except ImportError guards from all session modules
- Remove all 'if self._native:' conditional checks
- Delete dead Python fallback implementations (APDU-level code)
- Clean up unused imports and helper functions
- Preserve legitimate OTP/FIDO backends in management and yubiotp
- Re-export InvalidPinError from piv.py and hsmauth.py for CLI usage
- Update test fixtures to use ccid_connection for disconnect/reconnect
- All 401 device tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Global version override in Rust (set_override_version/patch_version)
  with PyO3 bridge so Python and Rust share the same override
- Each Rust session applies patch_version in init
- NFC reader support: open_reader(), list_readers re-export, --reader
  and --list-readers flags in ykinfo example
- ManagementSession and YubiOtpSession now use native Rust for OTP
  connections (ManagementOtpSession, YubiOtpOtpSession)
- Deleted Python OtpProtocol-based backends

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dainnilsson and others added 9 commits April 12, 2026 11:48
Restructure the monolithic Python project into a uv workspace:

- packages/yubikit/: Contains the yubikit Python package with the
  native Rust extension (_yubikit_native) built via maturin. This is
  the core library providing YubiKey protocol implementations.

- packages/yubikey-manager/: Contains the ykman Python package built
  with hatchling. Depends on yubikit. 'pip install yubikey-manager'
  pulls in yubikit automatically, preserving the same end-user
  experience.

Root pyproject.toml becomes the workspace root with shared dev
dependencies, test config, and linting settings.

Update CI workflows:
- wheels.yml: Build yubikit platform wheels via maturin-action with
  --manifest-path. Build yubikey-manager pure-Python wheel via uv.
- source-package.yml: Build sdists for both packages.

Other updates:
- Remove MANIFEST.in (no longer needed with per-package build systems)
- Update docs/conf.py paths for new package locations
- Update .pre-commit-config.yaml ty-check paths
- Fix import ordering (ruff) after package relocation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Version bumps:
- yubikit, yubikey-manager: 5.9.1-dev.0 → 6.0.0-dev.0
- ykman-cli crate: 0.1.0 → 6.0.0-dev.0

Rename packages/yubikey-manager/ → packages/ykman/ (project name
remains 'yubikey-manager' in pyproject.toml).

Move device enumeration and connection classes to yubikit.device:
- NativeFidoConnection (was ykman.hid.fido)
- _NativeOtpConnection (was ykman.hid.otp)
- ScardSmartCardConnection, list_readers (was ykman.pcsc)
- YkmanDevice, REINSERT_STATUS, CancelledException (was ykman.base)
- scan_devices, list_all_devices (was ykman.device)

ykman.device and ykman.base become thin re-export shims for backward
compatibility.

Delete ykman.diagnostics (Rust CLI has its own diagnostics).
Delete ykman.hid and ykman.pcsc (merged into yubikit.device).

InvalidPinError no longer inherits ValueError (v6 breaking change
that was documented and had a test waiting for it).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move pid property, reinsert (abstract), REINSERT_STATUS, and
CancelledException from yubikit.device into yubikit.core.YubiKeyDevice
so all device implementations share the same base interface.

Remove the intermediate YkmanDevice class. ykman.base and ykman.device
provide YkmanDevice as a type alias to YubiKeyDevice for backward
compatibility.

Rename internal classes to private:
- NativeFidoConnection → _NativeFidoConnection
- ScardSmartCardConnection → _NativeSmartCardConnection
- _NativeCompositeDevice → _NativeYubiKeyDevice

Drop public exports of connection classes from ykman.device.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…vice

YubiKeyDevice no longer has an __init__. Subclasses must implement
transport, pid, and fingerprint as concrete properties.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch yubikey-manager from hatchling to maturin with bindings='bin'
so pip install yubikey-manager installs the native ykman CLI binary
directly into the scripts directory. This follows the same pattern
used by uv (pip install uv).

The wheel includes both:
- The ykman Python library package (via python-packages)
- The native ykman binary (via maturin bin bindings)

Add ykman/_find_ykman.py to locate the installed binary, and
ykman/__main__.py for 'python -m ykman' support.

Update CI workflows:
- wheels.yml: Add ykman-wheels job (platform-specific maturin builds)
- source-package.yml: Build sdist via maturin for both packages

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove SmartCardCtapDevice (FIDO-over-NFC via CCID) — this was a
Python reimplementation of NFC CTAP framing that is now handled
natively in Rust.

Remove the fido2 package dependency from yubikit. The only fido2
imports were for SmartCardCtapDevice (CtapError, STATUS, CAPABILITY,
CTAPHID constants).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
maturin-action's --manifest-path expects a Cargo.toml, not a
pyproject.toml. Use working-directory to let maturin discover the
pyproject.toml in each package directory.

Fix autoapi_dirs in docs/conf.py to point to the new package paths
under packages/.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add dbus-devel to the manylinux container packages for ykman-wheels
(required by libdbus-sys, a dependency of the keyring crate).

Fix ruff lint error in examples/piv_certificate.py (extra blank line).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use SetupDiGetClassDevs to enumerate HID devices on Windows instead of
hidapi's list_all_hid_devices(). This works for FIDO devices even when
not running as Administrator, since it doesn't open device handles.

Changes:
- Add setupdi.rs transport module using Windows SetupDi/HID APIs to
  enumerate Yubico HID devices by parsing VID/PID from device paths
- Replace list_all_hid_devices() fallback in scan_usb_devices() with
  list_setupdi_devices()
- Add usb_interfaces_from_pid() and name_from_pid() public helpers
- Update CLI list command to show '<access denied>' for devices found
  by scan but not accessible via list_devices()
- Update select_device() to give clear admin requirement error when
  only blocked devices are found

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dainnilsson and others added 18 commits April 12, 2026 23:20
- Add make_credential(), get_assertion(), get_next_assertion() to Ctap2Session
- Add Info::get_identifier() and Info::get_cred_store_state() using HKDF+AES decryption
- Add static is_supported() methods to all CTAP2 command classes
- Add CredentialManagement::is_readonly_supported() static method
- Make ClientPin::is_supported() and is_token_supported() static (take &Info)
- Use Zeroizing<Vec<u8>> internally for PIN tokens in CTAP2 structs (not in public API)
- Zeroize PIN hashes and KDF intermediates after use in ClientPin and PinProtocol
- Add build_args_map() helper for positional CBOR argument encoding
- Refactor client_pin response parsing to use shared parse_int_map()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename to_value/from_value/from_int_map/from_template_info to
  to_cbor/from_cbor across all CTAP2 types
- Make to_cbor/from_cbor pub(crate) for internal serialization only
- Make encode_allow_exclude_list/encode_pub_key_cred_params pub(crate)
- Change send_cbor to parse CBOR and return Value instead of raw bytes
- Remove parse_int_map from Ctap2Session
- Update client_pin() to return Value, callers use map_get_int()
- Update credential_management/bio_enrollment call() to return Value
- Remove BTreeMap<u32, Value> from all internal CBOR parsing
- PyO3 send_cbor re-encodes Value to bytes for Python API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename cmd module to ctap2_cmd in mod.rs
- Rename result_key to bio_result_key in bio_enrollment.rs
- Rename result_key to cred_mgmt_result_key in credential_management.rs
- Convert ClientPinResult enum to client_pin_result_key constants module
- Move build_args_map from session.rs to mod.rs as pub(crate)
- Use build_args_map in credential_management, bio_enrollment, and
  config call() methods

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merge yubikit-wheels and ykman-wheels into a single parameterized
wheels job using package × platform matrix.

Combine source-package build and docs into a single job to eliminate
duplicated setup steps (uv, Python, apt deps, Rust, uv sync).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce webauthn/types.rs with WebAuthn-level types:
- PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions
- Entity types (RpEntity, UserEntity), credential descriptors
- Enums for attestation, user verification, resident key requirements
- CollectedClientData with JSON serialization and hashing
- Response types (RegistrationResponse, AuthenticationResponse)
- Conversion methods (to_ctap2) bridging WebAuthn to CTAP2 types

Introduce webauthn/client.rs with WebAuthnClient:
- Generic over Connection, UserInteraction, and ClientDataCollector traits
- UserInteraction trait for prompt_up, request_pin, request_uv callbacks
- ClientDataCollector trait for collecting client data per ceremony
- make_credential() implementing the registration ceremony
- get_assertion() implementing the authentication ceremony
- PIN/UV token acquisition with UV-first, PIN-fallback strategy
- Session ownership transfer pattern for ClientPin interaction

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Demonstrates the full WebAuthn flow using a FIDO2 security key:
- Console-based UserInteraction with PIN prompt on stdin
- Simple ClientDataCollector for example.com
- Registration ceremony (make_credential) creating an ES256 credential
- Authentication ceremony (get_assertion) using the new credential

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Unify PublicKeyCredentialUserEntity, PublicKeyCredentialParameters, and
PublicKeyCredentialDescriptor — remove duplicates from ctap2/types.rs
and use the webauthn versions everywhere. The webauthn types now carry
both CBOR (to_cbor/from_cbor) and JSON (serde) serialization. The
ctap2 module re-exports them for backward compatibility.

PublicKeyCredentialRpEntity remains separate in both modules because
the field requirements differ: CTAP2 requires id, WebAuthn requires
name.

Remove new() constructors from all WebAuthn structs — use direct
struct initialization instead.

Add serde/serde_json for WebAuthn JSON serialization only (CBOR keeps
the existing manual implementation which suits CTAP2's integer keys
and canonical ordering).

New public API:
- PublicKeyCredentialCreationOptions::from_json()
- PublicKeyCredentialRequestOptions::from_json()
- RegistrationResponse::to_json()
- AuthenticationResponse::to_json()

Extensions field changed from HashMap<String, Vec<u8>> to
serde_json::Value to support arbitrary JSON extension data.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add py_webauthn.rs with WebAuthnClient (FIDO HID) and WebAuthnCcidClient
  (SmartCard) Python classes
- Python callbacks bridge UserInteraction trait (prompt_up, request_pin,
  request_uv)
- Built-in ClientDataCollector uses origin parameter
- Options/responses use JSON serialization (from_json/to_json)
- Register classes in _yubikit_native.sessions module
- Add type stubs for new classes
- Add examples/webauthn.py demonstrating registration + authentication

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add yubikit/webauthn.py with WebAuthnClient facade abstracting HID/CCID
- Define UserInteraction ABC with prompt_up(), request_pin(permissions, rp_id),
  request_uv(permissions, rp_id) matching the Rust traits
- Define ClientDataCollector ABC with collect_create(options_json) and
  collect_get(options_json), returning (client_data_json: bytes, rp_id: str)
- Add CollectedClientData::from_json() to parse raw JSON bytes from Python
- Add to_json() for Options structs (needed to pass options to Python)
- Update pyo3 wrappers to accept UserInteraction/ClientDataCollector objects
- Update examples to use yubikit facade and ykman device discovery

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tion

Implement filter_creds() which uses silent getAssertion calls (UP=false)
to pre-filter exclude/allow credential lists before the actual ceremony,
matching python-fido2's _filter_creds behavior.

Changes:
- Refactor get_auth_params into get_token (returns token, protocol, internal_uv)
- Add compute_pin_uv_param helper for reuse across ceremonies and filtering
- Add filter_creds method that chunks credential lists by max_creds_in_list,
  handles NoCredentials (skip chunk) and RequestTooLarge (reduce chunk size)
- make_credential: request GET_ASSERTION permission when exclude list present,
  filter exclude list, pass only matching credential
- get_assertion: filter allow list, send dummy cred ID if no matches to force
  UP-then-fail behavior
- Rename CtapStatus::PukRequired to RequestTooLarge (correct CTAP2 code 0x39)
- Reorder CtapStatus enum variants for sorted-by-value consistency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…, credProps, minPinLength)

Implement WebAuthn extensions as stateless data types in webauthn/extensions/,
with CTAP2 CBOR building/parsing inlined. Extensions are integrated into
WebAuthnClient's make_credential and get_assertion ceremonies.

Extensions:
- PRF: wraps hmac-secret with salt hashing and ECDH key agreement
- credProtect: credential protection level (3 policies)
- credBlob: small credential-associated data
- largeBlob: large blob read/write via LargeBlobs API
- credProps: client-side credential properties (rk)
- minPinLength: minimum PIN length enforcement

Key changes:
- Options.extensions typed as RegistrationExtensionInputs/AuthenticationExtensionInputs
- Response structs gain client_extension_results field
- Auth data extension parsing from raw authenticator data bytes
- All extension types have serde JSON serialization
- 24 new unit tests for extension CBOR/JSON round-trips

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- webauthn_prf: PRF/hmac-secret with salt evaluation and determinism check
- webauthn_cred_protect: all three credential protection levels
- webauthn_cred_blob: store and retrieve small credential-associated data
- webauthn_large_blob: write and read large blobs via LargeBlobs API
- webauthn_cred_props: credProps (rk detection) and minPinLength

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ports of the Rust extension examples, using the JSON-based Python API:
- webauthn_prf.py: PRF/hmac-secret with determinism verification
- webauthn_cred_protect.py: all three credential protection levels
- webauthn_cred_blob.py: store and retrieve small credential data
- webauthn_large_blob.py: write and read large blobs
- webauthn_cred_props.py: credProps (rk detection) and minPinLength

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the extensions request a largeBlob write, the PIN/UV token must
include the LARGE_BLOB_WRITE (0x10) permission bit. Without this the
authenticator rejects the write with PIN_AUTH_INVALID.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The process_large_blob method previously required both protocol and
large_blob_key to be present. Read operations don't use a PIN token,
so when userVerification is 'discouraged' and no extra permissions
are needed, protocol would be None and the method would bail out.

Fix by making protocol optional — use V2 as a default since reads
don't exercise the token path. Also add tests for largeBlob JSON
deserialization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move duplicated boilerplate (ConsoleInteraction, SimpleCollector,
constants, device setup, helpers) into shared modules:
- crates/yubikit/examples/example_utils.rs (Rust)
- examples/example_utils.py (Python)

All 12 examples now import from their respective shared module,
reducing each by ~70 lines of duplicated code.

Set autoexamples=false in Cargo.toml to prevent example_utils.rs
from being compiled as a standalone example binary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The public info() accessor only served the examples. Move the Info
retrieval into open_client() which now returns (WebAuthnClient, Info).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ctap2Session::new(), ClientPin::new(), CredentialManagement::new(),
BioEnrollment::new(), Config::new(), Config::new_unauthenticated(),
and LargeBlobs::new() now return the owned session alongside the error
on failure, preventing session loss. ClientPin::new_with_protocol() is
now infallible and returns Self directly.

PyO3 wrappers restore the session back to the Python object on
constructor failure, preventing session loss in Python code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

2 participants