Stop retrying a PKCS#11 PIN the token rejected

This commit is contained in:
Viktor Liu
2026-10-01 08:33:20 +02:00
parent 83e7bf8ab4
commit a230d39aa0
5 changed files with 131 additions and 1 deletions
+47
View File
@@ -0,0 +1,47 @@
package certproof
import (
"crypto/sha256"
"encoding/binary"
"errors"
"sync"
)
// errPINRejectedBefore is returned instead of logging in with a PIN the token already
// refused: every failed login counts towards the token's lockout, which for a TPM is
// shared with everything else on the machine, and proofs are collected on every sync.
var errPINRejectedBefore = errors.New("PKCS#11 token rejected this PIN before, not trying it again")
// rejectedPINs outlives a single store, since a store is built for each collection.
var rejectedPINs = &pinLatch{keys: map[[sha256.Size]byte]struct{}{}}
// pinLatch remembers PINs a token rejected. Keys are hashes, so the PIN itself is not
// kept in memory any longer than the store that read it.
type pinLatch struct {
mu sync.Mutex
keys map[[sha256.Size]byte]struct{}
}
func (l *pinLatch) has(key [sha256.Size]byte) bool {
l.mu.Lock()
defer l.mu.Unlock()
_, ok := l.keys[key]
return ok
}
func (l *pinLatch) add(key [sha256.Size]byte) {
l.mu.Lock()
defer l.mu.Unlock()
l.keys[key] = struct{}{}
}
// rejectedPINKey identifies a PIN for one token of one module, so a PIN another token
// rejected is still tried on the token it belongs to.
func rejectedPINKey(module, token string, pin []byte) [sha256.Size]byte {
var buf []byte
for _, part := range [][]byte{[]byte(module), []byte(token), pin} {
buf = binary.BigEndian.AppendUint64(buf, uint64(len(part)))
buf = append(buf, part...)
}
return sha256.Sum256(buf)
}
@@ -0,0 +1,29 @@
package certproof
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRejectedPINKey_ScopesToModuleTokenAndPIN(t *testing.T) {
base := rejectedPINKey("/lib/a.so", "netbird", []byte("1234"))
assert.Equal(t, base, rejectedPINKey("/lib/a.so", "netbird", []byte("1234")), "the same PIN on the same token is the same key")
assert.NotEqual(t, base, rejectedPINKey("/lib/a.so", "netbird", []byte("4321")), "a corrected PIN must be tried")
assert.NotEqual(t, base, rejectedPINKey("/lib/a.so", "piv", []byte("1234")), "a PIN rejected by one token is still tried on another")
assert.NotEqual(t, base, rejectedPINKey("/lib/b.so", "netbird", []byte("1234")), "a PIN rejected through one module is still tried through another")
assert.NotEqual(t, rejectedPINKey("ab", "", []byte("1")), rejectedPINKey("a", "b", []byte("1")),
"field boundaries are part of the key, so shifted fields do not collide")
}
func TestPINLatch_RemembersRejectedKeys(t *testing.T) {
latch := &pinLatch{keys: map[[32]byte]struct{}{}}
rejected := rejectedPINKey("/lib/a.so", "netbird", []byte("0000"))
other := rejectedPINKey("/lib/a.so", "netbird", []byte("1234"))
assert.False(t, latch.has(rejected), "nothing is rejected before a login fails")
latch.add(rejected)
assert.True(t, latch.has(rejected), "a rejected PIN is not tried again")
assert.False(t, latch.has(other), "other PINs are unaffected")
}
+14 -1
View File
@@ -193,7 +193,20 @@ func (s *PKCS11Store) open() (*pkcs11.Session, error) {
if err != nil {
return nil, err
}
return module.OpenSession(s.uri.Token, pin)
if pin == nil {
return module.OpenSession(s.uri.Token, nil)
}
key := rejectedPINKey(s.uri.Module(), s.uri.Token, pin)
if rejectedPINs.has(key) {
return nil, errPINRejectedBefore
}
session, err := module.OpenSession(s.uri.Token, pin)
if pkcs11.PINRejected(err) {
rejectedPINs.add(key)
return nil, fmt.Errorf("%s rejected the PIN, not retrying it until the daemon restarts: %w", s, err)
}
return session, err
}
func (s *PKCS11Store) userPIN() ([]byte, error) {