Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# CHANGELOG


**v0.1.6 - 09-11-2026**

- Pin goroutine to OS thread for macos
- SetOpenExclusive(false) for darwin
- hidList and pcscList fixes


**v0.1.5 - 09-10-2026**

- Migrated to hidapi (removed ykpers)
- Migrated to hidapi (removed ykpers)
- Added nitrokey 1st pass proper (needs testing)

Expand Down
30 changes: 28 additions & 2 deletions hardware/chalresp/chalresp_hid.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package chalresp
import (
"errors"
"fmt"
"runtime"
"sync"
"time"

Expand Down Expand Up @@ -82,7 +83,15 @@ func hidEnsureInit() error {
hidInitOnce.Do(func() {
if err := hid.Init(); err != nil {
hidInitErr = fmt.Errorf("chalresp: hidapi init failed: %w", err)
return
}
// macOS: open the OTP interface NON-exclusively. hidapi's darwin backend
// defaults to kIOHIDOptionsTypeSeizeDevice ("backward compatibility"), but
// *seizing* a keyboard-class device needs more privilege than Input
// Monitoring grants, so IOHIDDeviceOpen returns kIOReturnNotPermitted even
// with the permission granted. ykman opens non-exclusively and works — match
// it. No-op on non-darwin backends. (hidConfigureOpen is platform-split.)
hidConfigureOpen()
})
return hidInitErr
}
Expand All @@ -96,8 +105,16 @@ func hidList() ([]DeviceDescriptor, error) {
var out []DeviceDescriptor
collect := func(family string) hid.EnumFunc {
return func(info *hid.DeviceInfo) error {
// Only the keyboard collection carries chalresp; skip FIDO/other.
if info.UsagePage != usagePageKeyboard || info.Usage != usageKeyboard {
// The OTP/chalresp interface is the HID keyboard collection. The hidraw
// backend (Linux) populates UsagePage/Usage, so match the keyboard there.
// The libusb backend (macOS/*BSD) does NOT fill usage (hidapi's
// INVASIVE_GET_USAGE is off), leaving both 0 — there, fall back to USB
// interface 0, which is the OTP interface on YubiKey/OnlyKey whenever it's
// enabled (FIDO/CCID are later interfaces). This keeps us off the FIDO
// collection on both backends.
isKeyboard := info.UsagePage == usagePageKeyboard && info.Usage == usageKeyboard
usageUnknown := info.UsagePage == 0 && info.Usage == 0
if !isKeyboard && !(usageUnknown && info.InterfaceNbr == 0) {
return nil
}
out = append(out, DeviceDescriptor{
Expand Down Expand Up @@ -161,6 +178,15 @@ func hidChallenge(desc DeviceDescriptor, challenge []byte, mayBlock bool) ([]byt
// withDevice opens the device (by its enumerated path, or the first available
// one if the descriptor carries none), runs fn, and closes it.
func withDevice(desc DeviceDescriptor, fn func(*hid.Device) error) error {
// Pin this goroutine to its OS thread for the whole device interaction. On
// macOS, IOKit's IOHIDDeviceGetReport/SetReport do mach calls that Go's async
// preemption (SIGURG, Go 1.14+) can interrupt, surfacing as a transient
// kIOReturnError (0xE00002BC) — the report backends (hidraw/HID.dll) are
// unaffected, but LockOSThread is harmless there. Keeping every open + feature
// report + close on one thread eliminates the interruption. (See go-hid #15.)
runtime.LockOSThread()
defer runtime.UnlockOSThread()

if err := hidEnsureInit(); err != nil {
return err
}
Expand Down
16 changes: 16 additions & 0 deletions hardware/chalresp/chalresp_hid_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build darwin && !android && !ios && !nohw

package chalresp

import hid "github.com/sstallion/go-hid"

// hidConfigureOpen makes the macOS backend open devices NON-exclusively
// (kIOHIDOptionsTypeNone) instead of hidapi's default kIOHIDOptionsTypeSeizeDevice.
// Seizing a keyboard-class HID device (the YubiKey OTP interface) requires more
// privilege than the "Input Monitoring" permission grants, so a seizing open fails
// with kIOReturnNotPermitted (0xE00002C1) even when Input Monitoring IS granted.
// Opening non-exclusively — as ykman does — succeeds with the permission. This only
// affects challenge-response feature reports, which never need exclusive access.
func hidConfigureOpen() {
hid.SetOpenExclusive(false)
}
8 changes: 8 additions & 0 deletions hardware/chalresp/chalresp_hid_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build !darwin && !android && !ios && !nohw

package chalresp

// hidConfigureOpen is a no-op on the non-darwin HID backends. Linux (hidraw) and
// Windows (hid.dll) don't have hidapi's macOS seize-on-open behavior, and go-hid's
// SetOpenExclusive is a darwin-only symbol, so there's nothing to configure here.
func hidConfigureOpen() {}
7 changes: 7 additions & 0 deletions hardware/chalresp/chalresp_pcsc.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ func pcscList() ([]DeviceDescriptor, error) {
}
var out []DeviceDescriptor
for _, reader := range readers {
// YubiKey slot-2 HMAC challenge-response is HID-only; its CCID OATH applet
// answers SELECT but NOT the HMAC APDU (returns 6B00). The HID backend handles
// YubiKeys, so never surface one here — otherwise on macOS (where the OS PC/SC
// service exposes the YubiKey's CCID iface) it would shadow the HID path.
if familyForReader(reader) == "yubikey" {
continue
}
card, err := ctx.Connect(reader, scard.ShareShared, scard.ProtocolAny)
if err != nil {
continue // no card / in use
Expand Down
Loading