A Kubernetes operator that bridges Hardware Security Module (HSM) data storage with Kubernetes Secrets, providing true secret portability th
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor cgo code

+705 -651
+6
.github/workflows/ci.yml
··· 23 23 with: 24 24 go-version-file: go.mod 25 25 26 + # Install system dependencies for CGO builds (libudev for USB discovery) 27 + - name: Install libudev 28 + run: | 29 + sudo apt-get update 30 + sudo apt-get install -y libudev-dev 31 + 26 32 # Run linting first (fast feedback) 27 33 - name: Go Lint 28 34 uses: golangci/golangci-lint-action@v8
+6
.github/workflows/e2e-tests.yml
··· 23 23 with: 24 24 go-version-file: go.mod 25 25 26 + # Install system dependencies for CGO builds (libudev for USB discovery) 27 + - name: Install libudev 28 + run: | 29 + sudo apt-get update 30 + sudo apt-get install -y libudev-dev 31 + 26 32 - name: Install Kind 27 33 run: | 28 34 curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
+2 -2
Makefile
··· 136 136 137 137 .PHONY: manifests 138 138 manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 139 - CGO_ENABLED=0 $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 139 + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 140 140 141 141 .PHONY: helm-sync 142 142 helm-sync: manifests ## Sync generated CRDs from config/ to helm/crds/ ··· 147 147 148 148 .PHONY: generate 149 149 generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 150 - CGO_ENABLED=0 $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 150 + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 151 151 152 152 .PHONY: fmt 153 153 fmt: ## Run go fmt against code.
+5 -82
internal/discovery/usb.go
··· 1 - //go:build cgo 2 - // +build cgo 3 - 4 1 /* 5 2 Copyright 2025. 6 3 ··· 22 19 import ( 23 20 "context" 24 21 "fmt" 22 + "maps" 25 23 "strings" 26 24 "time" 27 25 28 26 "github.com/go-logr/logr" 29 - "github.com/jochenvg/go-udev" 30 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 28 ctrl "sigs.k8s.io/controller-runtime" 32 29 ··· 55 52 // USBDiscoverer handles USB device discovery and monitoring 56 53 type USBDiscoverer struct { 57 54 logger logr.Logger 58 - udev *udev.Udev 55 + udev *Udev 59 56 60 57 // Event monitoring 61 - monitor *udev.Monitor 58 + monitor *Monitor 62 59 eventChannel chan USBEvent 63 60 activeSpecs map[string]*hsmv1alpha1.USBDeviceSpec 64 61 } ··· 67 64 func NewUSBDiscoverer() *USBDiscoverer { 68 65 logger := ctrl.Log.WithName("usb-discoverer") 69 66 70 - udev := &udev.Udev{} 67 + udev := &Udev{} 71 68 72 69 return &USBDiscoverer{ 73 70 logger: logger, ··· 129 126 return matchingDevices, nil 130 127 } 131 128 132 - // convertUdevDevice converts a go-udev Device to our USBDevice format 133 - func (u *USBDiscoverer) convertUdevDevice(device *udev.Device) *USBDevice { 134 - // Only process actual USB devices, not interfaces 135 - if device.PropertyValue("DEVTYPE") != "usb_device" { 136 - return nil 137 - } 138 - 139 - vendorID := device.PropertyValue("ID_VENDOR_ID") 140 - productID := device.PropertyValue("ID_MODEL_ID") 141 - 142 - // Skip devices without vendor/product IDs 143 - if vendorID == "" || productID == "" { 144 - return nil 145 - } 146 - 147 - return &USBDevice{ 148 - VendorID: vendorID, 149 - ProductID: productID, 150 - SerialNumber: device.PropertyValue("ID_SERIAL_SHORT"), 151 - DevicePath: device.Devnode(), 152 - Manufacturer: device.PropertyValue("ID_VENDOR"), 153 - Product: device.PropertyValue("ID_MODEL"), 154 - DeviceInfo: device.Properties(), 155 - } 156 - } 157 - 158 129 // matchesSpec checks if a USB device matches the given specification 159 130 func (u *USBDiscoverer) matchesSpec(device USBDevice, spec *hsmv1alpha1.USBDeviceSpec) bool { 160 131 // Check vendor ID ··· 241 212 } 242 213 } 243 214 244 - // handleDeviceEvent processes a single USB device event 245 - func (u *USBDiscoverer) handleDeviceEvent(device *udev.Device) { 246 - action := device.Action() 247 - 248 - // Only process add/remove events 249 - if action != "add" && action != "remove" { 250 - return 251 - } 252 - 253 - // Convert to USBDevice 254 - usbDev := u.convertUdevDevice(device) 255 - if usbDev == nil { 256 - return 257 - } 258 - 259 - u.logger.V(2).Info("Received USB event", 260 - "action", action, 261 - "vendor", usbDev.VendorID, 262 - "product", usbDev.ProductID, 263 - "serial", usbDev.SerialNumber) 264 - 265 - // Check which active specs match this device 266 - for hsmDeviceName, spec := range u.activeSpecs { 267 - if u.matchesSpec(*usbDev, spec) { 268 - event := USBEvent{ 269 - Action: action, 270 - Device: *usbDev, 271 - Timestamp: time.Now(), 272 - HSMDeviceName: hsmDeviceName, 273 - } 274 - 275 - select { 276 - case u.eventChannel <- event: 277 - u.logger.V(1).Info("Sent USB event", 278 - "action", action, 279 - "device", hsmDeviceName, 280 - "vendor", usbDev.VendorID, 281 - "product", usbDev.ProductID, 282 - "serial", usbDev.SerialNumber) 283 - default: 284 - u.logger.Error(nil, "USB event channel full, dropping event", "action", action) 285 - } 286 - } 287 - } 288 - } 289 - 290 215 // AddSpecForMonitoring registers an HSMDevice spec for event monitoring 291 216 func (u *USBDiscoverer) AddSpecForMonitoring(hsmDeviceName string, spec *hsmv1alpha1.USBDeviceSpec) { 292 217 u.activeSpecs[hsmDeviceName] = spec ··· 337 262 } 338 263 339 264 // Add additional device info 340 - for k, v := range usbDevice.DeviceInfo { 341 - device.DeviceInfo[k] = v 342 - } 265 + maps.Copy(device.DeviceInfo, usbDevice.DeviceInfo) 343 266 344 267 return device 345 268 }
+102
internal/discovery/usb_cgo.go
··· 1 + //go:build cgo 2 + 3 + /* 4 + Copyright 2025. 5 + 6 + Licensed under the Apache License, Version 2.0 (the "License"); 7 + you may not use this file except in compliance with the License. 8 + You may obtain a copy of the License at 9 + 10 + http://www.apache.org/licenses/LICENSE-2.0 11 + 12 + Unless required by applicable law or agreed to in writing, software 13 + distributed under the License is distributed on an "AS IS" BASIS, 14 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + See the License for the specific language governing permissions and 16 + limitations under the License. 17 + */ 18 + 19 + package discovery 20 + 21 + import ( 22 + "time" 23 + 24 + "github.com/jochenvg/go-udev" 25 + ) 26 + 27 + type Udev = udev.Udev 28 + type Monitor = udev.Monitor 29 + type UDevDevice = udev.Device 30 + type Enumerate = udev.Enumerate 31 + 32 + // convertUdevDevice converts a go-udev Device to our USBDevice format 33 + func (u *USBDiscoverer) convertUdevDevice(device *UDevDevice) *USBDevice { 34 + // Only process actual USB devices, not interfaces 35 + if device.PropertyValue("DEVTYPE") != "usb_device" { 36 + return nil 37 + } 38 + 39 + vendorID := device.PropertyValue("ID_VENDOR_ID") 40 + productID := device.PropertyValue("ID_MODEL_ID") 41 + 42 + // Skip devices without vendor/product IDs 43 + if vendorID == "" || productID == "" { 44 + return nil 45 + } 46 + 47 + return &USBDevice{ 48 + VendorID: vendorID, 49 + ProductID: productID, 50 + SerialNumber: device.PropertyValue("ID_SERIAL_SHORT"), 51 + DevicePath: device.Devnode(), 52 + Manufacturer: device.PropertyValue("ID_VENDOR"), 53 + Product: device.PropertyValue("ID_MODEL"), 54 + DeviceInfo: device.Properties(), 55 + } 56 + } 57 + 58 + // handleDeviceEvent processes a single USB device event 59 + func (u *USBDiscoverer) handleDeviceEvent(device *UDevDevice) { 60 + action := device.Action() 61 + 62 + // Only process add/remove events 63 + if action != "add" && action != "remove" { 64 + return 65 + } 66 + 67 + // Convert to USBDevice 68 + usbDev := u.convertUdevDevice(device) 69 + if usbDev == nil { 70 + return 71 + } 72 + 73 + u.logger.V(2).Info("Received USB event", 74 + "action", action, 75 + "vendor", usbDev.VendorID, 76 + "product", usbDev.ProductID, 77 + "serial", usbDev.SerialNumber) 78 + 79 + // Check which active specs match this device 80 + for hsmDeviceName, spec := range u.activeSpecs { 81 + if u.matchesSpec(*usbDev, spec) { 82 + event := USBEvent{ 83 + Action: action, 84 + Device: *usbDev, 85 + Timestamp: time.Now(), 86 + HSMDeviceName: hsmDeviceName, 87 + } 88 + 89 + select { 90 + case u.eventChannel <- event: 91 + u.logger.V(1).Info("Sent USB event", 92 + "action", action, 93 + "device", hsmDeviceName, 94 + "vendor", usbDev.VendorID, 95 + "product", usbDev.ProductID, 96 + "serial", usbDev.SerialNumber) 97 + default: 98 + u.logger.Error(nil, "USB event channel full, dropping event", "action", action) 99 + } 100 + } 101 + } 102 + }
+47 -70
internal/discovery/usb_nocgo.go
··· 1 1 //go:build !cgo 2 - // +build !cgo 3 2 4 3 /* 5 4 Copyright 2025. ··· 22 21 import ( 23 22 "context" 24 23 "fmt" 25 - "time" 24 + ) 26 25 27 - "github.com/go-logr/logr" 28 - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 - ctrl "sigs.k8s.io/controller-runtime" 26 + // UDevDevice is a stub type for non-CGO builds 27 + type UDevDevice struct{} 28 + 29 + // Monitor is a stub type for non-CGO builds 30 + type Monitor struct{} 30 31 31 - hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 32 - ) 32 + // Udev is a stub type for non-CGO builds 33 + type Udev struct{} 33 34 34 - // USBDevice represents a discovered USB device 35 - type USBDevice struct { 36 - VendorID string 37 - ProductID string 38 - SerialNumber string 39 - DevicePath string 40 - Manufacturer string 41 - Product string 42 - DeviceInfo map[string]string 35 + // NewEnumerate creates a stub enumerate object for non-CGO builds 36 + func (u *Udev) NewEnumerate() *Enumerate { 37 + return &Enumerate{} 43 38 } 44 39 45 - // USBEvent represents a USB device event 46 - type USBEvent struct { 47 - Action string // "add" or "remove" 48 - Device USBDevice // The device that changed 49 - Timestamp time.Time 50 - HSMDeviceName string // Which HSMDevice spec this event relates to 40 + // NewMonitorFromNetlink creates a stub monitor for non-CGO builds 41 + func (u *Udev) NewMonitorFromNetlink(string) *Monitor { 42 + return &Monitor{} 51 43 } 52 44 53 - // USBDiscoverer handles USB device discovery and monitoring (no-CGO stub) 54 - type USBDiscoverer struct { 55 - logger logr.Logger 56 - eventChannel chan USBEvent 57 - activeSpecs map[string]*hsmv1alpha1.USBDeviceSpec 45 + // Enumerate is a stub type for non-CGO builds 46 + type Enumerate struct{} 47 + 48 + // AddMatchSubsystem does nothing in non-CGO builds 49 + func (e *Enumerate) AddMatchSubsystem(string) error { 50 + return nil 58 51 } 59 52 60 - // NewUSBDiscoverer creates a new USB device discoverer (no-CGO stub) 61 - func NewUSBDiscoverer() *USBDiscoverer { 62 - logger := ctrl.Log.WithName("usb-discoverer-nocgo") 53 + // AddMatchProperty does nothing in non-CGO builds 54 + func (e *Enumerate) AddMatchProperty(string, string) error { 55 + return nil 56 + } 63 57 64 - return &USBDiscoverer{ 65 - logger: logger, 66 - eventChannel: make(chan USBEvent, 100), 67 - activeSpecs: make(map[string]*hsmv1alpha1.USBDeviceSpec), 68 - } 58 + // Devices returns an empty slice for non-CGO builds 59 + func (e *Enumerate) Devices() ([]*UDevDevice, error) { 60 + return []*UDevDevice{}, nil 69 61 } 70 62 71 - // NewUSBDiscovererWithMethod creates a new USB device discoverer (method parameter is ignored, kept for compatibility) 72 - func NewUSBDiscovererWithMethod(method string) *USBDiscoverer { 73 - return NewUSBDiscoverer() 74 - } 63 + // DeviceChan returns empty channels for non-CGO builds 64 + func (m *Monitor) DeviceChan(ctx context.Context) (<-chan *UDevDevice, <-chan error, error) { 65 + deviceChan := make(chan *UDevDevice) 66 + errorChan := make(chan error) 75 67 76 - // DiscoverDevices finds USB devices matching the given specification (no-CGO stub - returns empty) 77 - func (u *USBDiscoverer) DiscoverDevices(ctx context.Context, spec *hsmv1alpha1.USBDeviceSpec) ([]USBDevice, error) { 78 - u.logger.Info("USB device discovery not available (CGO disabled)", 79 - "vendorId", spec.VendorID, 80 - "productId", spec.ProductID) 68 + // Close channels immediately since no devices will be found 69 + close(deviceChan) 70 + close(errorChan) 81 71 82 - // Return empty slice - no devices found without udev 83 - return []USBDevice{}, nil 72 + return deviceChan, errorChan, nil 84 73 } 85 74 86 - // StartEventMonitoring starts monitoring for USB device events (no-CGO stub - does nothing) 87 - func (u *USBDiscoverer) StartEventMonitoring(ctx context.Context) error { 88 - u.logger.Info("USB event monitoring not available (CGO disabled)") 75 + // FilterAddMatchSubsystem does nothing in non-CGO builds 76 + func (m *Monitor) FilterAddMatchSubsystem(string) error { 89 77 return nil 90 78 } 91 79 92 - // AddSpecForMonitoring adds a device spec to monitor for events (no-CGO stub) 93 - func (u *USBDiscoverer) AddSpecForMonitoring(hsmDeviceName string, spec *hsmv1alpha1.USBDeviceSpec) { 94 - u.logger.V(1).Info("USB monitoring not available (CGO disabled)", "device", hsmDeviceName) 95 - u.activeSpecs[hsmDeviceName] = spec 80 + // convertUdevDevice always returns nil for non-CGO builds 81 + func (u *USBDiscoverer) convertUdevDevice(device *UDevDevice) *USBDevice { 82 + return nil 96 83 } 97 84 98 - // RemoveSpecFromMonitoring removes a device spec from event monitoring (no-CGO stub) 99 - func (u *USBDiscoverer) RemoveSpecFromMonitoring(hsmDeviceName string) { 100 - u.logger.V(1).Info("USB monitoring not available (CGO disabled)", "device", hsmDeviceName) 101 - delete(u.activeSpecs, hsmDeviceName) 85 + // handleDeviceEvent does nothing for non-CGO builds 86 + func (u *USBDiscoverer) handleDeviceEvent(device *UDevDevice) { 87 + // No-op for non-CGO builds 102 88 } 103 89 104 - // GetEventChannel returns the channel for USB events (no-CGO stub) 105 - func (u *USBDiscoverer) GetEventChannel() <-chan USBEvent { 106 - return u.eventChannel 107 - } 108 - 109 - // StopEventMonitoring stops USB device event monitoring (no-CGO stub) 110 - func (u *USBDiscoverer) StopEventMonitoring() { 111 - u.logger.Info("USB event monitoring not available (CGO disabled)") 90 + // init logs a warning about using fallback mode 91 + func init() { 92 + // Note: We can't use the logger here since it's not available during init 93 + fmt.Println("WARNING: USB discovery running in fallback mode (CGO disabled). No USB devices will be detected.") 112 94 } 113 - 114 - // IsEventMonitoringActive returns whether event monitoring is active (no-CGO stub - always false) 115 - func (u *USBDiscoverer) IsEventMonitoringActive() bool { 116 - return false 117 - }
+392
internal/hsm/pkcs11_cgo.go
··· 1 + //go:build cgo 2 + 3 + /* 4 + Copyright 2025. 5 + 6 + Licensed under the Apache License, Version 2.0 (the "License"); 7 + you may not use this file except in compliance with the License. 8 + You may obtain a copy of the License at 9 + 10 + http://www.apache.org/licenses/LICENSE-2.0 11 + 12 + Unless required by applicable law or agreed to in writing, software 13 + distributed under the License is distributed on an "AS IS" BASIS, 14 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + See the License for the specific language governing permissions and 16 + limitations under the License. 17 + */ 18 + 19 + package hsm 20 + 21 + import ( 22 + "fmt" 23 + "strings" 24 + 25 + "github.com/miekg/pkcs11" 26 + ) 27 + 28 + // CGO-specific types 29 + type Session struct { 30 + ctx *pkcs11.Ctx 31 + session pkcs11.SessionHandle 32 + slot uint 33 + } 34 + 35 + type ObjectHandle = pkcs11.ObjectHandle 36 + 37 + // initializePKCS11 establishes connection to the HSM via PKCS#11 38 + func initializePKCS11(config Config, pin string) (*Session, uint, error) { 39 + // Initialize PKCS#11 context 40 + ctx := pkcs11.New(config.PKCS11LibraryPath) 41 + if ctx == nil { 42 + return nil, 0, fmt.Errorf("failed to create PKCS#11 context for library: %s", config.PKCS11LibraryPath) 43 + } 44 + 45 + // Initialize the library 46 + if err := ctx.Initialize(); err != nil { 47 + return nil, 0, fmt.Errorf("failed to initialize PKCS#11 library: %w", err) 48 + } 49 + 50 + // Find the slot 51 + slots, err := ctx.GetSlotList(true) // true = only slots with tokens 52 + if err != nil { 53 + if finErr := ctx.Finalize(); finErr != nil { 54 + // Ignore finalize error, return original error 55 + _ = finErr 56 + } 57 + ctx.Destroy() 58 + return nil, 0, fmt.Errorf("failed to get slot list: %w", err) 59 + } 60 + 61 + if len(slots) == 0 { 62 + if finErr := ctx.Finalize(); finErr != nil { 63 + // Ignore finalize error, return original error 64 + _ = finErr 65 + } 66 + ctx.Destroy() 67 + return nil, 0, fmt.Errorf("no slots with tokens found") 68 + } 69 + 70 + // Use specified slot ID or find by token label 71 + var targetSlot uint 72 + found := false 73 + 74 + if config.UseSlotID { 75 + // Use specified slot ID 76 + for _, slot := range slots { 77 + if slot == config.SlotID { 78 + targetSlot = slot 79 + found = true 80 + break 81 + } 82 + } 83 + if !found { 84 + if finErr := ctx.Finalize(); finErr != nil { 85 + // Ignore finalize error, return original error 86 + _ = finErr 87 + } 88 + ctx.Destroy() 89 + return nil, 0, fmt.Errorf("specified slot ID %d not found", config.SlotID) 90 + } 91 + } else if config.TokenLabel != "" { 92 + // Find slot by token label 93 + for _, slot := range slots { 94 + tokenInfo, err := ctx.GetTokenInfo(slot) 95 + if err != nil { 96 + continue 97 + } 98 + if strings.TrimSpace(tokenInfo.Label) == config.TokenLabel { 99 + targetSlot = slot 100 + found = true 101 + break 102 + } 103 + } 104 + if !found { 105 + if finErr := ctx.Finalize(); finErr != nil { 106 + // Ignore finalize error, return original error 107 + _ = finErr 108 + } 109 + ctx.Destroy() 110 + return nil, 0, fmt.Errorf("token with label '%s' not found", config.TokenLabel) 111 + } 112 + } else { 113 + // Use first available slot 114 + targetSlot = slots[0] 115 + } 116 + 117 + // Open session 118 + session, err := ctx.OpenSession(targetSlot, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) 119 + if err != nil { 120 + if finErr := ctx.Finalize(); finErr != nil { 121 + // Ignore finalize error, return original error 122 + _ = finErr 123 + } 124 + ctx.Destroy() 125 + return nil, 0, fmt.Errorf("failed to open session: %w", err) 126 + } 127 + 128 + // Login with PIN 129 + if err := ctx.Login(session, pkcs11.CKU_USER, pin); err != nil { 130 + if closeErr := ctx.CloseSession(session); closeErr != nil { 131 + // Ignore close error, return original error 132 + _ = closeErr 133 + } 134 + if finErr := ctx.Finalize(); finErr != nil { 135 + // Ignore finalize error, return original error 136 + _ = finErr 137 + } 138 + ctx.Destroy() 139 + return nil, 0, fmt.Errorf("failed to login with PIN: %w", err) 140 + } 141 + 142 + return &Session{ 143 + ctx: ctx, 144 + session: session, 145 + slot: targetSlot, 146 + }, targetSlot, nil 147 + } 148 + 149 + // closePKCS11 terminates the HSM connection 150 + func closePKCS11(session *Session) error { 151 + if session == nil { 152 + return nil 153 + } 154 + 155 + // Logout and close session 156 + if session.ctx != nil && session.session != 0 { 157 + if logoutErr := session.ctx.Logout(session.session); logoutErr != nil { 158 + // Ignore logout error but continue 159 + _ = logoutErr 160 + } 161 + if closeErr := session.ctx.CloseSession(session.session); closeErr != nil { 162 + // Ignore close error but continue 163 + _ = closeErr 164 + } 165 + } 166 + 167 + // Finalize and destroy context 168 + if session.ctx != nil { 169 + if finErr := session.ctx.Finalize(); finErr != nil { 170 + // Ignore finalize error but continue 171 + _ = finErr 172 + } 173 + session.ctx.Destroy() 174 + } 175 + 176 + return nil 177 + } 178 + 179 + // getTokenInfoPKCS11 returns information about the HSM token 180 + func getTokenInfoPKCS11(session *Session, slot uint) (*tokenInfo, error) { 181 + if session == nil { 182 + return nil, fmt.Errorf("session is nil") 183 + } 184 + 185 + // Get token information from PKCS#11 186 + pkcs11TokenInfo, err := session.ctx.GetTokenInfo(session.slot) 187 + if err != nil { 188 + return nil, fmt.Errorf("failed to get token info: %w", err) 189 + } 190 + 191 + // Get slot information 192 + slotInfo, slotErr := session.ctx.GetSlotInfo(session.slot) 193 + 194 + info := &tokenInfo{ 195 + Label: strings.TrimSpace(pkcs11TokenInfo.Label), 196 + ManufacturerID: strings.TrimSpace(pkcs11TokenInfo.ManufacturerID), 197 + Model: strings.TrimSpace(pkcs11TokenInfo.Model), 198 + SerialNumber: strings.TrimSpace(pkcs11TokenInfo.SerialNumber), 199 + FirmwareVersion: fmt.Sprintf("%d.%d", pkcs11TokenInfo.FirmwareVersion.Major, pkcs11TokenInfo.FirmwareVersion.Minor), 200 + } 201 + 202 + // Add slot info if available 203 + if slotErr == nil { 204 + if info.ManufacturerID == "" { 205 + info.ManufacturerID = strings.TrimSpace(slotInfo.ManufacturerID) 206 + } 207 + if info.Model == "" { 208 + info.Model = strings.TrimSpace(slotInfo.SlotDescription) 209 + } 210 + } 211 + 212 + return info, nil 213 + } 214 + 215 + // findObjectsPKCS11 finds PKCS#11 data objects matching the given path 216 + func findObjectsPKCS11(session *Session, path string) ([]pkcs11Object, error) { 217 + if session == nil { 218 + return nil, fmt.Errorf("session is nil") 219 + } 220 + 221 + // Find all data objects (we'll filter by label after) 222 + template := []*pkcs11.Attribute{ 223 + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA), 224 + } 225 + 226 + if err := session.ctx.FindObjectsInit(session.session, template); err != nil { 227 + return nil, fmt.Errorf("failed to initialize object search: %w", err) 228 + } 229 + defer func() { 230 + if finalErr := session.ctx.FindObjectsFinal(session.session); finalErr != nil { 231 + // Ignore finalize error but continue 232 + _ = finalErr 233 + } 234 + }() 235 + 236 + // Get all matching objects 237 + objs, _, err := session.ctx.FindObjects(session.session, 1000) // Max 1000 objects 238 + if err != nil { 239 + return nil, fmt.Errorf("failed to find objects: %w", err) 240 + } 241 + 242 + // Pre-allocate slice for better performance 243 + objects := make([]pkcs11Object, 0, len(objs)) 244 + 245 + // Read each data object 246 + for _, obj := range objs { 247 + // Get the label and value 248 + attrs, err := session.ctx.GetAttributeValue(session.session, obj, []*pkcs11.Attribute{ 249 + pkcs11.NewAttribute(pkcs11.CKA_LABEL, nil), 250 + pkcs11.NewAttribute(pkcs11.CKA_VALUE, nil), 251 + }) 252 + if err != nil { 253 + continue // Skip objects we can't read 254 + } 255 + 256 + if len(attrs) < 2 || len(attrs[0].Value) == 0 { 257 + continue // Skip objects without proper attributes 258 + } 259 + 260 + label := string(attrs[0].Value) 261 + value := attrs[1].Value 262 + 263 + // If path is specified, filter by it 264 + if path != "" && !strings.HasPrefix(label, path) { 265 + continue 266 + } 267 + 268 + objects = append(objects, pkcs11Object{ 269 + Label: label, 270 + Value: value, 271 + Handle: obj, 272 + }) 273 + } 274 + 275 + return objects, nil 276 + } 277 + 278 + // createObjectPKCS11 creates a new PKCS#11 data object 279 + func createObjectPKCS11(session *Session, label string, value []byte) (ObjectHandle, error) { 280 + if session == nil { 281 + return 0, fmt.Errorf("session is nil") 282 + } 283 + 284 + // Infer data type from content 285 + dataType := InferDataType(value) 286 + 287 + // Get OID for data type 288 + oid, err := GetOIDForDataType(dataType) 289 + if err != nil { 290 + oid = OIDPlaintext // Default fallback 291 + } 292 + 293 + // Encode OID as DER 294 + derOID, err := EncodeDER(oid) 295 + if err != nil { 296 + derOID = nil // Will skip CKA_OBJECT_ID if encoding fails 297 + } 298 + 299 + // Build template with proper attributes 300 + template := []*pkcs11.Attribute{ 301 + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA), 302 + pkcs11.NewAttribute(pkcs11.CKA_LABEL, label), 303 + pkcs11.NewAttribute(pkcs11.CKA_APPLICATION, applicationName), // Proper application name 304 + pkcs11.NewAttribute(pkcs11.CKA_VALUE, value), 305 + pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), // Store persistently 306 + pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, true), // Require authentication 307 + pkcs11.NewAttribute(pkcs11.CKA_MODIFIABLE, true), // Allow updates 308 + } 309 + 310 + // Add OID if we successfully encoded it 311 + if derOID != nil { 312 + template = append(template, pkcs11.NewAttribute(pkcs11.CKA_OBJECT_ID, derOID)) 313 + } 314 + 315 + obj, err := session.ctx.CreateObject(session.session, template) 316 + if err != nil { 317 + return 0, fmt.Errorf("failed to create data object: %w", err) 318 + } 319 + 320 + return obj, nil 321 + } 322 + 323 + // deleteSecretObjectsPKCS11 removes all data objects matching the given path prefix 324 + func deleteSecretObjectsPKCS11(session *Session, path string) error { 325 + if session == nil { 326 + return fmt.Errorf("session is nil") 327 + } 328 + 329 + // Find all data objects (we'll filter by label after) 330 + template := []*pkcs11.Attribute{ 331 + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA), 332 + } 333 + 334 + if err := session.ctx.FindObjectsInit(session.session, template); err != nil { 335 + return fmt.Errorf("failed to initialize object search: %w", err) 336 + } 337 + defer func() { 338 + if finalErr := session.ctx.FindObjectsFinal(session.session); finalErr != nil { 339 + // Ignore finalize error but continue 340 + _ = finalErr 341 + } 342 + }() 343 + 344 + // Get all matching objects 345 + objs, _, err := session.ctx.FindObjects(session.session, 100) // Max 100 objects 346 + if err != nil { 347 + return fmt.Errorf("failed to find objects: %w", err) 348 + } 349 + 350 + // Delete each object that matches our path 351 + for _, obj := range objs { 352 + // Get the label to check if this object matches our path 353 + labelAttr, err := session.ctx.GetAttributeValue(session.session, obj, []*pkcs11.Attribute{ 354 + pkcs11.NewAttribute(pkcs11.CKA_LABEL, nil), 355 + }) 356 + if err != nil { 357 + continue 358 + } 359 + 360 + if len(labelAttr) == 0 || len(labelAttr[0].Value) == 0 { 361 + continue 362 + } 363 + 364 + label := string(labelAttr[0].Value) 365 + // Only delete objects that match our path 366 + if !strings.HasPrefix(label, path) { 367 + continue 368 + } 369 + 370 + if err := session.ctx.DestroyObject(session.session, obj); err != nil { 371 + // Log error but continue with other objects 372 + continue 373 + } 374 + } 375 + 376 + return nil 377 + } 378 + 379 + // changePINPKCS11 changes the HSM PIN 380 + func changePINPKCS11(session *Session, oldPIN, newPIN string) error { 381 + if session == nil { 382 + return fmt.Errorf("session is nil") 383 + } 384 + 385 + // Use PKCS#11 SetPIN function to change the PIN 386 + // Note: This changes the user PIN (not SO PIN) 387 + if err := session.ctx.SetPIN(session.session, oldPIN, newPIN); err != nil { 388 + return fmt.Errorf("failed to change HSM PIN: %w", err) 389 + } 390 + 391 + return nil 392 + }
+78 -407
internal/hsm/pkcs11_client.go
··· 1 - //go:build cgo 2 - // +build cgo 3 - 4 1 /* 5 2 Copyright 2025. 6 3 ··· 28 25 "time" 29 26 30 27 "github.com/go-logr/logr" 31 - "github.com/miekg/pkcs11" 32 28 ctrl "sigs.k8s.io/controller-runtime" 33 29 ) 34 30 ··· 44 40 logger logr.Logger 45 41 mutex sync.RWMutex 46 42 47 - // PKCS#11 objects 48 - ctx *pkcs11.Ctx 49 - session pkcs11.SessionHandle 43 + // Internal state 44 + session *Session // Will be concrete type in CGO, stub in non-CGO 50 45 slot uint 51 46 connected bool 52 47 53 - // Data object cache for faster lookups 54 - dataObjects map[string]pkcs11.ObjectHandle 48 + // Data object cache for faster lookups (CGO only) 49 + dataObjects map[string]ObjectHandle 50 + } 51 + 52 + // pkcs11Object represents a PKCS#11 data object 53 + type pkcs11Object struct { 54 + Label string 55 + Value []byte 56 + Handle ObjectHandle // Will be concrete type in CGO, stub in non-CGO 57 + } 58 + 59 + // tokenInfo represents HSM token information 60 + type tokenInfo struct { 61 + Label string 62 + ManufacturerID string 63 + Model string 64 + SerialNumber string 65 + FirmwareVersion string 55 66 } 56 67 57 68 // NewPKCS11Client creates a new PKCS#11 HSM client 58 69 func NewPKCS11Client() *PKCS11Client { 59 70 return &PKCS11Client{ 60 71 logger: ctrl.Log.WithName("hsm-pkcs11-client"), 61 - dataObjects: make(map[string]pkcs11.ObjectHandle), 72 + dataObjects: make(map[string]ObjectHandle), 62 73 } 63 74 } 64 75 ··· 73 84 "slot", config.SlotID, 74 85 "tokenLabel", config.TokenLabel) 75 86 76 - // Validate configuration 87 + // Common validation 77 88 if config.PKCS11LibraryPath == "" { 78 89 return fmt.Errorf("PKCS11LibraryPath is required") 79 90 } ··· 81 92 if config.PINProvider == nil { 82 93 return fmt.Errorf("PINProvider is required for HSM authentication") 83 94 } 84 - 85 - // Initialize PKCS#11 context 86 - c.ctx = pkcs11.New(config.PKCS11LibraryPath) 87 - if c.ctx == nil { 88 - return fmt.Errorf("failed to create PKCS#11 context for library: %s", config.PKCS11LibraryPath) 89 - } 90 - 91 - // Initialize the library 92 - if err := c.ctx.Initialize(); err != nil { 93 - return fmt.Errorf("failed to initialize PKCS#11 library: %w", err) 94 - } 95 - 96 - // Find the slot 97 - slots, err := c.ctx.GetSlotList(true) // true = only slots with tokens 98 - if err != nil { 99 - if finErr := c.ctx.Finalize(); finErr != nil { 100 - c.logger.V(1).Info("Failed to finalize PKCS#11 context", "error", finErr) 101 - } 102 - c.ctx.Destroy() 103 - return fmt.Errorf("failed to get slot list: %w", err) 104 - } 105 - 106 - if len(slots) == 0 { 107 - if finErr := c.ctx.Finalize(); finErr != nil { 108 - c.logger.V(1).Info("Failed to finalize PKCS#11 context", "error", finErr) 109 - } 110 - c.ctx.Destroy() 111 - return fmt.Errorf("no slots with tokens found") 112 - } 113 - 114 - // Use specified slot ID or find by token label 115 - var targetSlot uint 116 - found := false 117 - 118 - if config.UseSlotID { 119 - // Use specified slot ID 120 - for _, slot := range slots { 121 - if slot == config.SlotID { 122 - targetSlot = slot 123 - found = true 124 - break 125 - } 126 - } 127 - if !found { 128 - if finErr := c.ctx.Finalize(); finErr != nil { 129 - c.logger.V(1).Info("Failed to finalize PKCS#11 context", "error", finErr) 130 - } 131 - c.ctx.Destroy() 132 - return fmt.Errorf("specified slot ID %d not found", config.SlotID) 133 - } 134 - } else if config.TokenLabel != "" { 135 - // Find slot by token label 136 - for _, slot := range slots { 137 - tokenInfo, err := c.ctx.GetTokenInfo(slot) 138 - if err != nil { 139 - c.logger.V(1).Info("Failed to get token info for slot", "slot", slot, "error", err) 140 - continue 141 - } 142 - if strings.TrimSpace(tokenInfo.Label) == config.TokenLabel { 143 - targetSlot = slot 144 - found = true 145 - break 146 - } 147 - } 148 - if !found { 149 - if finErr := c.ctx.Finalize(); finErr != nil { 150 - c.logger.V(1).Info("Failed to finalize PKCS#11 context", "error", finErr) 151 - } 152 - c.ctx.Destroy() 153 - return fmt.Errorf("token with label '%s' not found", config.TokenLabel) 154 - } 155 - } else { 156 - // Use first available slot 157 - targetSlot = slots[0] 158 - } 159 - 160 - c.slot = targetSlot 161 - c.logger.Info("Using HSM slot", "slot", targetSlot) 162 - 163 - // Open session 164 - session, err := c.ctx.OpenSession(targetSlot, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) 165 - if err != nil { 166 - if finErr := c.ctx.Finalize(); finErr != nil { 167 - c.logger.V(1).Info("Failed to finalize PKCS#11 context", "error", finErr) 168 - } 169 - c.ctx.Destroy() 170 - return fmt.Errorf("failed to open session: %w", err) 171 - } 172 - c.session = session 173 95 174 96 // Get PIN from provider 175 97 pin, err := config.PINProvider.GetPIN(ctx) 176 98 if err != nil { 177 - if closeErr := c.ctx.CloseSession(session); closeErr != nil { 178 - c.logger.V(1).Info("Failed to close session", "error", closeErr) 179 - } 180 - if finErr := c.ctx.Finalize(); finErr != nil { 181 - c.logger.V(1).Info("Failed to finalize PKCS#11 context", "error", finErr) 182 - } 183 - c.ctx.Destroy() 184 99 return fmt.Errorf("failed to get PIN from provider: %w", err) 185 100 } 186 101 187 - // Login with PIN 188 - if err := c.ctx.Login(session, pkcs11.CKU_USER, pin); err != nil { 189 - if closeErr := c.ctx.CloseSession(session); closeErr != nil { 190 - c.logger.V(1).Info("Failed to close session", "error", closeErr) 191 - } 192 - if finErr := c.ctx.Finalize(); finErr != nil { 193 - c.logger.V(1).Info("Failed to finalize PKCS#11 context", "error", finErr) 194 - } 195 - c.ctx.Destroy() 196 - return fmt.Errorf("failed to login with PIN: %w", err) 102 + // Call platform-specific initialization 103 + session, slot, err := initializePKCS11(config, pin) 104 + if err != nil { 105 + return err 197 106 } 198 107 108 + c.session = session 109 + c.slot = slot 199 110 c.connected = true 200 - c.logger.Info("HSM connection established successfully", "slot", targetSlot) 111 + c.logger.Info("HSM connection established successfully", "slot", slot) 201 112 return nil 202 113 } 203 114 ··· 212 123 213 124 c.logger.Info("Closing HSM connection") 214 125 215 - // Logout and close session 216 - if c.ctx != nil && c.session != 0 { 217 - if logoutErr := c.ctx.Logout(c.session); logoutErr != nil { 218 - c.logger.V(1).Info("Failed to logout from HSM session", "error", logoutErr) 219 - } 220 - if closeErr := c.ctx.CloseSession(c.session); closeErr != nil { 221 - c.logger.V(1).Info("Failed to close HSM session", "error", closeErr) 222 - } 223 - } 224 - 225 - // Finalize and destroy context 226 - if c.ctx != nil { 227 - if finErr := c.ctx.Finalize(); finErr != nil { 228 - c.logger.V(1).Info("Failed to finalize PKCS#11 context", "error", finErr) 229 - } 230 - c.ctx.Destroy() 126 + // Call platform-specific cleanup 127 + if err := closePKCS11(c.session); err != nil { 128 + c.logger.V(1).Info("Error during PKCS#11 cleanup", "error", err) 231 129 } 232 130 233 131 c.connected = false 234 - c.session = 0 235 - c.ctx = nil 236 - c.dataObjects = make(map[string]pkcs11.ObjectHandle) 132 + c.session = nil 133 + c.dataObjects = make(map[string]ObjectHandle) 237 134 238 135 return nil 239 136 } ··· 247 144 return nil, fmt.Errorf("HSM not connected") 248 145 } 249 146 250 - // Get token information from PKCS#11 251 - tokenInfo, err := c.ctx.GetTokenInfo(c.slot) 147 + // Get token information via helper 148 + tokenInfo, err := getTokenInfoPKCS11(c.session, c.slot) 252 149 if err != nil { 253 150 return nil, fmt.Errorf("failed to get token info: %w", err) 254 151 } 255 152 256 - // Get slot information 257 - slotInfo, slotErr := c.ctx.GetSlotInfo(c.slot) 258 - if slotErr != nil { 259 - c.logger.V(1).Info("Failed to get slot info", "error", slotErr) 260 - } 261 - 262 153 info := &HSMInfo{ 263 154 Label: strings.TrimSpace(tokenInfo.Label), 264 155 Manufacturer: strings.TrimSpace(tokenInfo.ManufacturerID), 265 156 Model: strings.TrimSpace(tokenInfo.Model), 266 157 SerialNumber: strings.TrimSpace(tokenInfo.SerialNumber), 267 - FirmwareVersion: fmt.Sprintf("%d.%d", tokenInfo.FirmwareVersion.Major, tokenInfo.FirmwareVersion.Minor), 268 - } 269 - 270 - // Add slot info if available 271 - if slotErr == nil { 272 - if info.Manufacturer == "" { 273 - info.Manufacturer = strings.TrimSpace(slotInfo.ManufacturerID) 274 - } 275 - if info.Model == "" { 276 - info.Model = strings.TrimSpace(slotInfo.SlotDescription) 277 - } 158 + FirmwareVersion: tokenInfo.FirmwareVersion, 278 159 } 279 160 280 161 return info, nil ··· 291 172 292 173 c.logger.V(1).Info("Reading secret from HSM", "path", path) 293 174 294 - // Find all data objects (we'll filter by label after) 295 - template := []*pkcs11.Attribute{ 296 - pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA), 297 - } 298 - 299 - if err := c.ctx.FindObjectsInit(c.session, template); err != nil { 300 - return nil, fmt.Errorf("failed to initialize object search: %w", err) 301 - } 302 - defer func() { 303 - if finalErr := c.ctx.FindObjectsFinal(c.session); finalErr != nil { 304 - c.logger.V(1).Info("Failed to finalize object search", "error", finalErr) 305 - } 306 - }() 307 - 308 - // Get all matching objects 309 - objs, _, err := c.ctx.FindObjects(c.session, 100) // Max 100 objects 175 + // Find objects matching the path 176 + objects, err := findObjectsPKCS11(c.session, path) 310 177 if err != nil { 311 178 return nil, fmt.Errorf("failed to find objects: %w", err) 312 179 } 313 180 314 181 data := make(SecretData) 315 - // Track if we found any objects for this path 316 182 matchingObjects := 0 317 183 318 - // Read each data object and filter by path 319 - for _, obj := range objs { 320 - // Get the label to determine if this object matches our path 321 - labelAttr, err := c.ctx.GetAttributeValue(c.session, obj, []*pkcs11.Attribute{ 322 - pkcs11.NewAttribute(pkcs11.CKA_LABEL, nil), 323 - }) 324 - if err != nil { 325 - c.logger.V(1).Info("Failed to get object label", "error", err) 326 - continue 327 - } 328 - 329 - if len(labelAttr) == 0 || len(labelAttr[0].Value) == 0 { 330 - c.logger.V(1).Info("Object has no label, skipping") 331 - continue 332 - } 333 - 334 - label := string(labelAttr[0].Value) 335 - 184 + // Process each object 185 + for _, obj := range objects { 336 186 // Check if this object matches our path 337 - if !strings.HasPrefix(label, path) { 338 - continue // Skip objects that don't match our path 187 + if !strings.HasPrefix(obj.Label, path) { 188 + continue 339 189 } 340 190 341 191 // Skip metadata objects when reading secrets 342 - if strings.HasSuffix(label, metadataKeySuffix) { 192 + if strings.HasSuffix(obj.Label, metadataKeySuffix) { 343 193 continue 344 194 } 345 195 346 196 matchingObjects++ 347 197 348 198 // Extract key name from label (remove path prefix) 349 - key := strings.TrimPrefix(label, path) 199 + key := strings.TrimPrefix(obj.Label, path) 350 200 key = strings.TrimPrefix(key, "/") 351 201 if key == "" { 352 202 key = defaultKeyName // Default key name 353 203 } 354 204 355 - // Get the actual data value 356 - valueAttr, err := c.ctx.GetAttributeValue(c.session, obj, []*pkcs11.Attribute{ 357 - pkcs11.NewAttribute(pkcs11.CKA_VALUE, nil), 358 - }) 359 - if err != nil { 360 - c.logger.V(1).Info("Failed to get object value", "key", key, "error", err) 361 - continue 362 - } 363 - 364 - if len(valueAttr) > 0 && len(valueAttr[0].Value) > 0 { 365 - data[key] = valueAttr[0].Value 366 - } 205 + data[key] = obj.Value 367 206 } 368 207 369 208 if matchingObjects == 0 { ··· 393 232 "path", path, "keys", len(data)) 394 233 395 234 // First, delete any existing objects for this path to avoid duplicates 396 - if err := c.deleteSecretObjects(path); err != nil { 235 + if err := deleteSecretObjectsPKCS11(c.session, path); err != nil { 397 236 c.logger.V(1).Info("Failed to delete existing objects (may not exist)", "error", err) 398 237 } 399 238 ··· 404 243 label = path + "/" + key 405 244 } 406 245 407 - // Infer data type from content 408 - dataType := InferDataType(value) 409 - 410 - // Get OID for data type 411 - oid, err := GetOIDForDataType(dataType) 412 - if err != nil { 413 - c.logger.V(1).Info("Failed to get OID for data type, using default", 414 - "dataType", dataType, "error", err) 415 - oid = OIDPlaintext // Default fallback 416 - } 417 - 418 - // Encode OID as DER 419 - derOID, err := EncodeDER(oid) 420 - if err != nil { 421 - c.logger.V(1).Info("Failed to encode OID as DER", "error", err) 422 - derOID = nil // Will skip CKA_OBJECT_ID if encoding fails 423 - } 424 - 425 - // Build template with proper attributes 426 - template := []*pkcs11.Attribute{ 427 - pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA), 428 - pkcs11.NewAttribute(pkcs11.CKA_LABEL, label), 429 - pkcs11.NewAttribute(pkcs11.CKA_APPLICATION, applicationName), // Proper application name 430 - pkcs11.NewAttribute(pkcs11.CKA_VALUE, value), 431 - pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), // Store persistently 432 - pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, true), // Require authentication 433 - pkcs11.NewAttribute(pkcs11.CKA_MODIFIABLE, true), // Allow updates 434 - } 435 - 436 - // Add OID if we successfully encoded it 437 - if derOID != nil { 438 - template = append(template, pkcs11.NewAttribute(pkcs11.CKA_OBJECT_ID, derOID)) 439 - } 440 - 441 - obj, err := c.ctx.CreateObject(c.session, template) 246 + // Create the object via helper 247 + handle, err := createObjectPKCS11(c.session, label, value) 442 248 if err != nil { 443 249 return fmt.Errorf("failed to create data object for key '%s': %w", key, err) 444 250 } 445 251 446 252 // Cache the object handle for faster future lookups 447 - c.dataObjects[label] = obj 253 + c.dataObjects[label] = handle 448 254 449 - c.logger.V(2).Info("Created data object", "path", path, "key", key, "label", label, "dataType", dataType) 255 + c.logger.V(2).Info("Created data object", "path", path, "key", key, "label", label) 450 256 } 451 257 452 258 c.logger.Info("Successfully wrote secret to HSM", "path", path) ··· 469 275 // Create metadata object label 470 276 metadataLabel := path + metadataKeySuffix 471 277 472 - // Get OID for JSON data type 473 - oid, err := GetOIDForDataType(DataTypeJson) 474 - if err != nil { 475 - c.logger.V(1).Info("Failed to get OID for JSON metadata", "error", err) 476 - oid = OIDJson // Fallback 477 - } 478 - 479 - // Encode OID as DER 480 - derOID, err := EncodeDER(oid) 481 - if err != nil { 482 - c.logger.V(1).Info("Failed to encode metadata OID as DER", "error", err) 483 - derOID = nil 484 - } 485 - 486 - // Build metadata object template 487 - template := []*pkcs11.Attribute{ 488 - pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA), 489 - pkcs11.NewAttribute(pkcs11.CKA_LABEL, metadataLabel), 490 - pkcs11.NewAttribute(pkcs11.CKA_APPLICATION, applicationName), 491 - pkcs11.NewAttribute(pkcs11.CKA_VALUE, metadataJSON), 492 - pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), // Store persistently 493 - pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, true), // Require authentication 494 - pkcs11.NewAttribute(pkcs11.CKA_MODIFIABLE, true), // Allow updates 495 - } 496 - 497 - // Add OID if we successfully encoded it 498 - if derOID != nil { 499 - template = append(template, pkcs11.NewAttribute(pkcs11.CKA_OBJECT_ID, derOID)) 500 - } 501 - 502 - // Create the metadata object 503 - obj, err := c.ctx.CreateObject(c.session, template) 278 + // Create the metadata object via helper 279 + handle, err := createObjectPKCS11(c.session, metadataLabel, metadataJSON) 504 280 if err != nil { 505 281 return fmt.Errorf("failed to create metadata object: %w", err) 506 282 } 507 283 508 284 // Cache the metadata object handle 509 - c.dataObjects[metadataLabel] = obj 285 + c.dataObjects[metadataLabel] = handle 510 286 511 287 c.logger.V(2).Info("Created metadata object", "path", path, "label", metadataLabel) 512 288 return nil ··· 524 300 metadataLabel := path + metadataKeySuffix 525 301 526 302 // Find the metadata object 527 - template := []*pkcs11.Attribute{ 528 - pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA), 529 - pkcs11.NewAttribute(pkcs11.CKA_LABEL, metadataLabel), 530 - } 531 - 532 - if err := c.ctx.FindObjectsInit(c.session, template); err != nil { 533 - return nil, fmt.Errorf("failed to initialize metadata search: %w", err) 534 - } 535 - defer func() { 536 - if finalErr := c.ctx.FindObjectsFinal(c.session); finalErr != nil { 537 - c.logger.V(1).Info("Failed to finalize metadata search", "error", finalErr) 538 - } 539 - }() 540 - 541 - objs, _, err := c.ctx.FindObjects(c.session, 1) 303 + objects, err := findObjectsPKCS11(c.session, metadataLabel) 542 304 if err != nil { 543 305 return nil, fmt.Errorf("failed to find metadata object: %w", err) 544 306 } 545 307 546 - if len(objs) == 0 { 547 - return nil, fmt.Errorf("metadata not found for path: %s", path) 548 - } 549 - 550 - // Get the metadata value 551 - valueAttr, err := c.ctx.GetAttributeValue(c.session, objs[0], []*pkcs11.Attribute{ 552 - pkcs11.NewAttribute(pkcs11.CKA_VALUE, nil), 553 - }) 554 - if err != nil { 555 - return nil, fmt.Errorf("failed to get metadata value: %w", err) 556 - } 557 - 558 - if len(valueAttr) == 0 || len(valueAttr[0].Value) == 0 { 559 - return nil, fmt.Errorf("metadata object has no value") 560 - } 561 - 562 - // Parse the JSON metadata 563 - var metadata SecretMetadata 564 - if err := json.Unmarshal(valueAttr[0].Value, &metadata); err != nil { 565 - return nil, fmt.Errorf("failed to parse metadata JSON: %w", err) 566 - } 567 - 568 - return &metadata, nil 569 - } 570 - 571 - // deleteSecretObjects removes all data objects matching the given path prefix 572 - func (c *PKCS11Client) deleteSecretObjects(path string) error { 573 - // Find all data objects (we'll filter by label after) 574 - template := []*pkcs11.Attribute{ 575 - pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA), 576 - } 577 - 578 - if err := c.ctx.FindObjectsInit(c.session, template); err != nil { 579 - return fmt.Errorf("failed to initialize object search: %w", err) 580 - } 581 - defer func() { 582 - if finalErr := c.ctx.FindObjectsFinal(c.session); finalErr != nil { 583 - c.logger.V(1).Info("Failed to finalize object search", "error", finalErr) 584 - } 585 - }() 586 - 587 - // Get all matching objects 588 - objs, _, err := c.ctx.FindObjects(c.session, 100) // Max 100 objects 589 - if err != nil { 590 - return fmt.Errorf("failed to find objects: %w", err) 591 - } 592 - 593 - // Delete each object that matches our path 594 - for _, obj := range objs { 595 - // Get the label to check if this object matches our path 596 - labelAttr, err := c.ctx.GetAttributeValue(c.session, obj, []*pkcs11.Attribute{ 597 - pkcs11.NewAttribute(pkcs11.CKA_LABEL, nil), 598 - }) 599 - if err != nil { 600 - c.logger.V(1).Info("Failed to get object label for deletion", "error", err) 601 - continue 602 - } 603 - 604 - if len(labelAttr) == 0 || len(labelAttr[0].Value) == 0 { 605 - continue 606 - } 607 - 608 - label := string(labelAttr[0].Value) 609 - // Only delete objects that match our path 610 - if !strings.HasPrefix(label, path) { 611 - continue 612 - } 613 - 614 - if err := c.ctx.DestroyObject(c.session, obj); err != nil { 615 - c.logger.V(1).Info("Failed to delete object", "object", obj, "error", err) 616 - continue 617 - } 618 - 619 - // Remove from cache 620 - for label, cachedObj := range c.dataObjects { 621 - if cachedObj == obj { 622 - delete(c.dataObjects, label) 623 - break 308 + // Look for exact match 309 + for _, obj := range objects { 310 + if obj.Label == metadataLabel { 311 + // Parse the JSON metadata 312 + var metadata SecretMetadata 313 + if err := json.Unmarshal(obj.Value, &metadata); err != nil { 314 + return nil, fmt.Errorf("failed to parse metadata JSON: %w", err) 624 315 } 316 + return &metadata, nil 625 317 } 626 318 } 627 319 628 - return nil 320 + return nil, fmt.Errorf("metadata not found for path: %s", path) 629 321 } 630 322 631 323 // DeleteSecret removes secret data from the specified HSM path ··· 639 331 640 332 c.logger.Info("Deleting secret from HSM", "path", path) 641 333 642 - if err := c.deleteSecretObjects(path); err != nil { 334 + if err := deleteSecretObjectsPKCS11(c.session, path); err != nil { 643 335 return fmt.Errorf("failed to delete secret objects: %w", err) 644 336 } 645 337 338 + // Remove from cache 339 + for label := range c.dataObjects { 340 + if strings.HasPrefix(label, path) { 341 + delete(c.dataObjects, label) 342 + } 343 + } 344 + 646 345 c.logger.Info("Successfully deleted secret from HSM", "path", path) 647 346 return nil 648 347 } ··· 658 357 659 358 c.logger.V(1).Info("Listing secrets from HSM", "prefix", prefix) 660 359 661 - // Find all data objects (we'll filter by prefix after) 662 - template := []*pkcs11.Attribute{ 663 - pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA), 664 - } 665 - 666 - if err := c.ctx.FindObjectsInit(c.session, template); err != nil { 667 - return nil, fmt.Errorf("failed to initialize object search: %w", err) 668 - } 669 - defer func() { 670 - if finalErr := c.ctx.FindObjectsFinal(c.session); finalErr != nil { 671 - c.logger.V(1).Info("Failed to finalize object search", "error", finalErr) 672 - } 673 - }() 674 - 675 - // Get all matching objects 676 - objs, _, err := c.ctx.FindObjects(c.session, 1000) // Max 1000 objects 360 + // Find all data objects 361 + objects, err := findObjectsPKCS11(c.session, "") 677 362 if err != nil { 678 363 return nil, fmt.Errorf("failed to find objects: %w", err) 679 364 } 680 365 681 366 // Extract unique paths from object labels 682 367 pathsMap := make(map[string]bool) 683 - for _, obj := range objs { 684 - // Get the label 685 - labelAttr, err := c.ctx.GetAttributeValue(c.session, obj, []*pkcs11.Attribute{ 686 - pkcs11.NewAttribute(pkcs11.CKA_LABEL, nil), 687 - }) 688 - if err != nil { 689 - c.logger.V(1).Info("Failed to get object label", "error", err) 690 - continue 691 - } 692 - 693 - if len(labelAttr) == 0 || len(labelAttr[0].Value) == 0 { 694 - continue 695 - } 696 - 697 - label := string(labelAttr[0].Value) 368 + for _, obj := range objects { 369 + label := obj.Label 698 370 699 371 // Skip metadata objects when listing secrets 700 372 if strings.HasSuffix(label, metadataKeySuffix) { ··· 774 446 return fmt.Errorf("new PIN must be different from old PIN") 775 447 } 776 448 777 - // Use PKCS#11 SetPIN function to change the PIN 778 - // Note: This changes the user PIN (not SO PIN) 779 - if err := c.ctx.SetPIN(c.session, oldPIN, newPIN); err != nil { 449 + // Call platform-specific PIN change 450 + if err := changePINPKCS11(c.session, oldPIN, newPIN); err != nil { 780 451 c.logger.Error(err, "Failed to change HSM PIN") 781 452 return fmt.Errorf("failed to change HSM PIN: %w", err) 782 453 }
-90
internal/hsm/pkcs11_client_nocgo.go
··· 1 - //go:build !cgo 2 - // +build !cgo 3 - 4 - /* 5 - Copyright 2025. 6 - 7 - Licensed under the Apache License, Version 2.0 (the "License"); 8 - you may not use this file except in compliance with the License. 9 - You may obtain a copy of the License at 10 - 11 - http://www.apache.org/licenses/LICENSE-2.0 12 - 13 - Unless required by applicable law or agreed to in writing, software 14 - distributed under the License is distributed on an "AS IS" BASIS, 15 - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 - See the License for the specific language governing permissions and 17 - limitations under the License. 18 - */ 19 - 20 - package hsm 21 - 22 - import ( 23 - "context" 24 - "fmt" 25 - 26 - "github.com/go-logr/logr" 27 - ctrl "sigs.k8s.io/controller-runtime" 28 - ) 29 - 30 - // PKCS11Client stub implementation when CGO is disabled 31 - type PKCS11Client struct { 32 - logger logr.Logger 33 - } 34 - 35 - // NewPKCS11Client creates a new PKCS#11 HSM client stub 36 - func NewPKCS11Client() *PKCS11Client { 37 - return &PKCS11Client{ 38 - logger: ctrl.Log.WithName("hsm-pkcs11-client-nocgo"), 39 - } 40 - } 41 - 42 - // Initialize returns an error indicating PKCS#11 is not available 43 - func (c *PKCS11Client) Initialize(ctx context.Context, config Config) error { 44 - return fmt.Errorf("PKCS#11 support not available: binary was built without CGO") 45 - } 46 - 47 - // Close is a no-op for the stub implementation 48 - func (c *PKCS11Client) Close() error { 49 - return nil 50 - } 51 - 52 - // GetInfo returns an error indicating PKCS#11 is not available 53 - func (c *PKCS11Client) GetInfo(ctx context.Context) (*HSMInfo, error) { 54 - return nil, fmt.Errorf("PKCS#11 support not available: binary was built without CGO") 55 - } 56 - 57 - // ReadSecret returns an error indicating PKCS#11 is not available 58 - func (c *PKCS11Client) ReadSecret(ctx context.Context, path string) (SecretData, error) { 59 - return nil, fmt.Errorf("PKCS#11 support not available: binary was built without CGO") 60 - } 61 - 62 - // WriteSecret returns an error indicating PKCS#11 is not available 63 - func (c *PKCS11Client) WriteSecret(ctx context.Context, path string, data SecretData, metadata *SecretMetadata) error { 64 - return fmt.Errorf("PKCS#11 support not available: binary was built without CGO") 65 - } 66 - 67 - // DeleteSecret returns an error indicating PKCS#11 is not available 68 - func (c *PKCS11Client) DeleteSecret(ctx context.Context, path string) error { 69 - return fmt.Errorf("PKCS#11 support not available: binary was built without CGO") 70 - } 71 - 72 - // ListSecrets returns an error indicating PKCS#11 is not available 73 - func (c *PKCS11Client) ListSecrets(ctx context.Context, prefix string) ([]string, error) { 74 - return nil, fmt.Errorf("PKCS#11 support not available: binary was built without CGO") 75 - } 76 - 77 - // GetChecksum returns an error indicating PKCS#11 is not available 78 - func (c *PKCS11Client) GetChecksum(ctx context.Context, path string) (string, error) { 79 - return "", fmt.Errorf("PKCS#11 support not available: binary was built without CGO") 80 - } 81 - 82 - // IsConnected always returns false for the stub implementation 83 - func (c *PKCS11Client) IsConnected() bool { 84 - return false 85 - } 86 - 87 - // WithRetry returns an error indicating PKCS#11 is not available 88 - func (c *PKCS11Client) WithRetry(ctx context.Context, operation func() error) error { 89 - return fmt.Errorf("PKCS#11 support not available: binary was built without CGO") 90 - }
+67
internal/hsm/pkcs11_stub.go
··· 1 + //go:build !cgo 2 + 3 + /* 4 + Copyright 2025. 5 + 6 + Licensed under the Apache License, Version 2.0 (the "License"); 7 + you may not use this file except in compliance with the License. 8 + You may obtain a copy of the License at 9 + 10 + http://www.apache.org/licenses/LICENSE-2.0 11 + 12 + Unless required by applicable law or agreed to in writing, software 13 + distributed under the License is distributed on an "AS IS" BASIS, 14 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + See the License for the specific language governing permissions and 16 + limitations under the License. 17 + */ 18 + 19 + package hsm 20 + 21 + import ( 22 + "fmt" 23 + ) 24 + 25 + // Stub types for non-CGO builds 26 + type Session struct{} 27 + type ObjectHandle struct{} 28 + 29 + // initializePKCS11 returns an error for non-CGO builds 30 + func initializePKCS11(config Config, pin string) (*Session, uint, error) { 31 + return nil, 0, fmt.Errorf("PKCS#11 support requires CGO (set CGO_ENABLED=1 and rebuild)") 32 + } 33 + 34 + // closePKCS11 is a no-op for non-CGO builds 35 + func closePKCS11(session *Session) error { 36 + return nil 37 + } 38 + 39 + // getTokenInfoPKCS11 returns an error for non-CGO builds 40 + func getTokenInfoPKCS11(session *Session, slot uint) (*tokenInfo, error) { 41 + return nil, fmt.Errorf("PKCS#11 support requires CGO (set CGO_ENABLED=1 and rebuild)") 42 + } 43 + 44 + // findObjectsPKCS11 returns an error for non-CGO builds 45 + func findObjectsPKCS11(session *Session, path string) ([]pkcs11Object, error) { 46 + return nil, fmt.Errorf("PKCS#11 support requires CGO (set CGO_ENABLED=1 and rebuild)") 47 + } 48 + 49 + // createObjectPKCS11 returns an error for non-CGO builds 50 + func createObjectPKCS11(session *Session, label string, value []byte) (ObjectHandle, error) { 51 + return ObjectHandle{}, fmt.Errorf("PKCS#11 support requires CGO (set CGO_ENABLED=1 and rebuild)") 52 + } 53 + 54 + // deleteSecretObjectsPKCS11 returns an error for non-CGO builds 55 + func deleteSecretObjectsPKCS11(session *Session, path string) error { 56 + return fmt.Errorf("PKCS#11 support requires CGO (set CGO_ENABLED=1 and rebuild)") 57 + } 58 + 59 + // changePINPKCS11 returns an error for non-CGO builds 60 + func changePINPKCS11(session *Session, oldPIN, newPIN string) error { 61 + return fmt.Errorf("PKCS#11 support requires CGO (set CGO_ENABLED=1 and rebuild)") 62 + } 63 + 64 + // init logs a warning about CGO requirement 65 + func init() { 66 + fmt.Println("WARNING: PKCS#11 client running in fallback mode (CGO disabled). HSM operations will fail.") 67 + }