Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for setting PIN/PUK attempt counters #64

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const (
// Non-standard; as implemented by SoloKeys. Chosen for low probability of eventual
// clashes, if and when PIV standard adds Ed25519 support
AlgEd25519 Algorithm = 0x22

// Non-standard extensions
AlgPIN Algorithm = 0xFF
)

func (a Algorithm) algType() algorithmType {
Expand Down
20 changes: 20 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,23 @@ func (c *Card) Retries() (int, error) {

return 0, fmt.Errorf("invalid response: %w", err)
}

// SetRetries sets the number of attempts for PIN and PUK.
//
// Both PIN and PUK will be reset to default values when this is executed.
// Requires authentication with management key and PIN verification.
func (c *Card) SetRetries(key ManagementKey, pin string, pinAttempts, pukAttempts int) error {
if err := login(c.tx, pin); err != nil {
return fmt.Errorf("PIN verification failed: %w", err)
}

if err := c.authenticate(key); err != nil {
return fmt.Errorf("failed to authenticate with management key: %w", err)
}

if _, err := send(c.tx, insSetPINRetries, byte(pinAttempts), byte(pukAttempts), nil); err != nil {
return fmt.Errorf("failed to execute command: %w", err)
}

return nil
}
61 changes: 61 additions & 0 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,64 @@ func TestChangeManagementKey(t *testing.T) {
require.NoError(t, err, "Failed to reset management key")
})
}

func TestSetRetries(t *testing.T) {
withCard(t, true, false, nil, func(t *testing.T, c *Card) {
// Check default attempt counters
for _, key := range []byte{keyPIN, keyPUK} {
meta, err := c.Metadata(Slot{Key: key})
require.NoError(t, err)
require.Equal(t, 3, meta.RetriesRemaining)
require.Equal(t, 3, meta.RetriesTotal)
require.True(t, meta.IsDefault)
}

retries := map[byte]int{keyPIN: 5, keyPUK: 10}

// Modify retry counter
err := c.SetRetries(DefaultManagementKey, DefaultPIN, retries[keyPIN], retries[keyPUK])
require.NoError(t, err)

for key, cnt := range retries {
meta, err := c.Metadata(Slot{Key: key})
require.NoError(t, err)
require.Equal(t, cnt, meta.RetriesRemaining)
require.Equal(t, cnt, meta.RetriesTotal)
require.True(t, meta.IsDefault)
}

// Update remaining retries
var aErr AuthError

err = c.VerifyPIN("92837492")
require.ErrorAs(t, err, &aErr)
require.Equal(t, retries[keyPIN]-1, aErr.Retries)

err = c.Unblock("92837492", "12345678")
require.ErrorAs(t, err, &aErr)
require.Equal(t, retries[keyPUK]-1, aErr.Retries)

for key, cnt := range retries {
meta, err := c.Metadata(Slot{Key: key})
require.NoError(t, err)
require.Equal(t, cnt-1, meta.RetriesRemaining)
require.Equal(t, cnt, meta.RetriesTotal)
require.True(t, meta.IsDefault)
}

// Modify PIN/PUK
err = c.SetPIN(DefaultPIN, "981211")
require.NoError(t, err)

err = c.SetPUK(DefaultPUK, "981211")
require.NoError(t, err)

for key, cnt := range retries {
meta, err := c.Metadata(Slot{Key: key})
require.NoError(t, err)
require.Equal(t, cnt, meta.RetriesRemaining)
require.Equal(t, cnt, meta.RetriesTotal)
require.False(t, meta.IsDefault)
}
})
}
74 changes: 40 additions & 34 deletions metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,48 @@ import (

// Metadata holds unprotected metadata about a key slot.
type Metadata struct {
Algorithm Algorithm
PINPolicy PINPolicy
TouchPolicy TouchPolicy
Origin Origin
PublicKey crypto.PublicKey
Algorithm Algorithm
PINPolicy PINPolicy
TouchPolicy TouchPolicy
Origin Origin
PublicKey crypto.PublicKey
RetriesTotal int
RetriesRemaining int
IsDefault bool
}

//nolint:gocognit
func (ki *Metadata) unmarshal(tvs tlv.TagValues) (err error) {
// Algorithm
if v, _, ok := tvs.Get(0x01); ok {
if len(v) != 1 {
return fmt.Errorf("%w for algorithm", errUnexpectedLength)
if v, _, ok := tvs.Get(tagMetadataAlgo); ok {
if l := len(v); l != 1 {
return fmt.Errorf("%w for algorithm: %d", errUnexpectedLength, l)
}

ki.Algorithm = Algorithm(v[0])
}

// PIN & Touch Policy
if v, _, ok := tvs.Get(0x02); ok {
if len(v) != 2 {
return fmt.Errorf("%w for PIN and touch policy", errUnexpectedLength)
if v, _, ok := tvs.Get(tagMetadataPolicy); ok {
if l := len(v); l != 2 {
return fmt.Errorf("%w for PIN and touch policy: %d", errUnexpectedLength, l)
}

if ki.PINPolicy, ok = pinPolicyMapInv[v[0]]; !ok {
return errUnsupportedPinPolicy
if v[0] > 0 { // SlotCardManagement has no PIN policy
return fmt.Errorf("%w: %x", errUnsupportedPinPolicy, v[0])
}
}

if ki.TouchPolicy, ok = touchPolicyMapInv[v[1]]; !ok {
return errUnsupportedTouchPolicy
return fmt.Errorf("%w: %x", errUnsupportedTouchPolicy, v[1])
}
}

// Origin
if v, _, ok := tvs.Get(0x03); ok {
if len(v) != 1 {
return fmt.Errorf("%w for origin", errUnexpectedLength)
if v, _, ok := tvs.Get(tagMetadataOrigin); ok {
if l := len(v); l != 1 {
return fmt.Errorf("%w for origin: %d", errUnexpectedLength, l)
}

if ki.Origin, ok = originMapInv[v[0]]; !ok {
Expand All @@ -57,30 +62,31 @@ func (ki *Metadata) unmarshal(tvs tlv.TagValues) (err error) {
}

// Public Key
if v, _, ok := tvs.Get(0x04); ok {
if v, _, ok := tvs.Get(tagMetadataPublicKey); ok {
ki.PublicKey, err = decodePublic(v, ki.Algorithm)
if err != nil {
return fmt.Errorf("failed to parse public key: %w", err)
}
}

// TODO: According to the Yubico website, we get two more fields,
// if we pass 0x80 or 0x81 as slots:
// 1. Default value (for PIN/PUK and management key): Whether the
// default value is used.
// 2. Retries (for PIN/PUK): The number of retries remaining
// However, it seems the reference implementation does not expect
// these and can not parse them out:
// https://github.com/Yubico/yubico-piv-tool/blob/yubico-piv-tool-2.3.1/lib/util.c#L1529
// For now, we just ignore them.

// Default Value
// if _, v, ok := tvs.Get(0x05); ok {
// }

// Retries
// if _, v, ok := tvs.Get(0x06); ok {
// }
// Has default value
if v, _, ok := tvs.Get(tagMetadataIsDefault); ok {
if l := len(v); l != 1 {
return fmt.Errorf("%w for default value: %d", errUnexpectedLength, l)
}

ki.IsDefault = v[0] != 0
}

// Number of retries left
if v, _, ok := tvs.Get(tagMetadataRetries); ok {
if l := len(v); l != 2 {
return fmt.Errorf("%w for retries: %d", errUnexpectedLength, l)
}

ki.RetriesTotal = int(v[0])
ki.RetriesRemaining = int(v[1])
}

return nil
}
Expand Down
52 changes: 45 additions & 7 deletions metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestMetadata(t *testing.T) {
tests := []struct {
name string
slot Slot
policy Key
key Key
importKey bool
}{
{
Expand Down Expand Up @@ -113,21 +113,22 @@ func TestMetadata(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
withCard(t, true, false, SupportsMetadata, func(t *testing.T, c *Card) {
want := &Metadata{
Algorithm: test.policy.Algorithm,
PINPolicy: test.policy.PINPolicy,
TouchPolicy: test.policy.TouchPolicy,
Algorithm: test.key.Algorithm,
PINPolicy: test.key.PINPolicy,
TouchPolicy: test.key.TouchPolicy,
IsDefault: false,
}

if test.importKey {
key := testKey(t, test.policy.Algorithm.algType(), test.policy.Algorithm.bits())
key := testKey(t, test.key.Algorithm.algType(), test.key.Algorithm.bits())

err := c.SetPrivateKeyInsecure(DefaultManagementKey, test.slot, key, test.policy)
err := c.SetPrivateKeyInsecure(DefaultManagementKey, test.slot, key, test.key)
require.NoError(t, err, "importing key")

want.Origin = OriginImported
want.PublicKey = key.Public()
} else {
pub, err := c.GenerateKey(DefaultManagementKey, test.slot, test.policy)
pub, err := c.GenerateKey(DefaultManagementKey, test.slot, test.key)
require.NoError(t, err, "Failed to generate key")

want.Origin = OriginGenerated
Expand All @@ -141,3 +142,40 @@ func TestMetadata(t *testing.T) {
})
}
}

func TestMetadataPINPUK(t *testing.T) {
for typ, slot := range map[string]Slot{
"PIN": SlotPIN,
"PUK": SlotPUK,
} {
t.Run(typ, func(t *testing.T) {
withCard(t, true, false, SupportsMetadata, func(t *testing.T, c *Card) {
want := &Metadata{
Algorithm: AlgPIN,
RetriesTotal: 3,
RetriesRemaining: 3,
IsDefault: true,
}

// Get default metadata
got, err := c.Metadata(slot)
require.NoError(t, err)
require.Equal(t, want, got)
})
})
}
}

func TestMetadataCardManagement(t *testing.T) {
withCard(t, true, false, SupportsMetadata, func(t *testing.T, c *Card) {
want := &Metadata{
Algorithm: Alg3DES,
TouchPolicy: TouchPolicyNever,
IsDefault: true,
}

got, err := c.Metadata(SlotCardManagement)
require.NoError(t, err)
require.Equal(t, want, got)
})
}
26 changes: 26 additions & 0 deletions mockdata/TestMetadataCardManagement/yk-5.4.3

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions mockdata/TestMetadataCardManagement/yk-5.7.1

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions mockdata/TestMetadataPINPUK/PIN/yk-5.4.3

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading