diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1790b87..7c43c91 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,8 +22,6 @@ jobs: id: go - name: Check out code into the Go module directory uses: actions/checkout@v2 - - name: Install staticcheck - run: go install honnef.co/go/tools/cmd/staticcheck@v0.3.3 - name: Install libpcsc run: sudo apt-get install -y libpcsclite-dev pcscd pcsc-tools - name: Test @@ -42,8 +40,6 @@ jobs: id: go - name: Check out code into the Go module directory uses: actions/checkout@v2 - - name: Install staticcheck - run: go install honnef.co/go/tools/cmd/staticcheck@v0.3.3 - name: Test run: "make build" env: diff --git a/Makefile b/Makefile index 698d316..d454b69 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,7 @@ .PHONY: test -test: lint +test: go test -v ./... -.PHONY: lint -lint: - staticcheck ./... - .PHONY: build -build: lint +build: go build ./... diff --git a/piv/key.go b/piv/key.go index d3ec1a5..9125272 100644 --- a/piv/key.go +++ b/piv/key.go @@ -507,7 +507,12 @@ func (yk *YubiKey) AttestationCertificate() (*x509.Certificate, error) { // // If the slot doesn't have a key, the returned error wraps ErrNotFound. func (yk *YubiKey) Attest(slot Slot) (*x509.Certificate, error) { - cert, err := ykAttest(yk.tx, slot) + var cert *x509.Certificate + err := yk.tx(func(tx *scTx) error { + var err error + cert, err = ykAttest(tx, slot) + return err + }) if err == nil { return cert, nil } @@ -558,7 +563,12 @@ func (yk *YubiKey) Certificate(slot Slot) (*x509.Certificate, error) { byte(slot.Object), }, } - resp, err := yk.tx.Transmit(cmd) + var resp []byte + err := yk.tx(func(tx *scTx) error { + var err error + resp, err = tx.Transmit(cmd) + return err + }) if err != nil { return nil, fmt.Errorf("command failed: %w", err) } @@ -605,10 +615,12 @@ func marshalASN1(tag byte, data []byte) []byte { // certificate isn't required to use the associated key for signing or // decryption. func (yk *YubiKey) SetCertificate(key [24]byte, slot Slot, cert *x509.Certificate) error { - if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil { - return fmt.Errorf("authenticating with management key: %w", err) - } - return ykStoreCertificate(yk.tx, slot, cert) + return yk.tx(func(tx *scTx) error { + if err := ykAuthenticate(tx, key, yk.rand); err != nil { + return fmt.Errorf("authenticating with management key: %w", err) + } + return ykStoreCertificate(tx, slot, cert) + }) } func ykStoreCertificate(tx *scTx, slot Slot, cert *x509.Certificate) error { @@ -659,10 +671,17 @@ type Key struct { // GenerateKey generates an asymmetric key on the card, returning the key's // public key. func (yk *YubiKey) GenerateKey(key [24]byte, slot Slot, opts Key) (crypto.PublicKey, error) { - if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil { - return nil, fmt.Errorf("authenticating with management key: %w", err) - } - return ykGenerateKey(yk.tx, slot, opts) + var pub crypto.PublicKey + err := yk.tx(func(tx *scTx) error { + if err := ykAuthenticate(tx, key, yk.rand); err != nil { + return fmt.Errorf("authenticating with management key: %w", err) + } + + var err error + pub, err = ykGenerateKey(tx, slot, opts) + return err + }) + return pub, err } func ykGenerateKey(tx *scTx, slot Slot, o Key) (crypto.PublicKey, error) { @@ -731,8 +750,12 @@ type KeyAuth struct { // If provided, PINPrompt is ignored. PIN string // PINPrompt can be used to interactively request the PIN from the user. The - // method is only called when needed. For example, if a key specifies - // PINPolicyOnce, PINPrompt will only be called once per YubiKey struct. + // method is only called when needed for exclusive connections. If a key + // specifies PINPolicyOnce, PINPrompt will only be called once per YubiKey + // struct. + // + // Clients using a shared connection will call the PIN prompt on every + // operation, regardless of PIN policy. PINPrompt func() (pin string, err error) // PINPolicy can be used to specify the PIN caching strategy for the slot. If @@ -743,7 +766,7 @@ type KeyAuth struct { PINPolicy PINPolicy } -func (k KeyAuth) authTx(yk *YubiKey, pp PINPolicy) error { +func (k KeyAuth) authTx(tx *scTx, pp PINPolicy) error { // PINPolicyNever shouldn't require a PIN. if pp == PINPolicyNever { return nil @@ -752,7 +775,7 @@ func (k KeyAuth) authTx(yk *YubiKey, pp PINPolicy) error { // PINPolicyAlways should always prompt a PIN even if the key says that // login isn't needed. // https://github.com/go-piv/piv-go/issues/49 - if pp != PINPolicyAlways && !ykLoginNeeded(yk.tx) { + if pp != PINPolicyAlways && !ykLoginNeeded(tx) { return nil } @@ -767,14 +790,20 @@ func (k KeyAuth) authTx(yk *YubiKey, pp PINPolicy) error { if pin == "" { return fmt.Errorf("pin required but wasn't provided") } - return ykLogin(yk.tx, pin) + return ykLogin(tx, pin) } func (k KeyAuth) do(yk *YubiKey, pp PINPolicy, f func(tx *scTx) ([]byte, error)) ([]byte, error) { - if err := k.authTx(yk, pp); err != nil { - return nil, err - } - return f(yk.tx) + var b []byte + err := yk.tx(func(tx *scTx) error { + var err error + if err := k.authTx(tx, pp); err != nil { + return err + } + b, err = f(tx) + return err + }) + return b, err } func pinPolicy(yk *YubiKey, slot Slot) (PINPolicy, error) { @@ -919,11 +948,12 @@ func (yk *YubiKey) SetPrivateKeyInsecure(key [24]byte, slot Slot, private crypto tags = append(tags, param...) } - if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil { - return fmt.Errorf("authenticating with management key: %w", err) - } - - return ykImportKey(yk.tx, tags, slot, policy) + return yk.tx(func(tx *scTx) error { + if err := ykAuthenticate(tx, key, yk.rand); err != nil { + return fmt.Errorf("authenticating with management key: %w", err) + } + return ykImportKey(tx, tags, slot, policy) + }) } func ykImportKey(tx *scTx, tags []byte, slot Slot, o Key) error { diff --git a/piv/pcsc_test.go b/piv/pcsc_test.go index 5902564..7dcf2ee 100644 --- a/piv/pcsc_test.go +++ b/piv/pcsc_test.go @@ -63,7 +63,7 @@ func runHandleTest(t *testing.T, f func(t *testing.T, h *scHandle)) { if reader == "" { t.Skip("could not find yubikey, skipping testing") } - h, err := c.Connect(reader) + h, err := c.Connect(reader, false) if err != nil { t.Fatalf("connecting to %s: %v", reader, err) } diff --git a/piv/pcsc_unix.go b/piv/pcsc_unix.go index a43d259..081e37e 100644 --- a/piv/pcsc_unix.go +++ b/piv/pcsc_unix.go @@ -92,13 +92,17 @@ type scHandle struct { h C.SCARDHANDLE } -func (c *scContext) Connect(reader string) (*scHandle, error) { +func (c *scContext) Connect(reader string, shared bool) (*scHandle, error) { var ( handle C.SCARDHANDLE activeProtocol C.DWORD ) + var mode C.DWORD = C.SCARD_SHARE_EXCLUSIVE + if shared { + mode = C.SCARD_SHARE_SHARED + } rc := C.SCardConnect(c.ctx, C.CString(reader), - C.SCARD_SHARE_EXCLUSIVE, C.SCARD_PROTOCOL_T1, + mode, C.SCARD_PROTOCOL_T1, &handle, &activeProtocol) if err := scCheck(rc); err != nil { return nil, err diff --git a/piv/pcsc_windows.go b/piv/pcsc_windows.go index 845194f..3b92505 100644 --- a/piv/pcsc_windows.go +++ b/piv/pcsc_windows.go @@ -35,6 +35,7 @@ var ( const ( scardScopeSystem = 2 scardShareExclusive = 1 + scardShareShared = 2 scardLeaveCard = 0 scardProtocolT1 = 2 scardPCIT1 = 0 @@ -122,7 +123,7 @@ func (c *scContext) ListReaders() ([]string, error) { return readers, nil } -func (c *scContext) Connect(reader string) (*scHandle, error) { +func (c *scContext) Connect(reader string, shared bool) (*scHandle, error) { var ( handle syscall.Handle activeProtocol uint16 @@ -131,10 +132,15 @@ func (c *scContext) Connect(reader string) (*scHandle, error) { if err != nil { return nil, fmt.Errorf("invalid reader string: %v", err) } + + mode := uintptr(scardShareExclusive) + if shared { + mode = scardShareShared + } r0, _, _ := procSCardConnectW.Call( uintptr(c.ctx), uintptr(unsafe.Pointer(readerPtr)), - scardShareExclusive, + mode, scardProtocolT1, uintptr(unsafe.Pointer(&handle)), uintptr(activeProtocol), diff --git a/piv/piv.go b/piv/piv.go index 4e7171a..1c42e29 100644 --- a/piv/piv.go +++ b/piv/piv.go @@ -41,6 +41,8 @@ var ( 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, } + + defaultClient = &Client{} ) // Cards lists all smart cards available via PC/SC interface. Card names are @@ -51,8 +53,7 @@ var ( // // See: https://ludovicrousseau.blogspot.com/2010/05/what-is-in-pcsc-reader-name.html func Cards() ([]string, error) { - var c client - return c.Cards() + return defaultClient.Cards() } const ( @@ -102,7 +103,9 @@ const ( type YubiKey struct { ctx *scContext h *scHandle - tx *scTx + + // May be nil, use tx() to access. + exclusiveTx *scTx rand io.Reader @@ -114,32 +117,74 @@ type YubiKey struct { version *version } +func (yk *YubiKey) tx(fn func(tx *scTx) error) (err error) { + tx := yk.exclusiveTx + if tx == nil { + tx, err = yk.h.Begin() + if err != nil { + return fmt.Errorf("beginning transaction: %v", err) + } + defer func() { + terr := tx.Close() + if terr != nil && err == nil { + err = fmt.Errorf("closing trasnaction: %v", terr) + } + }() + + if err := ykSelectApplication(tx, aidPIV[:]); err != nil { + return fmt.Errorf("selecting piv applet: %w", err) + } + } + return fn(tx) +} + // Close releases the connection to the smart card. func (yk *YubiKey) Close() error { - err1 := yk.h.Close() - err2 := yk.ctx.Close() - if err1 == nil { - return err2 + var err error + if yk.h != nil { + err = yk.h.Close() } - return err1 + if yk.ctx != nil { + cerr := yk.ctx.Close() + if err == nil { + err = cerr + } + } + return err } // Open connects to a YubiKey smart card. func Open(card string) (*YubiKey, error) { - var c client - return c.Open(card) + return defaultClient.Open(card) } -// client is a smart card client and may be exported in the future to allow -// configuration for the top level Open() and Cards() APIs. -type client struct { - // Rand is a cryptographic source of randomness used for card challenges. +// Client can be used to configure the behavior of the smartcard connection. +type Client struct { + // Rand is a cryptographic source of randomness used for card challenges, + // and other operations. // // If nil, defaults to crypto.Rand. Rand io.Reader + + // Shared causes the client to use a non-exclusive connection to the + // smartcard, allowing multiple concurrent users of the PCSC daemon. + // Note that a single client using an exclusive connection will block + // transactions for all other clients. For example, common GPG agents + // use exclusive connections that don't work with this option. + // + // Enabling this option breaks PINPolicyOnce, requiring a PIN on every + // operation. + Shared bool } -func (c *client) Cards() ([]string, error) { +// Cards lists all smart cards available via PC/SC interface. Card names are +// strings describing the key, such as "Yubico Yubikey NEO OTP+U2F+CCID 00 00". +// +// Card names depend on the operating system and what port a card is plugged +// into. To uniquely identify a card, use its serial number. +// +// See: https://ludovicrousseau.blogspot.com/2010/05/what-is-in-pcsc-reader-name.html +func (c *Client) Cards() ([]string, error) { ctx, err := newSCContext() if err != nil { return nil, fmt.Errorf("connecting to pscs: %w", err) @@ -148,37 +193,51 @@ func (c *client) Cards() ([]string, error) { return ctx.ListReaders() } -func (c *client) Open(card string) (*YubiKey, error) { +// Open connects to a YubiKey smart card. +func (c *Client) Open(card string) (*YubiKey, error) { ctx, err := newSCContext() if err != nil { return nil, fmt.Errorf("connecting to smart card daemon: %w", err) } - h, err := ctx.Connect(card) - if err != nil { - ctx.Close() - return nil, fmt.Errorf("connecting to smart card: %w", err) + yk := &YubiKey{ctx: ctx} + + if c.Rand != nil { + yk.rand = c.Rand + } else { + yk.rand = rand.Reader } - tx, err := h.Begin() + + h, err := ctx.Connect(card, c.Shared) if err != nil { - return nil, fmt.Errorf("beginning smart card transaction: %w", err) + yk.Close() + return nil, fmt.Errorf("connecting to smart card: %w", err) } - if err := ykSelectApplication(tx, aidPIV[:]); err != nil { - tx.Close() - return nil, fmt.Errorf("selecting piv applet: %w", err) + yk.h = h + + if !c.Shared { + tx, err := h.Begin() + if err != nil { + return nil, fmt.Errorf("beginning smart card transaction: %w", err) + } + yk.exclusiveTx = tx } - yk := &YubiKey{ctx: ctx, h: h, tx: tx} - v, err := ykVersion(yk.tx) + err = yk.tx(func(tx *scTx) error { + if err := ykSelectApplication(tx, aidPIV[:]); err != nil { + return fmt.Errorf("selecting piv applet: %w", err) + } + + v, err := ykVersion(tx) + if err != nil { + return fmt.Errorf("getting yubikey version: %w", err) + } + yk.version = v + return nil + }) if err != nil { yk.Close() - return nil, fmt.Errorf("getting yubikey version: %w", err) - } - yk.version = v - if c.Rand != nil { - yk.rand = c.Rand - } else { - yk.rand = rand.Reader + return nil, err } return yk, nil } @@ -198,7 +257,13 @@ func (yk *YubiKey) Version() Version { // Serial returns the YubiKey's serial number. func (yk *YubiKey) Serial() (uint32, error) { - return ykSerial(yk.tx, yk.version) + var n uint32 + err := yk.tx(func(tx *scTx) error { + var err error + n, err = ykSerial(tx, yk.version) + return err + }) + return n, err } func encodePIN(pin string) ([]byte, error) { @@ -225,7 +290,9 @@ func encodePIN(pin string) ([]byte, error) { // // Use DefaultPIN if the PIN hasn't been set. func (yk *YubiKey) authPIN(pin string) error { - return ykLogin(yk.tx, pin) + return yk.tx(func(tx *scTx) error { + return ykLogin(tx, pin) + }) } func ykLogin(tx *scTx, pin string) error { @@ -250,7 +317,13 @@ func ykLoginNeeded(tx *scTx) bool { // Retries returns the number of attempts remaining to enter the correct PIN. func (yk *YubiKey) Retries() (int, error) { - return ykPINRetries(yk.tx) + var n int + err := yk.tx(func(tx *scTx) error { + var err error + n, err = ykPINRetries(tx) + return err + }) + return n, err } func ykPINRetries(tx *scTx) (int, error) { @@ -270,7 +343,9 @@ func ykPINRetries(tx *scTx) (int, error) { // and resetting the PIN, PUK, and Management Key to their default values. This // does NOT affect data on other applets, such as GPG or U2F. func (yk *YubiKey) Reset() error { - return ykReset(yk.tx, yk.rand) + return yk.tx(func(tx *scTx) error { + return ykReset(tx, yk.rand) + }) } func ykReset(tx *scTx, r io.Reader) error { @@ -338,8 +413,8 @@ type version struct { // certificates to slots. // // Use DefaultManagementKey if the management key hasn't been set. -func (yk *YubiKey) authManagementKey(key [24]byte) error { - return ykAuthenticate(yk.tx, key, yk.rand) +func (yk *YubiKey) authManagementKey(tx *scTx, key [24]byte) error { + return ykAuthenticate(tx, key, yk.rand) } var ( @@ -457,13 +532,12 @@ func ykAuthenticate(tx *scTx, key [24]byte, rand io.Reader) error { // // func (yk *YubiKey) SetManagementKey(oldKey, newKey [24]byte) error { - if err := ykAuthenticate(yk.tx, oldKey, yk.rand); err != nil { - return fmt.Errorf("authenticating with old key: %w", err) - } - if err := ykSetManagementKey(yk.tx, newKey, false); err != nil { - return err - } - return nil + return yk.tx(func(tx *scTx) error { + if err := ykAuthenticate(tx, oldKey, yk.rand); err != nil { + return fmt.Errorf("authenticating with old key: %w", err) + } + return ykSetManagementKey(tx, newKey, false) + }) } // ykSetManagementKey updates the management key to a new key. This requires @@ -503,7 +577,9 @@ func ykSetManagementKey(tx *scTx, key [24]byte, touch bool) error { // } // func (yk *YubiKey) SetPIN(oldPIN, newPIN string) error { - return ykChangePIN(yk.tx, oldPIN, newPIN) + return yk.tx(func(tx *scTx) error { + return ykChangePIN(tx, oldPIN, newPIN) + }) } func ykChangePIN(tx *scTx, oldPIN, newPIN string) error { @@ -526,7 +602,9 @@ func ykChangePIN(tx *scTx, oldPIN, newPIN string) error { // Unblock unblocks the PIN, setting it to a new value. func (yk *YubiKey) Unblock(puk, newPIN string) error { - return ykUnblockPIN(yk.tx, puk, newPIN) + return yk.tx(func(tx *scTx) error { + return ykUnblockPIN(tx, puk, newPIN) + }) } func ykUnblockPIN(tx *scTx, puk, newPIN string) error { @@ -564,7 +642,9 @@ func ykUnblockPIN(tx *scTx, puk, newPIN string) error { // } // func (yk *YubiKey) SetPUK(oldPUK, newPUK string) error { - return ykChangePUK(yk.tx, oldPUK, newPUK) + return yk.tx(func(tx *scTx) error { + return ykChangePUK(tx, oldPUK, newPUK) + }) } func ykChangePUK(tx *scTx, oldPUK, newPUK string) error { @@ -635,21 +715,28 @@ func ykSerial(tx *scTx, v *version) (uint32, error) { // Metadata returns protected data stored on the card. This can be used to // retrieve PIN protected management keys. func (yk *YubiKey) Metadata(pin string) (*Metadata, error) { - m, err := ykGetProtectedMetadata(yk.tx, pin) - if err != nil { - if errors.Is(err, ErrNotFound) { - return &Metadata{}, nil + var m *Metadata + err := yk.tx(func(tx *scTx) error { + var err error + m, err = ykGetProtectedMetadata(tx, pin) + if err != nil { + if errors.Is(err, ErrNotFound) { + m = &Metadata{} + err = nil + } } - return nil, err - } - return m, nil + return err + }) + return m, err } // SetMetadata sets PIN protected metadata on the key. This is primarily to // store the management key on the smart card instead of managing the PIN and // management key seperately. func (yk *YubiKey) SetMetadata(key [24]byte, m *Metadata) error { - return ykSetProtectedMetadata(yk.tx, key, m) + return yk.tx(func(tx *scTx) error { + return ykSetProtectedMetadata(tx, key, m) + }) } // Metadata holds protected metadata. This is primarily used by YubiKey manager diff --git a/piv/piv_test.go b/piv/piv_test.go index 534b259..e7575c7 100644 --- a/piv/piv_test.go +++ b/piv/piv_test.go @@ -20,6 +20,7 @@ import ( "encoding/hex" "errors" "flag" + "fmt" "io" "math/bits" "strings" @@ -74,8 +75,27 @@ func TestCards(t *testing.T) { } } +func runTest(t *testing.T, fn func(*testing.T, *YubiKey)) { + t.Run("Exclusive", func(t *testing.T) { + yk, close := newTestYubiKey(t) + fn(t, yk) + close() + }) + t.Run("Shared", func(t *testing.T) { + c := &Client{Shared: true} + yk, close := newTestYubiKeyClient(t, c) + fn(t, yk) + close() + }) +} + func newTestYubiKey(t *testing.T) (*YubiKey, func()) { - cards, err := Cards() + var c Client + return newTestYubiKeyClient(t, &c) +} + +func newTestYubiKeyClient(t *testing.T, c *Client) (*YubiKey, func()) { + cards, err := c.Cards() if err != nil { t.Fatalf("listing cards: %v", err) } @@ -86,7 +106,7 @@ func newTestYubiKey(t *testing.T) (*YubiKey, func()) { if !canModifyYubiKey { t.Skip("not running test that accesses yubikey, provide --wipe-yubikey flag") } - yk, err := Open(card) + yk, err := c.Open(card) if err != nil { t.Fatalf("getting new yubikey: %v", err) } @@ -144,220 +164,229 @@ func TestMultipleConnections(t *testing.T) { } func TestYubiKeySerial(t *testing.T) { - yk, close := newTestYubiKey(t) - defer close() - - if _, err := yk.Serial(); err != nil { - t.Fatalf("getting serial number: %v", err) - } + runTest(t, func(t *testing.T, yk *YubiKey) { + if _, err := yk.Serial(); err != nil { + t.Fatalf("getting serial number: %v", err) + } + }) } func TestYubiKeyLoginNeeded(t *testing.T) { - yk, close := newTestYubiKey(t) - defer close() - - testRequiresVersion(t, yk, 4, 3, 0) - - if !ykLoginNeeded(yk.tx) { - t.Errorf("expected login needed") - } - if err := ykLogin(yk.tx, DefaultPIN); err != nil { - t.Fatalf("login: %v", err) - } - if ykLoginNeeded(yk.tx) { - t.Errorf("expected no login needed") - } + runTest(t, func(t *testing.T, yk *YubiKey) { + err := yk.tx(func(tx *scTx) error { + if !ykLoginNeeded(tx) { + t.Errorf("expected login needed") + } + if err := ykLogin(tx, DefaultPIN); err != nil { + t.Fatalf("login: %v", err) + } + if ykLoginNeeded(tx) { + t.Errorf("expected no login needed") + } + return nil + }) + if err != nil { + t.Errorf("transaction failed: %v", err) + } + }) } func TestYubiKeyPINRetries(t *testing.T) { - yk, close := newTestYubiKey(t) - defer close() - retries, err := yk.Retries() - if err != nil { - t.Fatalf("getting retries: %v", err) - } - if retries < 0 || retries > 15 { - t.Fatalf("invalid number of retries: %d", retries) - } + runTest(t, func(t *testing.T, yk *YubiKey) { + retries, err := yk.Retries() + if err != nil { + t.Fatalf("getting retries: %v", err) + } + if retries < 0 || retries > 15 { + t.Fatalf("invalid number of retries: %d", retries) + } + }) } func TestYubiKeyReset(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode.") - } - yk, close := newTestYubiKey(t) - defer close() - if err := yk.Reset(); err != nil { - t.Fatalf("resetting yubikey: %v", err) - } - if err := yk.authPIN(DefaultPIN); err != nil { - t.Fatalf("login: %v", err) - } + runTest(t, func(t *testing.T, yk *YubiKey) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + if err := yk.Reset(); err != nil { + t.Fatalf("resetting yubikey: %v", err) + } + if err := yk.authPIN(DefaultPIN); err != nil { + t.Fatalf("login: %v", err) + } + }) } func TestYubiKeyLogin(t *testing.T) { - yk, close := newTestYubiKey(t) - defer close() - - if err := yk.authPIN(DefaultPIN); err != nil { - t.Fatalf("login: %v", err) - } + runTest(t, func(t *testing.T, yk *YubiKey) { + if err := yk.authPIN(DefaultPIN); err != nil { + t.Fatalf("login: %v", err) + } + }) } func TestYubiKeyAuthenticate(t *testing.T) { - yk, close := newTestYubiKey(t) - defer close() - - if err := yk.authManagementKey(DefaultManagementKey); err != nil { - t.Errorf("authenticating: %v", err) - } + runTest(t, func(t *testing.T, yk *YubiKey) { + err := yk.tx(func(tx *scTx) error { + return yk.authManagementKey(tx, DefaultManagementKey) + }) + if err != nil { + t.Errorf("authenticating: %v", err) + } + }) } func TestYubiKeySetManagementKey(t *testing.T) { - yk, close := newTestYubiKey(t) - defer close() - - var mgmtKey [24]byte - if _, err := io.ReadFull(rand.Reader, mgmtKey[:]); err != nil { - t.Fatalf("generating management key: %v", err) - } + runTest(t, func(t *testing.T, yk *YubiKey) { + var mgmtKey [24]byte + if _, err := io.ReadFull(rand.Reader, mgmtKey[:]); err != nil { + t.Fatalf("generating management key: %v", err) + } - if err := yk.SetManagementKey(DefaultManagementKey, mgmtKey); err != nil { - t.Fatalf("setting management key: %v", err) - } - if err := yk.authManagementKey(mgmtKey); err != nil { - t.Errorf("authenticating with new management key: %v", err) - } - if err := yk.SetManagementKey(mgmtKey, DefaultManagementKey); err != nil { - t.Fatalf("resetting management key: %v", err) - } + if err := yk.SetManagementKey(DefaultManagementKey, mgmtKey); err != nil { + t.Fatalf("setting management key: %v", err) + } + err := yk.tx(func(tx *scTx) error { + return yk.authManagementKey(tx, DefaultManagementKey) + }) + if err != nil { + t.Errorf("authenticating with new management key: %v", err) + t.Errorf("authenticating: %v", err) + } + if err := yk.SetManagementKey(mgmtKey, DefaultManagementKey); err != nil { + t.Fatalf("resetting management key: %v", err) + } + }) } func TestYubiKeyUnblockPIN(t *testing.T) { - yk, close := newTestYubiKey(t) - defer close() - - badPIN := "0" - for { - err := ykLogin(yk.tx, badPIN) - if err == nil { - t.Fatalf("login with bad pin succeeded") + runTest(t, func(t *testing.T, yk *YubiKey) { + err := yk.tx(func(tx *scTx) error { + badPIN := "0" + for { + err := ykLogin(tx, badPIN) + if err == nil { + return fmt.Errorf("login with bad pin succeeded") + } + var e AuthErr + if !errors.As(err, &e) { + return fmt.Errorf("error returned was not a wrong pin error: %v", err) + } + if e.Retries == 0 { + return nil + } + } + }) + if err != nil { + t.Fatalf("blocking pin: %v", err) } - var e AuthErr - if !errors.As(err, &e) { - t.Fatalf("error returned was not a wrong pin error: %v", err) + + if err := yk.Unblock(DefaultPUK, DefaultPIN); err != nil { + t.Fatalf("unblocking pin: %v", err) } - if e.Retries == 0 { - break + if err := yk.tx(func(tx *scTx) error { + return ykLogin(tx, DefaultPIN) + }); err != nil { + t.Errorf("failed to login with pin after unblock: %v", err) } - } - - if err := yk.Unblock(DefaultPUK, DefaultPIN); err != nil { - t.Fatalf("unblocking pin: %v", err) - } - if err := ykLogin(yk.tx, DefaultPIN); err != nil { - t.Errorf("failed to login with pin after unblock: %v", err) - } + }) } func TestYubiKeyChangePIN(t *testing.T) { - yk, close := newTestYubiKey(t) - defer close() - - newPIN := "654321" - if err := yk.SetPIN(newPIN, newPIN); err == nil { - t.Errorf("successfully changed pin with invalid pin, expected error") - } - if err := yk.SetPIN(DefaultPIN, newPIN); err != nil { - t.Fatalf("changing pin: %v", err) - } - if err := yk.SetPIN(newPIN, DefaultPIN); err != nil { - t.Fatalf("resetting pin: %v", err) - } + runTest(t, func(t *testing.T, yk *YubiKey) { + newPIN := "654321" + if err := yk.SetPIN(newPIN, newPIN); err == nil { + t.Errorf("successfully changed pin with invalid pin, expected error") + } + if err := yk.SetPIN(DefaultPIN, newPIN); err != nil { + t.Fatalf("changing pin: %v", err) + } + if err := yk.SetPIN(newPIN, DefaultPIN); err != nil { + t.Fatalf("resetting pin: %v", err) + } + }) } func TestYubiKeyChangePUK(t *testing.T) { - yk, close := newTestYubiKey(t) - defer close() - - newPUK := "87654321" - if err := yk.SetPUK(newPUK, newPUK); err == nil { - t.Errorf("successfully changed puk with invalid puk, expected error") - } - if err := yk.SetPUK(DefaultPUK, newPUK); err != nil { - t.Fatalf("changing puk: %v", err) - } - if err := yk.SetPUK(newPUK, DefaultPUK); err != nil { - t.Fatalf("resetting puk: %v", err) - } + runTest(t, func(t *testing.T, yk *YubiKey) { + newPUK := "87654321" + if err := yk.SetPUK(newPUK, newPUK); err == nil { + t.Errorf("successfully changed puk with invalid puk, expected error") + } + if err := yk.SetPUK(DefaultPUK, newPUK); err != nil { + t.Fatalf("changing puk: %v", err) + } + if err := yk.SetPUK(newPUK, DefaultPUK); err != nil { + t.Fatalf("resetting puk: %v", err) + } + }) } func TestChangeManagementKey(t *testing.T) { - yk, close := newTestYubiKey(t) - defer close() - - var newKey [24]byte - if _, err := io.ReadFull(rand.Reader, newKey[:]); err != nil { - t.Fatalf("generating new management key: %v", err) - } - // Apply odd-parity - for i, b := range newKey { - if bits.OnesCount8(uint8(b))%2 == 0 { - newKey[i] = b ^ 1 // flip least significant bit + runTest(t, func(t *testing.T, yk *YubiKey) { + var newKey [24]byte + if _, err := io.ReadFull(rand.Reader, newKey[:]); err != nil { + t.Fatalf("generating new management key: %v", err) } - } - if err := yk.SetManagementKey(newKey, newKey); err == nil { - t.Errorf("successfully changed management key with invalid key, expected error") - } - if err := yk.SetManagementKey(DefaultManagementKey, newKey); err != nil { - t.Fatalf("changing management key: %v", err) - } - if err := yk.SetManagementKey(newKey, DefaultManagementKey); err != nil { - t.Fatalf("resetting management key: %v", err) - } + // Apply odd-parity + for i, b := range newKey { + if bits.OnesCount8(uint8(b))%2 == 0 { + newKey[i] = b ^ 1 // flip least significant bit + } + } + if err := yk.SetManagementKey(newKey, newKey); err == nil { + t.Errorf("successfully changed management key with invalid key, expected error") + } + if err := yk.SetManagementKey(DefaultManagementKey, newKey); err != nil { + t.Fatalf("changing management key: %v", err) + } + if err := yk.SetManagementKey(newKey, DefaultManagementKey); err != nil { + t.Fatalf("resetting management key: %v", err) + } + }) } func TestMetadata(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode.") - } - func() { - yk, close := newTestYubiKey(t) - defer close() - if err := yk.Reset(); err != nil { - t.Fatalf("resetting yubikey: %v", err) + runTest(t, func(t *testing.T, yk *YubiKey) { + if testing.Short() { + t.Skip("skipping test in short mode.") } - }() + func() { + if err := yk.Reset(); err != nil { + t.Fatalf("resetting yubikey: %v", err) + } + }() - yk, close := newTestYubiKey(t) - defer close() + yk, close := newTestYubiKey(t) + defer close() - if m, err := yk.Metadata(DefaultPIN); err != nil { - t.Errorf("getting metadata: %v", err) - } else if m.ManagementKey != nil { - t.Errorf("expected no management key set") - } + if m, err := yk.Metadata(DefaultPIN); err != nil { + t.Errorf("getting metadata: %v", err) + } else if m.ManagementKey != nil { + t.Errorf("expected no management key set") + } - wantKey := [24]byte{ - 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, - 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, - 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad, - } - m := &Metadata{ - ManagementKey: &wantKey, - } - if err := yk.SetMetadata(DefaultManagementKey, m); err != nil { - t.Fatalf("setting metadata: %v", err) - } - got, err := yk.Metadata(DefaultPIN) - if err != nil { - t.Fatalf("getting metadata: %v", err) - } - if got.ManagementKey == nil { - t.Errorf("no management key") - } else if *got.ManagementKey != wantKey { - t.Errorf("wanted management key=0x%x, got=0x%x", wantKey, got.ManagementKey) - } + wantKey := [24]byte{ + 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, + 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, + 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad, + } + m := &Metadata{ + ManagementKey: &wantKey, + } + if err := yk.SetMetadata(DefaultManagementKey, m); err != nil { + t.Fatalf("setting metadata: %v", err) + } + got, err := yk.Metadata(DefaultPIN) + if err != nil { + t.Fatalf("getting metadata: %v", err) + } + if got.ManagementKey == nil { + t.Errorf("no management key") + } else if *got.ManagementKey != wantKey { + t.Errorf("wanted management key=0x%x, got=0x%x", wantKey, got.ManagementKey) + } + }) } func TestMetadataUnmarshal(t *testing.T) {