···2020# Check if the request was successful
2121success=$(echo "$response" | jq -r '.success')
2222if [ "$success" = "true" ]; then
2323- # Extract pagination info
2424- total=$(echo "$response" | jq -r '.data.total')
2525- current_page=$(echo "$response" | jq -r '.data.page')
2626- page_size=$(echo "$response" | jq -r '.data.page_size')
2323+ # Extract secret info
2424+ count=$(echo "$response" | jq -r '.data.count')
2525+ prefix=$(echo "$response" | jq -r '.data.prefix // ""')
27262827 echo "📊 Summary:"
2929- echo " Total Secrets: $total"
3030- echo " Current Page: $current_page"
3131- echo " Page Size: $page_size"
2828+ echo " Total Secrets: $count"
2929+ if [ -n "$prefix" ] && [ "$prefix" != "" ]; then
3030+ echo " Prefix Filter: $prefix"
3131+ fi
3232 echo ""
33333434 # List secrets
3535 echo "🔐 Secrets:"
3636- echo "$response" | jq -r '.data.secrets[] | " • \(.label) (ID: \(.id)) - Updated: \(.updated_at // "N/A")"'
3737-3838- # Show detailed table if there are secrets
3939- if [ "$total" -gt 0 ]; then
4040- echo ""
4141- echo "📋 Detailed List:"
4242- echo "$response" | jq -r '
4343- .data.secrets |
4444- ["Label", "ID", "Checksum", "Replicated", "Updated"] as $headers |
4545- $headers,
4646- (["-----", "---", "--------", "---------", "-------"]) as $separators |
4747- $separators,
4848- (.[] | [.label, (.id // "N/A"), (.checksum[0:8] // "N/A"), .is_replicated, (.updated_at[0:10] // "N/A")]) |
4949- @tsv
5050- ' | column -t
3636+ if [ "$count" -gt 0 ]; then
3737+ echo "$response" | jq -r '.data.paths[] | " • \(.)"'
3838+ else
3939+ echo " No secrets found"
5140 fi
5241else
5342 echo "❌ Failed to list secrets!"
+2-2
helm/hsm-secrets-operator/Chart.yaml
···22name: hsm-secrets-operator
33description: A Kubernetes operator that bridges Pico HSM binary data storage with Kubernetes Secrets
44type: application
55-version: 0.4.4
66-appVersion: v0.4.4
55+version: 0.4.5
66+appVersion: v0.4.5
77icon: https://raw.githubusercontent.com/cncf/artwork/master/projects/kubernetes/icon/color/kubernetes-icon-color.svg
88home: https://github.com/evanjarrett/hsm-secrets-operator
99sources:
+24
internal/agent/client.go
···393393 c.timeout = timeout
394394 c.httpClient.Timeout = timeout
395395}
396396+397397+// WriteSecretWithMetadata writes secret data and metadata to the specified HSM path
398398+func (c *Client) WriteSecretWithMetadata(ctx context.Context, path string, data hsm.SecretData, metadata *hsm.SecretMetadata) error {
399399+ // For now, just write the secret data - metadata support can be added to agent API later
400400+ // This provides compatibility with the updated interface
401401+ if err := c.WriteSecret(ctx, path, data); err != nil {
402402+ return err
403403+ }
404404+405405+ // TODO: Add metadata storage to agent API endpoints
406406+ if metadata != nil {
407407+ c.logger.V(1).Info("Metadata not yet supported in agent API, skipping", "path", path)
408408+ }
409409+410410+ return nil
411411+}
412412+413413+// ReadMetadata reads metadata for a secret at the given path
414414+func (c *Client) ReadMetadata(ctx context.Context, path string) (*hsm.SecretMetadata, error) {
415415+ // TODO: Add metadata reading from agent API endpoints
416416+ // For now, return empty metadata to satisfy interface
417417+ c.logger.V(1).Info("Metadata reading not yet supported in agent API", "path", path)
418418+ return nil, fmt.Errorf("metadata not found for path: %s (agent API doesn't support metadata yet)", path)
419419+}
+17
internal/hsm/client.go
···3535 FirmwareVersion string
3636}
37373838+// SecretMetadata contains metadata about an HSM secret
3939+type SecretMetadata struct {
4040+ Label string `json:"label,omitempty"`
4141+ Description string `json:"description,omitempty"`
4242+ Tags map[string]string `json:"tags,omitempty"`
4343+ Format string `json:"format,omitempty"`
4444+ DataType SecretDataType `json:"dataType,omitempty"`
4545+ CreatedAt string `json:"createdAt,omitempty"`
4646+ Source string `json:"source,omitempty"`
4747+}
4848+3849// Client defines the interface for HSM operations
3950type Client interface {
4051 // Initialize establishes connection to the HSM
···51625263 // WriteSecret writes secret data to the specified HSM path
5364 WriteSecret(ctx context.Context, path string, data SecretData) error
6565+6666+ // WriteSecretWithMetadata writes secret data and metadata to the specified HSM path
6767+ WriteSecretWithMetadata(ctx context.Context, path string, data SecretData, metadata *SecretMetadata) error
6868+6969+ // ReadMetadata reads metadata for a secret at the given path
7070+ ReadMetadata(ctx context.Context, path string) (*SecretMetadata, error)
54715572 // DeleteSecret removes secret data from the specified HSM path
5673 DeleteSecret(ctx context.Context, path string) error
+38-2
internal/hsm/mock_client.go
···3232 mutex sync.RWMutex
3333 connected bool
3434 secrets map[string]SecretData
3535+ metadata map[string]*SecretMetadata
3536 config Config
3637}
37383839// NewMockClient creates a new mock HSM client for testing
3940func NewMockClient() *MockClient {
4041 return &MockClient{
4141- logger: ctrl.Log.WithName("hsm-mock-client"),
4242- secrets: make(map[string]SecretData),
4242+ logger: ctrl.Log.WithName("hsm-mock-client"),
4343+ secrets: make(map[string]SecretData),
4444+ metadata: make(map[string]*SecretMetadata),
4345 }
4446}
4547···142144 return nil
143145}
144146147147+// WriteSecretWithMetadata writes secret data and metadata to the specified HSM path
148148+func (m *MockClient) WriteSecretWithMetadata(ctx context.Context, path string, data SecretData, metadata *SecretMetadata) error {
149149+ if err := m.WriteSecret(ctx, path, data); err != nil {
150150+ return err
151151+ }
152152+153153+ if metadata != nil {
154154+ m.mutex.Lock()
155155+ defer m.mutex.Unlock()
156156+ m.metadata[path] = metadata
157157+ m.logger.V(1).Info("Wrote metadata to mock HSM", "path", path)
158158+ }
159159+160160+ return nil
161161+}
162162+163163+// ReadMetadata reads metadata for a secret at the given path
164164+func (m *MockClient) ReadMetadata(ctx context.Context, path string) (*SecretMetadata, error) {
165165+ m.mutex.RLock()
166166+ defer m.mutex.RUnlock()
167167+168168+ if !m.connected {
169169+ return nil, fmt.Errorf("HSM not connected")
170170+ }
171171+172172+ metadata, exists := m.metadata[path]
173173+ if !exists {
174174+ return nil, fmt.Errorf("metadata not found for path: %s", path)
175175+ }
176176+177177+ return metadata, nil
178178+}
179179+145180// DeleteSecret removes secret data from mock storage
146181func (m *MockClient) DeleteSecret(ctx context.Context, path string) error {
147182 m.mutex.Lock()
···156191 }
157192158193 delete(m.secrets, path)
194194+ delete(m.metadata, path) // Also delete metadata
159195 m.logger.Info("Deleted secret from mock HSM", "path", path)
160196 return nil
161197}
+148
internal/hsm/oids.go
···11+package hsm
22+33+import (
44+ "encoding/asn1"
55+ "fmt"
66+)
77+88+// HSM Secrets Operator OID namespace
99+// Using experimental/private range: 1.3.6.1.4.1.99999 (not officially registered)
1010+// In production, this should be replaced with a properly registered enterprise OID
1111+1212+var (
1313+ // Data type OIDs
1414+ OIDPlaintext = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 99999, 1, 1} // Plain text secrets
1515+ OIDJson = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 99999, 1, 2} // JSON configuration
1616+ OIDPem = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 99999, 1, 3} // PEM encoded certificates/keys
1717+ OIDBinary = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 99999, 1, 4} // Binary data
1818+ OIDBase64 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 99999, 1, 5} // Base64 encoded data
1919+ OIDX509Cert = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 99999, 1, 6} // X.509 certificate
2020+ OIDPrivKey = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 99999, 1, 7} // Private key material
2121+ OIDDockerCfg = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 99999, 1, 8} // Docker config JSON
2222+)
2323+2424+// SecretDataType represents the type of data stored in HSM
2525+type SecretDataType string
2626+2727+const (
2828+ DataTypePlaintext SecretDataType = "plaintext"
2929+ DataTypeJson SecretDataType = "json"
3030+ DataTypePem SecretDataType = "pem"
3131+ DataTypeBinary SecretDataType = "binary"
3232+ DataTypeBase64 SecretDataType = "base64"
3333+ DataTypeX509Cert SecretDataType = "x509-cert"
3434+ DataTypePrivKey SecretDataType = "private-key"
3535+ DataTypeDockerCfg SecretDataType = "docker-config"
3636+)
3737+3838+// GetOIDForDataType returns the OID for a given data type
3939+func GetOIDForDataType(dataType SecretDataType) (asn1.ObjectIdentifier, error) {
4040+ switch dataType {
4141+ case DataTypePlaintext:
4242+ return OIDPlaintext, nil
4343+ case DataTypeJson:
4444+ return OIDJson, nil
4545+ case DataTypePem:
4646+ return OIDPem, nil
4747+ case DataTypeBinary:
4848+ return OIDBinary, nil
4949+ case DataTypeBase64:
5050+ return OIDBase64, nil
5151+ case DataTypeX509Cert:
5252+ return OIDX509Cert, nil
5353+ case DataTypePrivKey:
5454+ return OIDPrivKey, nil
5555+ case DataTypeDockerCfg:
5656+ return OIDDockerCfg, nil
5757+ default:
5858+ return nil, fmt.Errorf("unknown data type: %s", dataType)
5959+ }
6060+}
6161+6262+// GetDataTypeForOID returns the data type for a given OID
6363+func GetDataTypeForOID(oid asn1.ObjectIdentifier) (SecretDataType, error) {
6464+ switch {
6565+ case oid.Equal(OIDPlaintext):
6666+ return DataTypePlaintext, nil
6767+ case oid.Equal(OIDJson):
6868+ return DataTypeJson, nil
6969+ case oid.Equal(OIDPem):
7070+ return DataTypePem, nil
7171+ case oid.Equal(OIDBinary):
7272+ return DataTypeBinary, nil
7373+ case oid.Equal(OIDBase64):
7474+ return DataTypeBase64, nil
7575+ case oid.Equal(OIDX509Cert):
7676+ return DataTypeX509Cert, nil
7777+ case oid.Equal(OIDPrivKey):
7878+ return DataTypePrivKey, nil
7979+ case oid.Equal(OIDDockerCfg):
8080+ return DataTypeDockerCfg, nil
8181+ default:
8282+ return "", fmt.Errorf("unknown OID: %s", oid.String())
8383+ }
8484+}
8585+8686+// EncodeDER returns the DER encoding of an OID for use in PKCS#11 CKA_OBJECT_ID
8787+func EncodeDER(oid asn1.ObjectIdentifier) ([]byte, error) {
8888+ return asn1.Marshal(oid)
8989+}
9090+9191+// DecodeDER decodes a DER-encoded OID from PKCS#11 CKA_OBJECT_ID
9292+func DecodeDER(der []byte) (asn1.ObjectIdentifier, error) {
9393+ var oid asn1.ObjectIdentifier
9494+ _, err := asn1.Unmarshal(der, &oid)
9595+ return oid, err
9696+}
9797+9898+// InferDataType attempts to infer the data type from content
9999+func InferDataType(data []byte) SecretDataType {
100100+ content := string(data)
101101+102102+ // Check for PEM format
103103+ if len(content) > 20 && content[:5] == "-----" {
104104+ return DataTypePem
105105+ }
106106+107107+ // Check for JSON format
108108+ if len(content) > 0 && (content[0] == '{' || content[0] == '[') {
109109+ return DataTypeJson
110110+ }
111111+112112+ // Check if it's valid base64
113113+ if isValidBase64(content) {
114114+ return DataTypeBase64
115115+ }
116116+117117+ // Check for binary data (contains non-printable chars)
118118+ for _, b := range data {
119119+ if b < 32 && b != 9 && b != 10 && b != 13 { // Allow tab, LF, CR
120120+ return DataTypeBinary
121121+ }
122122+ }
123123+124124+ // Default to plaintext
125125+ return DataTypePlaintext
126126+}
127127+128128+// isValidBase64 checks if a string is valid base64
129129+func isValidBase64(s string) bool {
130130+ if len(s) == 0 {
131131+ return false
132132+ }
133133+134134+ // Base64 strings should be multiple of 4 in length (with padding)
135135+ if len(s)%4 != 0 {
136136+ return false
137137+ }
138138+139139+ // Check for valid base64 characters
140140+ for _, c := range s {
141141+ if (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') &&
142142+ (c < '0' || c > '9') && c != '+' && c != '/' && c != '=' {
143143+ return false
144144+ }
145145+ }
146146+147147+ return true
148148+}
+153-2
internal/hsm/pkcs11_client.go
···21212222import (
2323 "context"
2424+ "encoding/json"
2425 "fmt"
2526 "strings"
2627 "sync"
···3233)
33343435const (
3535- defaultKeyName = "data"
3636+ defaultKeyName = "data"
3737+ metadataKeySuffix = "/_metadata"
3838+ applicationName = "hsm-secrets-operator"
3639)
37403841// PKCS11Client implements the Client interface using PKCS#11
···359362 return data, nil
360363}
361364365365+// WriteSecretWithMetadata writes secret data and metadata to the specified HSM path
366366+func (c *PKCS11Client) WriteSecretWithMetadata(ctx context.Context, path string, data SecretData, metadata *SecretMetadata) error {
367367+ if err := c.WriteSecret(ctx, path, data); err != nil {
368368+ return err
369369+ }
370370+371371+ if metadata != nil {
372372+ return c.writeMetadata(path, metadata)
373373+ }
374374+375375+ return nil
376376+}
377377+362378// WriteSecret writes secret data to the specified HSM path
363379func (c *PKCS11Client) WriteSecret(ctx context.Context, path string, data SecretData) error {
364380 c.mutex.Lock()
···383399 label = path + "/" + key
384400 }
385401402402+ // Infer data type from content
403403+ dataType := InferDataType(value)
404404+405405+ // Get OID for data type
406406+ oid, err := GetOIDForDataType(dataType)
407407+ if err != nil {
408408+ c.logger.V(1).Info("Failed to get OID for data type, using default",
409409+ "dataType", dataType, "error", err)
410410+ oid = OIDPlaintext // Default fallback
411411+ }
412412+413413+ // Encode OID as DER
414414+ derOID, err := EncodeDER(oid)
415415+ if err != nil {
416416+ c.logger.V(1).Info("Failed to encode OID as DER", "error", err)
417417+ derOID = nil // Will skip CKA_OBJECT_ID if encoding fails
418418+ }
419419+420420+ // Build template with proper attributes
386421 template := []*pkcs11.Attribute{
387422 pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA),
388423 pkcs11.NewAttribute(pkcs11.CKA_LABEL, label),
424424+ pkcs11.NewAttribute(pkcs11.CKA_APPLICATION, applicationName), // Proper application name
389425 pkcs11.NewAttribute(pkcs11.CKA_VALUE, value),
390426 pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), // Store persistently
391427 pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, true), // Require authentication
392428 pkcs11.NewAttribute(pkcs11.CKA_MODIFIABLE, true), // Allow updates
429429+ }
430430+431431+ // Add OID if we successfully encoded it
432432+ if derOID != nil {
433433+ template = append(template, pkcs11.NewAttribute(pkcs11.CKA_OBJECT_ID, derOID))
393434 }
394435395436 obj, err := c.ctx.CreateObject(c.session, template)
···400441 // Cache the object handle for faster future lookups
401442 c.dataObjects[label] = obj
402443403403- c.logger.V(2).Info("Created data object", "path", path, "key", key, "label", label)
444444+ c.logger.V(2).Info("Created data object", "path", path, "key", key, "label", label, "dataType", dataType)
404445 }
405446406447 c.logger.Info("Successfully wrote secret to HSM", "path", path)
407448 return nil
449449+}
450450+451451+// writeMetadata creates a metadata object for the secret
452452+func (c *PKCS11Client) writeMetadata(path string, metadata *SecretMetadata) error {
453453+ // Serialize metadata to JSON
454454+ metadataJSON, err := json.Marshal(metadata)
455455+ if err != nil {
456456+ return fmt.Errorf("failed to serialize metadata: %w", err)
457457+ }
458458+459459+ // Create metadata object label
460460+ metadataLabel := path + metadataKeySuffix
461461+462462+ // Get OID for JSON data type
463463+ oid, err := GetOIDForDataType(DataTypeJson)
464464+ if err != nil {
465465+ c.logger.V(1).Info("Failed to get OID for JSON metadata", "error", err)
466466+ oid = OIDJson // Fallback
467467+ }
468468+469469+ // Encode OID as DER
470470+ derOID, err := EncodeDER(oid)
471471+ if err != nil {
472472+ c.logger.V(1).Info("Failed to encode metadata OID as DER", "error", err)
473473+ derOID = nil
474474+ }
475475+476476+ // Build metadata object template
477477+ template := []*pkcs11.Attribute{
478478+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA),
479479+ pkcs11.NewAttribute(pkcs11.CKA_LABEL, metadataLabel),
480480+ pkcs11.NewAttribute(pkcs11.CKA_APPLICATION, applicationName),
481481+ pkcs11.NewAttribute(pkcs11.CKA_VALUE, metadataJSON),
482482+ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), // Store persistently
483483+ pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, true), // Require authentication
484484+ pkcs11.NewAttribute(pkcs11.CKA_MODIFIABLE, true), // Allow updates
485485+ }
486486+487487+ // Add OID if we successfully encoded it
488488+ if derOID != nil {
489489+ template = append(template, pkcs11.NewAttribute(pkcs11.CKA_OBJECT_ID, derOID))
490490+ }
491491+492492+ // Create the metadata object
493493+ obj, err := c.ctx.CreateObject(c.session, template)
494494+ if err != nil {
495495+ return fmt.Errorf("failed to create metadata object: %w", err)
496496+ }
497497+498498+ // Cache the metadata object handle
499499+ c.dataObjects[metadataLabel] = obj
500500+501501+ c.logger.V(2).Info("Created metadata object", "path", path, "label", metadataLabel)
502502+ return nil
503503+}
504504+505505+// ReadMetadata reads metadata for a secret at the given path
506506+func (c *PKCS11Client) ReadMetadata(ctx context.Context, path string) (*SecretMetadata, error) {
507507+ c.mutex.RLock()
508508+ defer c.mutex.RUnlock()
509509+510510+ if !c.connected {
511511+ return nil, fmt.Errorf("HSM not connected")
512512+ }
513513+514514+ metadataLabel := path + metadataKeySuffix
515515+516516+ // Find the metadata object
517517+ template := []*pkcs11.Attribute{
518518+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA),
519519+ pkcs11.NewAttribute(pkcs11.CKA_LABEL, metadataLabel),
520520+ }
521521+522522+ if err := c.ctx.FindObjectsInit(c.session, template); err != nil {
523523+ return nil, fmt.Errorf("failed to initialize metadata search: %w", err)
524524+ }
525525+ defer func() {
526526+ if finalErr := c.ctx.FindObjectsFinal(c.session); finalErr != nil {
527527+ c.logger.V(1).Info("Failed to finalize metadata search", "error", finalErr)
528528+ }
529529+ }()
530530+531531+ objs, _, err := c.ctx.FindObjects(c.session, 1)
532532+ if err != nil {
533533+ return nil, fmt.Errorf("failed to find metadata object: %w", err)
534534+ }
535535+536536+ if len(objs) == 0 {
537537+ return nil, fmt.Errorf("metadata not found for path: %s", path)
538538+ }
539539+540540+ // Get the metadata value
541541+ valueAttr, err := c.ctx.GetAttributeValue(c.session, objs[0], []*pkcs11.Attribute{
542542+ pkcs11.NewAttribute(pkcs11.CKA_VALUE, nil),
543543+ })
544544+ if err != nil {
545545+ return nil, fmt.Errorf("failed to get metadata value: %w", err)
546546+ }
547547+548548+ if len(valueAttr) == 0 || len(valueAttr[0].Value) == 0 {
549549+ return nil, fmt.Errorf("metadata object has no value")
550550+ }
551551+552552+ // Parse the JSON metadata
553553+ var metadata SecretMetadata
554554+ if err := json.Unmarshal(valueAttr[0].Value, &metadata); err != nil {
555555+ return nil, fmt.Errorf("failed to parse metadata JSON: %w", err)
556556+ }
557557+558558+ return &metadata, nil
408559}
409560410561// deleteSecretObjects removes all data objects matching the given path prefix