Repository: go-piv/piv-go
Pull request #39 (2020-04-26 00:51:00)

piv: have YubiKey hold on to a transaction


Patch:
diff --git a/piv/key.go b/piv/key.go
index 789ba4c..073d2c3 100644
--- a/piv/key.go
+++ b/piv/key.go
@@ -306,12 +306,7 @@ func (yk *YubiKey) AttestationCertificate() (*x509.Certificate, error) {
 // certificate. This can be used to prove a key was generate on a specific
 // YubiKey.
 func (yk *YubiKey) Attest(slot Slot) (*x509.Certificate, error) {
-	tx, err := yk.begin()
-	if err != nil {
-		return nil, err
-	}
-	defer tx.Close()
-	return ykAttest(tx, slot)
+	return ykAttest(yk.tx, slot)
 }
 
 func ykAttest(tx *scTx, slot Slot) (*x509.Certificate, error) {
@@ -339,15 +334,6 @@ func ykAttest(tx *scTx, slot Slot) (*x509.Certificate, error) {
 
 // Certificate returns the certifiate object stored in a given slot.
 func (yk *YubiKey) Certificate(slot Slot) (*x509.Certificate, error) {
-	tx, err := yk.begin()
-	if err != nil {
-		return nil, err
-	}
-	defer tx.Close()
-	return ykGetCertificate(tx, slot)
-}
-
-func ykGetCertificate(tx *scTx, slot Slot) (*x509.Certificate, error) {
 	cmd := apdu{
 		instruction: insGetData,
 		param1:      0x3f,
@@ -360,7 +346,7 @@ func ykGetCertificate(tx *scTx, slot Slot) (*x509.Certificate, error) {
 			byte(slot.Object),
 		},
 	}
-	resp, err := tx.Transmit(cmd)
+	resp, err := yk.tx.Transmit(cmd)
 	if err != nil {
 		return nil, fmt.Errorf("command failed: %w", err)
 	}
@@ -401,15 +387,10 @@ 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 {
-	tx, err := yk.begin()
-	if err != nil {
-		return err
-	}
-	defer tx.Close()
-	if err := ykAuthenticate(tx, key, yk.rand); err != nil {
+	if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil {
 		return fmt.Errorf("authenticating with management key: %w", err)
 	}
-	return ykStoreCertificate(tx, slot, cert)
+	return ykStoreCertificate(yk.tx, slot, cert)
 }
 
 func ykStoreCertificate(tx *scTx, slot Slot, cert *x509.Certificate) error {
@@ -455,15 +436,10 @@ 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) {
-	tx, err := yk.begin()
-	if err != nil {
-		return nil, err
-	}
-	defer tx.Close()
-	if err := ykAuthenticate(tx, key, yk.rand); err != nil {
+	if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil {
 		return nil, fmt.Errorf("authenticating with management key: %w", err)
 	}
-	return ykGenerateKey(tx, slot, opts)
+	return ykGenerateKey(yk.tx, slot, opts)
 }
 
 func ykGenerateKey(tx *scTx, slot Slot, o Key) (crypto.PublicKey, error) {
@@ -530,21 +506,10 @@ type KeyAuth struct {
 }
 
 func (k KeyAuth) begin(yk *YubiKey) (tx *scTx, err error) {
-	tx, err = yk.begin()
-	if err != nil {
-		return nil, err
-	}
-	defer func() {
-		if err != nil {
-			tx.Close()
-			tx = nil
-		}
-	}()
-
 	// TODO(ericchiang): support cached pin and touch policies, possibly by
 	// attempting to sign, then prompting on specific apdu error codes.
 	if k.PIN != "" {
-		if err := ykLogin(tx, k.PIN); err != nil {
+		if err := ykLogin(yk.tx, k.PIN); err != nil {
 			return tx, fmt.Errorf("authenticating with pin: %w", err)
 		}
 	} else if k.PINPrompt != nil {
@@ -552,11 +517,11 @@ func (k KeyAuth) begin(yk *YubiKey) (tx *scTx, err error) {
 		if err != nil {
 			return tx, fmt.Errorf("pin prompt: %v", err)
 		}
-		if err := ykLogin(tx, pin); err != nil {
+		if err := ykLogin(yk.tx, pin); err != nil {
 			return tx, fmt.Errorf("authenticating with pin: %w", err)
 		}
 	}
-	return tx, nil
+	return yk.tx, nil
 }
 
 // PrivateKey is used to access signing and decryption options for the key
diff --git a/piv/piv.go b/piv/piv.go
index 281297b..99c3a6a 100644
--- a/piv/piv.go
+++ b/piv/piv.go
@@ -92,10 +92,14 @@ const (
 	insGetSerial     = 0xf8
 )
 
-// YubiKey is an open connection to a YubiKey smart card.
+// YubiKey is an exclusive open connection to a YubiKey smart card. While open,
+// no other process can query the given card.
+//
+// To release the connection, call the Close method.
 type YubiKey struct {
 	ctx *scContext
 	h   *scHandle
+	tx  *scTx
 
 	rand io.Reader
 
@@ -152,14 +156,17 @@ func (c *client) Open(card string) (*YubiKey, error) {
 		ctx.Close()
 		return nil, fmt.Errorf("connecting to smart card: %w", err)
 	}
-
-	yk := &YubiKey{ctx: ctx, h: h}
-	tx, err := yk.begin()
+	tx, err := h.Begin()
 	if err != nil {
-		yk.Close()
-		return nil, fmt.Errorf("initializing yubikey: %w", err)
+		return nil, fmt.Errorf("beginning smart card transaction: %w", err)
 	}
-	v, err := ykVersion(tx)
+	if err := ykSelectApplication(tx, aidPIV[:]); err != nil {
+		tx.Close()
+		return nil, fmt.Errorf("selecting piv applet: %w", err)
+	}
+
+	yk := &YubiKey{ctx: ctx, h: h, tx: tx}
+	v, err := ykVersion(yk.tx)
 	if err != nil {
 		yk.Close()
 		return nil, fmt.Errorf("getting yubikey version: %w", err)
@@ -173,26 +180,9 @@ func (c *client) Open(card string) (*YubiKey, error) {
 	return yk, nil
 }
 
-func (yk *YubiKey) begin() (*scTx, error) {
-	tx, err := yk.h.Begin()
-	if err != nil {
-		return nil, fmt.Errorf("beginning smart card transaction: %w", err)
-	}
-	if err := ykSelectApplication(tx, aidPIV[:]); err != nil {
-		tx.Close()
-		return nil, fmt.Errorf("selecting piv applet: %w", err)
-	}
-	return tx, nil
-}
-
 // Serial returns the YubiKey's serial number.
 func (yk *YubiKey) Serial() (uint32, error) {
-	tx, err := yk.begin()
-	if err != nil {
-		return 0, err
-	}
-	defer tx.Close()
-	return ykSerial(tx, yk.version)
+	return ykSerial(yk.tx, yk.version)
 }
 
 func encodePIN(pin string) ([]byte, error) {
@@ -219,12 +209,7 @@ func encodePIN(pin string) ([]byte, error) {
 //
 // Use DefaultPIN if the PIN hasn't been set.
 func (yk *YubiKey) authPIN(pin string) error {
-	tx, err := yk.begin()
-	if err != nil {
-		return err
-	}
-	defer tx.Close()
-	return ykLogin(tx, pin)
+	return ykLogin(yk.tx, pin)
 }
 
 func ykLogin(tx *scTx, pin string) error {
@@ -242,12 +227,7 @@ func ykLogin(tx *scTx, pin string) error {
 
 // Retries returns the number of attempts remaining to enter the correct PIN.
 func (yk *YubiKey) Retries() (int, error) {
-	tx, err := yk.begin()
-	if err != nil {
-		return 0, err
-	}
-	defer tx.Close()
-	return ykPINRetries(tx)
+	return ykPINRetries(yk.tx)
 }
 
 func ykPINRetries(tx *scTx) (int, error) {
@@ -267,12 +247,7 @@ 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 {
-	tx, err := yk.begin()
-	if err != nil {
-		return err
-	}
-	defer tx.Close()
-	return ykReset(tx, yk.rand)
+	return ykReset(yk.tx, yk.rand)
 }
 
 func ykReset(tx *scTx, r io.Reader) error {
@@ -341,12 +316,7 @@ type version struct {
 //
 // Use DefaultManagementKey if the management key hasn't been set.
 func (yk *YubiKey) authManagementKey(key [24]byte) error {
-	tx, err := yk.begin()
-	if err != nil {
-		return err
-	}
-	defer tx.Close()
-	return ykAuthenticate(tx, key, yk.rand)
+	return ykAuthenticate(yk.tx, key, yk.rand)
 }
 
 var (
@@ -464,16 +434,10 @@ func ykAuthenticate(tx *scTx, key [24]byte, rand io.Reader) error {
 //
 //
 func (yk *YubiKey) SetManagementKey(oldKey, newKey [24]byte) error {
-	tx, err := yk.begin()
-	if err != nil {
-		return err
-	}
-	defer tx.Close()
-
-	if err := ykAuthenticate(tx, oldKey, yk.rand); err != nil {
+	if err := ykAuthenticate(yk.tx, oldKey, yk.rand); err != nil {
 		return fmt.Errorf("authenticating with old key: %w", err)
 	}
-	if err := ykSetManagementKey(tx, newKey, false); err != nil {
+	if err := ykSetManagementKey(yk.tx, newKey, false); err != nil {
 		return err
 	}
 	return nil
@@ -516,12 +480,7 @@ func ykSetManagementKey(tx *scTx, key [24]byte, touch bool) error {
 //		}
 //
 func (yk *YubiKey) SetPIN(oldPIN, newPIN string) error {
-	tx, err := yk.begin()
-	if err != nil {
-		return err
-	}
-	defer tx.Close()
-	return ykChangePIN(tx, oldPIN, newPIN)
+	return ykChangePIN(yk.tx, oldPIN, newPIN)
 }
 
 func ykChangePIN(tx *scTx, oldPIN, newPIN string) error {
@@ -544,12 +503,7 @@ 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 {
-	tx, err := yk.begin()
-	if err != nil {
-		return err
-	}
-	defer tx.Close()
-	return ykUnblockPIN(tx, puk, newPIN)
+	return ykUnblockPIN(yk.tx, puk, newPIN)
 }
 
 func ykUnblockPIN(tx *scTx, puk, newPIN string) error {
@@ -587,12 +541,7 @@ func ykUnblockPIN(tx *scTx, puk, newPIN string) error {
 //		}
 //
 func (yk *YubiKey) SetPUK(oldPUK, newPUK string) error {
-	tx, err := yk.begin()
-	if err != nil {
-		return err
-	}
-	defer tx.Close()
-	return ykChangePUK(tx, oldPUK, newPUK)
+	return ykChangePUK(yk.tx, oldPUK, newPUK)
 }
 
 func ykChangePUK(tx *scTx, oldPUK, newPUK string) error {
@@ -704,12 +653,7 @@ func unmarshalDERField(b []byte, tag uint64) (obj []byte, err 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) {
-	tx, err := yk.begin()
-	if err != nil {
-		return nil, err
-	}
-	defer tx.Close()
-	m, err := ykGetProtectedMetadata(tx, pin)
+	m, err := ykGetProtectedMetadata(yk.tx, pin)
 	if err != nil {
 		if errors.Is(err, ErrNotFound) {
 			return &Metadata{}, nil
@@ -723,12 +667,7 @@ func (yk *YubiKey) Metadata(pin string) (*Metadata, error) {
 // 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 {
-	tx, err := yk.begin()
-	if err != nil {
-		return err
-	}
-	defer tx.Close()
-	return ykSetProtectedMetadata(tx, key, m)
+	return ykSetProtectedMetadata(yk.tx, key, m)
 }
 
 // Metadata holds protected metadata. This is primarily used by YubiKey manager


Test patch:
diff --git a/piv/piv_test.go b/piv/piv_test.go
index cb6145f..20bac0c 100644
--- a/piv/piv_test.go
+++ b/piv/piv_test.go
@@ -157,15 +157,9 @@ func TestYubiKeyUnblockPIN(t *testing.T) {
 	yk, close := newTestYubiKey(t)
 	defer close()
 
-	tx, err := yk.begin()
-	if err != nil {
-		t.Fatalf("begin transaction: %v", err)
-	}
-	defer tx.Close()
-
 	badPIN := "0"
 	for {
-		err := ykLogin(tx, badPIN)
+		err := ykLogin(yk.tx, badPIN)
 		if err == nil {
 			t.Fatalf("login with bad pin succeeded")
 		}
@@ -178,10 +172,10 @@ func TestYubiKeyUnblockPIN(t *testing.T) {
 		}
 	}
 
-	if err := ykUnblockPIN(tx, DefaultPUK, DefaultPIN); err != nil {
+	if err := yk.Unblock(DefaultPUK, DefaultPIN); err != nil {
 		t.Fatalf("unblocking pin: %v", err)
 	}
-	if err := ykLogin(tx, DefaultPIN); err != nil {
+	if err := ykLogin(yk.tx, DefaultPIN); err != nil {
 		t.Errorf("failed to login with pin after unblock: %v", err)
 	}
 }