start TPM support

This commit is contained in:
pascal
2026-09-16 23:00:44 +02:00
parent d89e9c86eb
commit 24f5f2a7ec
22 changed files with 1836 additions and 145 deletions
+336
View File
@@ -0,0 +1,336 @@
//go:build pkcs11 && linux && (amd64 || arm64)
package pkcs11
import (
"encoding/binary"
"errors"
"fmt"
"runtime"
"strings"
"unsafe"
"github.com/ebitengine/purego"
)
// ulong is CK_ULONG, an unsigned long, which is pointer-sized on the 64-bit Linux ABIs
// this file builds for. The struct layouts below assume that width and natural alignment.
type ulong = uintptr
const (
unavailableInformation = ^ulong(0)
flagOSLockingOK = 0x2
flagRWSession = 0x2
flagSerialSession = 0x4
userTypeUser = 0x1
findBatch = 32
)
type version struct {
major byte
minor byte
}
type attribute struct {
typ ulong
value unsafe.Pointer
len ulong
}
type mechanism struct {
typ ulong
parameter unsafe.Pointer
len ulong
}
type pssParams struct {
hashAlg ulong
mgf ulong
saltLen ulong
}
type tokenInfo struct {
label [32]byte
manufacturerID [32]byte
model [16]byte
serialNumber [16]byte
flags ulong
counters [10]ulong
hardware version
firmware version
utcTime [16]byte
}
type initializeArgs struct {
createMutex uintptr
destroyMutex uintptr
lockMutex uintptr
unlockMutex uintptr
flags ulong
reserved unsafe.Pointer
}
// functionList mirrors CK_FUNCTION_LIST: a CK_VERSION padded to pointer alignment, then
// the PKCS#11 v2.40 entry points in specification order.
type functionList struct {
version version
_ [6]byte
fn [68]uintptr
}
const (
fnInitialize = 0
fnGetSlotList = 4
fnGetTokenInfo = 6
fnOpenSession = 12
fnCloseSession = 13
fnLogin = 18
fnLogout = 19
fnCreateObject = 20
fnGetAttributeValue = 24
fnFindObjectsInit = 26
fnFindObjects = 27
fnFindObjectsFinal = 28
fnSignInit = 42
fnSign = 43
)
// module holds the entry points of one loaded library, bound straight from its
// CK_FUNCTION_LIST.
type module struct {
cInitialize func(args *initializeArgs) ulong
cGetSlotList func(tokenPresent byte, slots *ulong, count *ulong) ulong
cGetTokenInfo func(slot ulong, info *tokenInfo) ulong
cOpenSession func(slot ulong, flags ulong, application unsafe.Pointer, notify uintptr, session *ulong) ulong
cCloseSession func(session ulong) ulong
cLogin func(session ulong, userType ulong, pin *byte, pinLen ulong) ulong
cLogout func(session ulong) ulong
cCreateObject func(session ulong, template *attribute, count ulong, object *ulong) ulong
cGetAttributeValue func(session ulong, object ulong, template *attribute, count ulong) ulong
cFindObjectsInit func(session ulong, template *attribute, count ulong) ulong
cFindObjects func(session ulong, objects *ulong, max ulong, count *ulong) ulong
cFindObjectsFinal func(session ulong) ulong
cSignInit func(session ulong, mech *mechanism, key ulong) ulong
cSign func(session ulong, data *byte, dataLen ulong, signature *byte, signatureLen *ulong) ulong
}
func load(path string) (driver, error) {
lib, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_LOCAL)
if err != nil {
return nil, fmt.Errorf("open PKCS#11 module %s: %w", path, err)
}
symbol, err := purego.Dlsym(lib, "C_GetFunctionList")
if err != nil {
return nil, fmt.Errorf("%s is not a PKCS#11 module: %w", path, err)
}
var getFunctionList func(list **functionList) ulong
purego.RegisterFunc(&getFunctionList, symbol)
var list *functionList
if rv := getFunctionList(&list); rv != rvOK || list == nil {
return nil, Error{Op: "C_GetFunctionList", Code: uint(rv)}
}
m := &module{}
for _, entry := range []struct {
fn any
index int
}{
{&m.cInitialize, fnInitialize},
{&m.cGetSlotList, fnGetSlotList},
{&m.cGetTokenInfo, fnGetTokenInfo},
{&m.cOpenSession, fnOpenSession},
{&m.cCloseSession, fnCloseSession},
{&m.cLogin, fnLogin},
{&m.cLogout, fnLogout},
{&m.cCreateObject, fnCreateObject},
{&m.cGetAttributeValue, fnGetAttributeValue},
{&m.cFindObjectsInit, fnFindObjectsInit},
{&m.cFindObjects, fnFindObjects},
{&m.cFindObjectsFinal, fnFindObjectsFinal},
{&m.cSignInit, fnSignInit},
{&m.cSign, fnSign},
} {
if list.fn[entry.index] == 0 {
return nil, fmt.Errorf("%s lacks PKCS#11 entry point %d", path, entry.index)
}
purego.RegisterFunc(entry.fn, list.fn[entry.index])
}
args := &initializeArgs{flags: flagOSLockingOK}
if rv := m.cInitialize(args); rv != rvOK && rv != rvAlreadyInitialized {
return nil, Error{Op: "C_Initialize", Code: uint(rv)}
}
return m, nil
}
func (m *module) tokens() ([]Token, error) {
var count ulong
if rv := m.cGetSlotList(1, nil, &count); rv != rvOK {
return nil, Error{Op: "C_GetSlotList", Code: uint(rv)}
}
if count == 0 {
return nil, nil
}
slots := make([]ulong, count)
if rv := m.cGetSlotList(1, &slots[0], &count); rv != rvOK {
return nil, Error{Op: "C_GetSlotList", Code: uint(rv)}
}
tokens := make([]Token, 0, count)
for _, slot := range slots[:count] {
var info tokenInfo
if rv := m.cGetTokenInfo(slot, &info); rv != rvOK {
continue
}
tokens = append(tokens, Token{Slot: uint(slot), Label: strings.TrimRight(string(info.label[:]), " \x00")})
}
return tokens, nil
}
func (m *module) openSession(slot uint, readWrite bool) (uint, error) {
flags := ulong(flagSerialSession)
if readWrite {
flags |= flagRWSession
}
var session ulong
if rv := m.cOpenSession(ulong(slot), flags, nil, 0, &session); rv != rvOK {
return 0, Error{Op: "C_OpenSession", Code: uint(rv)}
}
return uint(session), nil
}
func (m *module) closeSession(session uint) {
m.cCloseSession(ulong(session))
}
func (m *module) login(session uint, pin []byte) error {
var pinPtr *byte
if len(pin) > 0 {
pinPtr = &pin[0]
}
rv := m.cLogin(ulong(session), userTypeUser, pinPtr, ulong(len(pin)))
runtime.KeepAlive(pin)
if rv != rvOK && rv != rvUserAlreadyLoggedIn {
return Error{Op: "C_Login", Code: uint(rv)}
}
return nil
}
func (m *module) logout(session uint) {
m.cLogout(ulong(session))
}
func (m *module) findObjects(session uint, template []Attribute) ([]Object, error) {
attrs := toAttributes(template)
rv := m.cFindObjectsInit(ulong(session), first(attrs), ulong(len(attrs)))
runtime.KeepAlive(template)
if rv != rvOK {
return nil, Error{Op: "C_FindObjectsInit", Code: uint(rv)}
}
defer m.cFindObjectsFinal(ulong(session))
var objects []Object
for {
var batch [findBatch]ulong
var count ulong
if rv := m.cFindObjects(ulong(session), &batch[0], findBatch, &count); rv != rvOK {
return nil, Error{Op: "C_FindObjects", Code: uint(rv)}
}
for _, handle := range batch[:count] {
objects = append(objects, Object(handle))
}
if count < findBatch {
return objects, nil
}
}
}
func (m *module) attribute(session uint, obj Object, typ uint) ([]byte, error) {
attr := attribute{typ: ulong(typ)}
if rv := m.cGetAttributeValue(ulong(session), ulong(obj), &attr, 1); rv != rvOK {
return nil, Error{Op: "C_GetAttributeValue", Code: uint(rv)}
}
if attr.len == unavailableInformation {
return nil, fmt.Errorf("attribute 0x%x is unavailable", typ)
}
if attr.len == 0 {
return nil, nil
}
value := make([]byte, attr.len)
attr.value = unsafe.Pointer(&value[0])
rv := m.cGetAttributeValue(ulong(session), ulong(obj), &attr, 1)
runtime.KeepAlive(value)
if rv != rvOK {
return nil, Error{Op: "C_GetAttributeValue", Code: uint(rv)}
}
return value[:attr.len], nil
}
func (m *module) sign(session uint, mech Mechanism, key Object, data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, errors.New("nothing to sign")
}
native := mechanism{typ: ulong(mech.Type)}
var params *pssParams
if mech.PSS != nil {
params = &pssParams{hashAlg: ulong(mech.PSS.Hash), mgf: ulong(mech.PSS.MGF), saltLen: ulong(mech.PSS.SaltLen)}
native.parameter = unsafe.Pointer(params)
native.len = ulong(unsafe.Sizeof(*params))
}
rv := m.cSignInit(ulong(session), &native, ulong(key))
runtime.KeepAlive(params)
if rv != rvOK {
return nil, Error{Op: "C_SignInit", Code: uint(rv)}
}
var size ulong
if rv := m.cSign(ulong(session), &data[0], ulong(len(data)), nil, &size); rv != rvOK {
return nil, Error{Op: "C_Sign", Code: uint(rv)}
}
signature := make([]byte, size)
rv = m.cSign(ulong(session), &data[0], ulong(len(data)), &signature[0], &size)
runtime.KeepAlive(data)
if rv != rvOK {
return nil, Error{Op: "C_Sign", Code: uint(rv)}
}
return signature[:size], nil
}
func (m *module) createObject(session uint, template []Attribute) (Object, error) {
attrs := toAttributes(template)
var object ulong
rv := m.cCreateObject(ulong(session), first(attrs), ulong(len(attrs)), &object)
runtime.KeepAlive(template)
if rv != rvOK {
return 0, Error{Op: "C_CreateObject", Code: uint(rv)}
}
return Object(object), nil
}
func toAttributes(template []Attribute) []attribute {
attrs := make([]attribute, len(template))
for i, a := range template {
attrs[i].typ = ulong(a.Type)
if len(a.Value) > 0 {
attrs[i].value = unsafe.Pointer(&a.Value[0])
attrs[i].len = ulong(len(a.Value))
}
}
return attrs
}
func first(attrs []attribute) *attribute {
if len(attrs) == 0 {
return nil
}
return &attrs[0]
}
// ULong encodes an integer attribute value the way the module reads a CK_ULONG.
func ULong(v uint) []byte {
buf := make([]byte, unsafe.Sizeof(ulong(0)))
binary.NativeEndian.PutUint64(buf, uint64(v))
return buf
}
@@ -0,0 +1,72 @@
//go:build pkcs11 && linux && (amd64 || arm64)
package pkcs11
import (
"crypto/x509"
"os"
"testing"
"unsafe"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStructLayoutsMatchTheCABI(t *testing.T) {
assert.Equal(t, uintptr(24), unsafe.Sizeof(attribute{}), "CK_ATTRIBUTE")
assert.Equal(t, uintptr(24), unsafe.Sizeof(mechanism{}), "CK_MECHANISM")
assert.Equal(t, uintptr(24), unsafe.Sizeof(pssParams{}), "CK_RSA_PKCS_PSS_PARAMS")
assert.Equal(t, uintptr(208), unsafe.Sizeof(tokenInfo{}), "CK_TOKEN_INFO")
assert.Equal(t, uintptr(48), unsafe.Sizeof(initializeArgs{}), "CK_C_INITIALIZE_ARGS")
assert.Equal(t, uintptr(8), unsafe.Offsetof(functionList{}.fn), "entry points follow the padded CK_VERSION")
assert.Equal(t, uintptr(8+68*8), unsafe.Sizeof(functionList{}), "CK_FUNCTION_LIST v2.40")
}
// TestTrustModule_ListsSystemCertificates drives a real module through the binding:
// p11-kit's trust module exposes the system CA store as certificate objects with no login.
func TestTrustModule_ListsSystemCertificates(t *testing.T) {
module := loadFirst(t,
"/usr/lib/pkcs11/p11-kit-trust.so",
"/usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-trust.so",
"/usr/lib/aarch64-linux-gnu/pkcs11/p11-kit-trust.so",
"/usr/lib64/pkcs11/p11-kit-trust.so",
)
tokens, err := module.Tokens()
require.NoError(t, err)
require.NotEmpty(t, tokens, "the trust module must present at least one token")
parsed := 0
for _, token := range tokens {
session, err := module.OpenSession(token.Label, nil)
require.NoError(t, err, token.Label)
objects, err := session.FindObjects(
Attribute{Type: AttrClass, Value: ULong(ClassCertificate)},
Attribute{Type: AttrCertificateType, Value: ULong(CertificateX509)},
)
require.NoError(t, err, token.Label)
for _, object := range objects {
der, err := session.Attribute(object, AttrValue)
require.NoError(t, err)
_, err = x509.ParseCertificate(der)
require.NoError(t, err, "CKA_VALUE must be a DER certificate")
parsed++
}
session.Close()
}
assert.Positive(t, parsed, "system trust anchors must be readable through the binding")
}
func loadFirst(t *testing.T, paths ...string) *Module {
t.Helper()
for _, path := range paths {
if _, err := os.Stat(path); err != nil {
continue
}
module, err := Load(path)
require.NoError(t, err, path)
return module
}
t.Skip("p11-kit trust module not installed")
return nil
}
+14
View File
@@ -0,0 +1,14 @@
//go:build !(pkcs11 && linux && (amd64 || arm64))
package pkcs11
import "encoding/binary"
func load(string) (driver, error) {
return nil, ErrUnsupported
}
// ULong encodes an integer attribute value; no module ever reads it in this build.
func ULong(v uint) []byte {
return binary.NativeEndian.AppendUint64(nil, uint64(v))
}
+235
View File
@@ -0,0 +1,235 @@
// Package pkcs11 is a minimal PKCS#11 client. It loads a module at runtime without cgo,
// opens a token session, lists objects and signs with keys the token holds. It exists so
// certificates whose keys live in a TPM behind tpm2-pkcs11 can be proven; whatever a
// certificate store does not need is left out.
package pkcs11
import (
"errors"
"fmt"
"sync"
)
// Object classes, attribute types, mechanisms and generators from PKCS#11 v2.40.
const (
ClassCertificate = 0x1
ClassPublicKey = 0x2
ClassPrivateKey = 0x3
CertificateX509 = 0x0
AttrClass = 0x0
AttrToken = 0x1
AttrLabel = 0x3
AttrValue = 0x11
AttrCertificateType = 0x80
AttrSubject = 0x101
AttrID = 0x102
MechRSAPKCSPSS = 0xd
MechSHA256 = 0x250
MechSHA384 = 0x260
MechECDSA = 0x1041
MGF1SHA256 = 0x2
MGF1SHA384 = 0x3
rvOK = 0x0
rvUserAlreadyLoggedIn = 0x100
rvAlreadyInitialized = 0x191
)
var ErrUnsupported = errors.New("PKCS#11 modules need a build with the pkcs11 tag on linux/amd64 or linux/arm64")
// Error is a PKCS#11 return value other than CKR_OK.
type Error struct {
Op string
Code uint
}
func (e Error) Error() string {
if name, ok := returnValueNames[e.Code]; ok {
return fmt.Sprintf("%s: %s", e.Op, name)
}
return fmt.Sprintf("%s: CKR 0x%x", e.Op, e.Code)
}
var returnValueNames = map[uint]string{
0x2: "CKR_HOST_MEMORY",
0x3: "CKR_SLOT_ID_INVALID",
0x5: "CKR_GENERAL_ERROR",
0x7: "CKR_ARGUMENTS_BAD",
0x12: "CKR_ATTRIBUTE_TYPE_INVALID",
0x13: "CKR_ATTRIBUTE_VALUE_INVALID",
0x30: "CKR_DEVICE_ERROR",
0x54: "CKR_FUNCTION_NOT_SUPPORTED",
0x68: "CKR_KEY_FUNCTION_NOT_PERMITTED",
0x70: "CKR_MECHANISM_INVALID",
0x71: "CKR_MECHANISM_PARAM_INVALID",
0x82: "CKR_OBJECT_HANDLE_INVALID",
0xa0: "CKR_PIN_INCORRECT",
0xa4: "CKR_PIN_LOCKED",
0xb3: "CKR_SESSION_HANDLE_INVALID",
0xd0: "CKR_TEMPLATE_INCOMPLETE",
0xd1: "CKR_TEMPLATE_INCONSISTENT",
0xe0: "CKR_TOKEN_NOT_PRESENT",
0x101: "CKR_USER_NOT_LOGGED_IN",
0x150: "CKR_BUFFER_TOO_SMALL",
0x190: "CKR_CRYPTOKI_NOT_INITIALIZED",
}
// Attribute is one entry of a PKCS#11 template. Integer-valued attributes such as the
// object class are encoded with ULong.
type Attribute struct {
Type uint
Value []byte
}
// Mechanism selects a signing algorithm. PSS carries the parameters CKM_RSA_PKCS_PSS needs.
type Mechanism struct {
Type uint
PSS *PSSParams
}
type PSSParams struct {
Hash uint
MGF uint
SaltLen uint
}
// Object is a handle the token issued for one of its objects.
type Object uint
// Token is a slot with a token present.
type Token struct {
Slot uint
Label string
}
// Module is a loaded and initialised PKCS#11 library. A module is loaded once per path
// and never finalised: tokens such as tpm2-pkcs11 do real work in C_Initialize, and the
// process exit releases everything anyway.
type Module struct {
d driver
}
var (
modulesMu sync.Mutex
modules = map[string]*Module{}
)
// Load opens the shared library at path and initialises it, or returns the module already
// loaded from that path.
func Load(path string) (*Module, error) {
modulesMu.Lock()
defer modulesMu.Unlock()
if m, ok := modules[path]; ok {
return m, nil
}
d, err := load(path)
if err != nil {
return nil, err
}
m := &Module{d: d}
modules[path] = m
return m, nil
}
func (m *Module) Tokens() ([]Token, error) {
return m.d.tokens()
}
// OpenSession opens a read-only session with the token carrying label, or with the first
// token when label is empty, and logs in as the user when pin is not nil. An empty,
// non-nil pin still logs in.
func (m *Module) OpenSession(label string, pin []byte) (*Session, error) {
return m.openSession(label, pin, false)
}
// OpenReadWriteSession is OpenSession for callers that create objects on the token.
func (m *Module) OpenReadWriteSession(label string, pin []byte) (*Session, error) {
return m.openSession(label, pin, true)
}
func (m *Module) openSession(label string, pin []byte, readWrite bool) (*Session, error) {
token, err := m.token(label)
if err != nil {
return nil, err
}
handle, err := m.d.openSession(token.Slot, readWrite)
if err != nil {
return nil, err
}
s := &Session{d: m.d, handle: handle}
if pin == nil {
return s, nil
}
if err := m.d.login(handle, pin); err != nil {
s.Close()
return nil, err
}
s.loggedIn = true
return s, nil
}
func (m *Module) token(label string) (Token, error) {
tokens, err := m.Tokens()
if err != nil {
return Token{}, err
}
for _, token := range tokens {
if label == "" || token.Label == label {
return token, nil
}
}
if label == "" {
return Token{}, errors.New("no token present")
}
return Token{}, fmt.Errorf("no token labelled %q among %d tokens", label, len(tokens))
}
// Session is an open session with one token. Close logs out again if the session logged in.
type Session struct {
d driver
handle uint
loggedIn bool
}
func (s *Session) Close() {
if s.loggedIn {
s.d.logout(s.handle)
}
s.d.closeSession(s.handle)
}
// FindObjects returns the handles of every object matching all attributes of template.
func (s *Session) FindObjects(template ...Attribute) ([]Object, error) {
return s.d.findObjects(s.handle, template)
}
// Attribute reads one attribute of an object.
func (s *Session) Attribute(obj Object, typ uint) ([]byte, error) {
return s.d.attribute(s.handle, obj, typ)
}
// Sign signs data, normally a digest, with the token-held key in a single operation.
func (s *Session) Sign(mech Mechanism, key Object, data []byte) ([]byte, error) {
return s.d.sign(s.handle, mech, key, data)
}
// CreateObject stores a new object described by template on the token.
func (s *Session) CreateObject(template ...Attribute) (Object, error) {
return s.d.createObject(s.handle, template)
}
type driver interface {
tokens() ([]Token, error)
openSession(slot uint, readWrite bool) (uint, error)
closeSession(session uint)
login(session uint, pin []byte) error
logout(session uint)
findObjects(session uint, template []Attribute) ([]Object, error)
attribute(session uint, obj Object, typ uint) ([]byte, error)
sign(session uint, mech Mechanism, key Object, data []byte) ([]byte, error)
createObject(session uint, template []Attribute) (Object, error)
}
+98
View File
@@ -0,0 +1,98 @@
package pkcs11
import (
"errors"
"fmt"
"net/url"
"os"
"strings"
)
// DefaultModule is p11-kit's proxy, which exposes every module the system has registered,
// tpm2-pkcs11 included, so a URI without module-path works on a stock p11-kit setup.
const DefaultModule = "p11-kit-proxy.so"
// URI is the subset of an RFC 7512 PKCS#11 URI this client understands: the token label,
// the module to load and where the user PIN comes from. Unknown attributes are ignored.
type URI struct {
Token string
ModulePath string
pinValue *string
pinSource string
}
func ParseURI(raw string) (*URI, error) {
rest, ok := strings.CutPrefix(raw, "pkcs11:")
if !ok {
return nil, errors.New("PKCS#11 URI must start with pkcs11:")
}
path, query, _ := strings.Cut(rest, "?")
u := &URI{}
if err := eachAttribute(path, ";", func(name, value string) {
if name == "token" {
u.Token = value
}
}); err != nil {
return nil, err
}
err := eachAttribute(query, "&", func(name, value string) {
switch name {
case "module-path":
u.ModulePath = value
case "module-name":
u.ModulePath = "lib" + value + ".so"
case "pin-value":
u.pinValue = &value
case "pin-source":
u.pinSource = value
}
})
if err != nil {
return nil, err
}
return u, nil
}
func eachAttribute(list, sep string, fn func(name, value string)) error {
if list == "" {
return nil
}
for _, pair := range strings.Split(list, sep) {
name, value, ok := strings.Cut(pair, "=")
if !ok {
return fmt.Errorf("PKCS#11 URI attribute %q has no value", pair)
}
value, err := url.PathUnescape(value)
if err != nil {
return fmt.Errorf("PKCS#11 URI attribute %s: %w", name, err)
}
fn(name, value)
}
return nil
}
// Module is the library to load, DefaultModule when the URI names none.
func (u *URI) Module() string {
if u.ModulePath == "" {
return DefaultModule
}
return u.ModulePath
}
// PIN returns the user PIN, or nil when the URI carries none and no login should happen.
// A pin-source names a file whose single line is the PIN.
func (u *URI) PIN() ([]byte, error) {
if u.pinValue != nil {
return []byte(*u.pinValue), nil
}
if u.pinSource == "" {
return nil, nil
}
path := strings.TrimPrefix(strings.TrimPrefix(u.pinSource, "file://"), "file:")
pin, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read PIN: %w", err)
}
return []byte(strings.TrimRight(string(pin), "\r\n")), nil
}
+87
View File
@@ -0,0 +1,87 @@
package pkcs11
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseURI(t *testing.T) {
tests := []struct {
name string
raw string
wantToken string
wantModule string
wantPIN []byte
}{
{
name: "token with module path and pin value",
raw: "pkcs11:token=netbird?module-path=/usr/lib/libtpm2_pkcs11.so&pin-value=1234",
wantToken: "netbird",
wantModule: "/usr/lib/libtpm2_pkcs11.so",
wantPIN: []byte("1234"),
},
{
name: "module name becomes a library file",
raw: "pkcs11:token=netbird?module-name=tpm2_pkcs11",
wantToken: "netbird",
wantModule: "libtpm2_pkcs11.so",
},
{
name: "percent encoding and unknown attributes",
raw: "pkcs11:model=SoftHSM%20v2;token=my%20token;serial=1?max-sessions=1",
wantToken: "my token",
wantModule: DefaultModule,
},
{
name: "bare scheme uses the p11-kit proxy and no login",
raw: "pkcs11:",
wantModule: DefaultModule,
},
{
name: "empty pin value still logs in",
raw: "pkcs11:?pin-value=",
wantModule: DefaultModule,
wantPIN: []byte{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
uri, err := ParseURI(tt.raw)
require.NoError(t, err)
assert.Equal(t, tt.wantToken, uri.Token, "token label")
assert.Equal(t, tt.wantModule, uri.Module(), "module to load")
pin, err := uri.PIN()
require.NoError(t, err)
assert.Equal(t, tt.wantPIN, pin, "PIN, nil meaning no login")
})
}
}
func TestParseURI_Rejections(t *testing.T) {
for _, raw := range []string{"pkcs11", "https://example.com", "pkcs11:token", "pkcs11:token=%zz"} {
_, err := ParseURI(raw)
assert.Error(t, err, raw)
}
}
func TestURI_PINFromFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "pin")
require.NoError(t, os.WriteFile(path, []byte("secret\n"), 0o600))
for _, source := range []string{path, "file:" + path, "file://" + path} {
uri, err := ParseURI("pkcs11:token=netbird?pin-source=" + source)
require.NoError(t, err)
pin, err := uri.PIN()
require.NoError(t, err)
assert.Equal(t, []byte("secret"), pin, "PIN from %s must drop the trailing newline", source)
}
uri, err := ParseURI("pkcs11:?pin-source=" + filepath.Join(t.TempDir(), "missing"))
require.NoError(t, err)
_, err = uri.PIN()
assert.Error(t, err, "a missing PIN file must fail loudly instead of logging in without a PIN")
}