mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-23 05:49:07 +02:00
start TPM support
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user