pki

package
v0.5.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 27, 2026 License: BSD-2-Clause Imports: 20 Imported by: 0

README

PKI Package - Centralized Key Management & Signing

This package provides unified key management, certificate handling, and cryptographic signing operations for the VC project.

Overview

The PKI package consolidates:

  • SignerConfig: High-level configuration object for signing and encryption operations
  • KeyLoader: Centralized key and certificate loading with multiple format support
  • Signer Interface: Abstraction for signing operations (software and HSM)
  • Algorithm Detection: Optimized JWT signing method selection
  • Legacy Functions: Backward-compatible utilities (marked deprecated where applicable)

Features

  • Multiple Key Format Support: PKCS#8, PKCS#1 (RSA), SEC1 (EC)
  • Certificate Chain Loading: Automatic parsing and base64 encoding for x5c
  • Type Validation: Ensure keys match expected types (RSA, ECDSA)
  • Optimized Algorithm Detection: O(1) map-based lookups for signing method selection
  • Thread-Safe: All KeyLoader methods safe for concurrent use
  • HSM Support: PKCS#11 hardware security modules (with build tag pkcs11)
  • Centralized Error Handling: Consistent error messages across the codebase
  • Lazy Loading: Keys loaded on first use with thread-safe initialization

Performance Optimizations

  • Map-based Algorithm Selection: Replaced switch statements with hash maps for O(1) lookups
  • Unified Hash Functions: Single algorithmHashMap for all hash selections
  • Curve Normalization: Direct elliptic.Curve comparison instead of string parsing
  • Reduced Allocations: Streamlined signing path with pre-computed hashes
  • Lazy Initialization: SignerConfig loads keys only when needed

Usage

The SignerConfig provides the highest-level abstraction. Services create a config and call methods directly:

import "vc/pkg/pki"

// File-based keys
signerConfig := pki.NewSignerConfig(&pki.KeyConfig{
    PrivateKeyPath:  "/path/to/key.pem",
    ChainPath: "/path/to/chain.pem",
})

// Sign arbitrary data
signature, err := signerConfig.Sign([]byte("data to sign"))

// Sign JWT with claims
token, err := signerConfig.SignJWT(jwt.MapClaims{
    "sub": "user123",
    "exp": time.Now().Add(time.Hour).Unix(),
})

// Get public key as JWK
jwk, err := signerConfig.GetJWK()

// Get certificate
cert, err := signerConfig.GetCertificate()
SignerConfig with HSM
// HSM-based keys
signerConfig := pki.NewSignerConfig(&pki.KeyConfig{
    PrivateKeyPath: "my-signing-key", // HSM label
    PKCS11: &pki.PKCS11Config{
        ModulePath: "/usr/lib/softhsm/libsofthsm2.so",
        SlotID:     0,
        PIN:        "1234",
        KeyLabel:   "signing-key",
    },
})

// Same interface works for HSM!
token, err := signerConfig.SignJWT(claims)
SignerConfig with Fallback
// Try HSM first, fall back to file
signerConfig := pki.NewSignerConfig(&pki.KeyConfig{
    PrivateKeyPath:  "/backup/key.pem",
    ChainPath: "/backup/chain.pem",
    PKCS11: &pki.PKCS11Config{
        ModulePath: "/usr/lib/softhsm/libsofthsm2.so",
        SlotID:     0,
        PIN:        "1234",
        KeyLabel:   "signing-key",
    },
    Priority:   []pki.KeySource{pki.KeySourceHSM, pki.KeySourceFile},
    EnableHSM:  true,
    EnableFile: true,
})

// Automatically uses HSM if available, falls back to file
signature, err := signerConfig.Sign(data)
Example: Service Integration
type TokenService struct {
    signer *pki.SignerConfig
}

func NewTokenService(keyPath, chainPath string) *TokenService {
    return &TokenService{
        signer: pki.NewSignerConfig(&pki.KeyConfig{
            PrivateKeyPath:  keyPath,
            ChainPath: chainPath,
        }),
    }
}

func (ts *TokenService) GenerateAccessToken(userID string) (string, error) {
    claims := jwt.MapClaims{
        "sub": userID,
        "iss": "my-service",
        "iat": time.Now().Unix(),
        "exp": time.Now().Add(time.Hour).Unix(),
    }
    
    // SignerConfig handles all the complexity
    return ts.signer.SignJWT(claims)
}

func (ts *TokenService) GetPublicKey() (*jose.JSONWebKey, error) {
    return ts.signer.GetJWK()
}
KeyConfig Approach (Lower-level)

Use KeyConfig directly with KeyLoader when you need more control:

import "vc/pkg/pki"

keyLoader := pki.NewKeyLoader()

// File-based keys
config := &pki.KeyConfig{
    PrivateKeyPath:  "/path/to/key.pem",
    ChainPath: "/path/to/chain.pem",
}
km, err := keyLoader.LoadKeyMaterial(config)

// HSM-based keys
config := &pki.KeyConfig{
    PrivateKeyPath: "my-signing-key", // HSM label
    HSM: &pki.PKCS11Config{
        ModulePath: "/usr/lib/softhsm/libsofthsm2.so",
        SlotID:     0,
        PIN:        "1234",
        KeyID:      "hsm-key-1",
    },
}
km, err := keyLoader.LoadKeyMaterial(config)

// Fallback: try HSM first, then file
config := &pki.KeyConfig{
    PrivateKeyPath:  "/backup/key.pem",
    ChainPath: "/backup/chain.pem",
    HSM: &pki.PKCS11Config{...},
    Priority: []pki.KeySource{pki.KeySourceHSM, pki.KeySourceFile},
}
km, err := keyLoader.LoadKeyMaterial(config)

// Explicit control with enable flags
config := &pki.KeyConfig{
    PrivateKeyPath:   "/path/to/key.pem",
    HSM:        &pki.PKCS11Config{...},
    EnableFile: true,  // Use file
    EnableHSM:  false, // Don't use HSM even though configured
}
km, err := keyLoader.LoadKeyMaterial(config)

Key insight: All key source configuration is in KeyConfig, making it easy to:

  • Switch between file and HSM by changing config
  • Support fallback scenarios with Priority
  • Enable/disable sources with flags
  • Store configuration in YAML/JSON
Basic Key Loading (Low-level)

For more explicit control when needed:

import "vc/pkg/pki"

keyLoader := pki.NewKeyLoader()

// Load just a private key
key, err := keyLoader.LoadPrivateKey("/path/to/key.pem")
if err != nil {
    return err
}

// Explicitly load from file (even if PKCS11Config is set)
km, err := keyLoader.LoadKeyMaterialFromFile("/path/to/key.pem", "/path/to/chain.pem")
if err != nil {
    return err
}

// Access the loaded materials
privateKey := km.PrivateKey
cert := km.Cert             // First certificate in chain
chain := km.Chain           // Base64-encoded DER certificates for x5c
signingMethod := km.SigningMethod  // jwt.SigningMethod for JWT operations
Advanced: Explicit Source Control
// Override automatic detection
keyLoader := pki.NewKeyLoaderWithPKCS11(&pki.PKCS11Config{...})
keyLoader.DefaultSource = pki.KeySourceFile  // Force file loading

// Now LoadKeyMaterial uses files even though PKCS11Config is set
km, err := keyLoader.LoadKeyMaterial("/path/to/key.pem", "")

// Or explicitly specify source per call
km, err := keyLoader.LoadKeyMaterialFromFile("/path/to/key.pem", "")  // Always file
km, err := keyLoader.LoadKeyMaterialFromHSM("key-label")                // Always HSM (deprecated)
Example: Source-Agnostic Component
Example: Source-Agnostic Component
// Component that doesn't care about key source
type Signer struct {
    keyLoader *pki.KeyLoader
    keyID     string
    chainID   string
}

func (s *Signer) Sign(data []byte) ([]byte, error) {
    // Same code works for both file and HSM keys!
    km, err := s.keyLoader.LoadKeyMaterial(s.keyID, s.chainID)
    if err != nil {
        return nil, err
    }
    
    // Sign using standard crypto.Signer interface
    signer := km.PrivateKey.(crypto.Signer)
    hash := crypto.SHA256.New()
    hash.Write(data)
    
    return signer.Sign(rand.Reader, hash.Sum(nil), crypto.SHA256)
}

// Usage with files
fileSigner := &Signer{
    keyLoader: pki.NewKeyLoader(),
    keyID:     "/etc/keys/signing.pem",
    chainID:   "/etc/keys/chain.pem",
}

// Usage with HSM (same component code!)
hsmSigner := &Signer{
    keyLoader: pki.NewKeyLoaderWithPKCS11(&pki.PKCS11Config{...}),
    keyID:     "hsm-signing-key",  // HSM label
    chainID:   "",                   // Chain from HSM or empty
}
Loading Keys from HSM (Legacy Example)

For backward compatibility, explicit HSM methods still exist:

// Build with: go build -tags=pkcs11

// Create KeyLoader with HSM configuration
keyLoader := pki.NewKeyLoaderWithPKCS11(&pki.PKCS11Config{
    ModulePath: "/usr/lib/softhsm/libsofthsm2.so",
    SlotID:     0,
    PIN:        "1234",
    KeyID:      "hsm-key-1",
})

// Deprecated: Explicit HSM loading (use LoadKeyMaterial instead)
km, err := keyLoader.LoadKeyMaterialFromHSM("my-signing-key")
if err != nil {
    return err
}

// Load certificate from HSM (optional)
cert, err := keyLoader.LoadCertificateFromHSM("my-cert")

// Use the HSM key with standard crypto interfaces
// km.PrivateKey implements crypto.Signer
signature, err := km.PrivateKey.(crypto.Signer).Sign(rand.Reader, digest, crypto.SHA256)
Signing Operations
import "vc/pkg/pki"

// Create a software signer
key, _ := keyLoader.LoadPrivateKey("/path/to/key.pem")
signer, err := pki.NewSoftwareSigner(key, "my-key-id")
if err != nil {
    return err
}

// Sign data
signature, err := signer.Sign(context.Background(), dataToSign)
if err != nil {
    return err
}

// Access signer properties
algorithm := signer.Algorithm()  // e.g., "RS256", "ES256"
keyID := signer.KeyID()          // "my-key-id"
pubKey := signer.PublicKey()     // Public key for verification
HSM Signing (with PKCS#11)
// Build with: go build -tags=pkcs11

signer, err := pki.NewPKCS11Signer(&pki.PKCS11Config{
    ModulePath: "/usr/lib/softhsm/libsofthsm2.so",
    SlotID:     0,
    PIN:        "1234",
    KeyLabel:   "signing-key",
    KeyID:      "hsm-key-1",
})
if err != nil {
    return err
}
defer signer.Close()

// Use same Signer interface as software
signature, err := signer.Sign(ctx, data)
Key Type Validation
keyLoader := pki.NewKeyLoader()
key, _ := keyLoader.LoadPrivateKey("/path/to/key.pem")

// Validate it's an RSA key
if err := keyLoader.ValidateKeyType(key, "RSA"); err != nil {
    return fmt.Errorf("invalid key type: %w", err)
}

// Get algorithm for the key
alg, err := keyLoader.GetKeyAlgorithm(key)
// Returns: "RS256" for RSA, "ES256"/"ES384"/"ES512" for ECDSA

Supported Key Formats

PKCS#8 (Preferred)
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----

Supports both RSA and ECDSA keys.

PKCS#1 (RSA)
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----

RSA keys only.

SEC1 (EC)
-----BEGIN EC PRIVATE KEY-----
...
-----END EC PRIVATE KEY-----

ECDSA keys only.

Certificate Chain Format

Certificate chains should be in PEM format with multiple certificates:

-----BEGIN CERTIFICATE-----
(Signing certificate)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
(Intermediate CA)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
(Root CA)
-----END CERTIFICATE-----

Components Using PKI Package

Using KeyLoader
  • Verifier (internal/verifier/apiv1): OpenID4VP request object signing, OIDC Provider tokens
  • OAuth2 (pkg/oauth2): Authorization server metadata signing
  • OpenID4VCI (pkg/openid4vci): Credential issuer metadata signing
Using KeyLoader with HSM
  • Issuer (internal/issuer/apiv1): Can load keys from HSM for credential signing
  • Any component: KeyLoader supports both file-based and HSM-based key loading
Using Signer Interface
  • Issuer (internal/issuer/apiv1): Credential signing with software or HSM keys
  • Registry (internal/registry): Token status list signing

HSM Integration Details

KeyLoader now supports PKCS#11 HSM integration through two approaches:

// Create loader with HSM config
kl := pki.NewKeyLoaderWithPKCS11(&pki.PKCS11Config{...})

// Load key material from HSM
km, err := kl.LoadKeyMaterialFromHSM("key-label")

// km.PrivateKey is a PKCS11PrivateKey that implements crypto.Signer
// It can be used with standard Go crypto interfaces
2. PKCS11Signer (Legacy, for compatibility)
// Direct signer creation for backward compatibility
signer, err := pki.NewPKCS11Signer(&pki.PKCS11Config{...})
defer signer.Close()

// Use Signer interface
sig, err := signer.Sign(ctx, data)

Key Difference:

  • KeyLoader returns crypto.Signer compatible keys that work with standard crypto packages
  • PKCS11Signer implements the custom Signer interface with automatic session management
  • Both require -tags=pkcs11 build flag

Algorithm Selection

The package automatically selects appropriate algorithms based on key properties:

RSA Keys
  • 2048-3071 bits → RS256 (SHA-256)
  • 3072-4095 bits → RS384 (SHA-384)
  • 4096+ bits → RS512 (SHA-512)
ECDSA Keys
  • P-256 (secp256r1) → ES256 (SHA-256)
  • P-384 (secp384r1) → ES384 (SHA-384)
  • P-521 (secp521r1) → ES512 (SHA-512)

Algorithm selection uses optimized map lookups for O(1) performance.

Migration Notes

From Custom Key Loading
// Before
keyData, err := os.ReadFile(keyPath)
block, _ := pem.Decode(keyData)
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
// ... multiple fallback attempts ...

// After
keyLoader := pki.NewKeyLoader()
km, err := keyLoader.LoadKeyMaterial(keyPath, chainPath)

Architecture

Type Hierarchy
KeyLoader (unified interface)
├── LoadKeyMaterial(id, chain) → auto-detects source
│   ├── → LoadPrivateKey(path) if file-based
│   └── → loadKeyMaterialFromHSM(label) if HSM-based
├── LoadKeyMaterialFromFile(path, chain) → explicit file
└── LoadKeyMaterialFromHSM(label) → explicit HSM (deprecated)

Returns: KeyMaterial
├── PrivateKey: crypto.PrivateKey (crypto.Signer compatible)
├── Cert: *x509.Certificate
├── Chain: []string (base64-encoded DER)
└── SigningMethod: jwt.SigningMethod
Design Benefits
  1. Source Transparency: Components don't need to know if keys come from files or HSM
  2. Configuration-Based: Key source determined by KeyLoader setup, not by calling code
  3. Standard Interfaces: All keys implement crypto.Signer, work with standard Go crypto
  4. Backward Compatible: Explicit methods still available for migration
  5. Zero Code Changes: Switch from files to HSM by changing only KeyLoader initialization
Internal Optimizations
  • algorithmHashMap: O(1) hash function lookup by algorithm name
  • ecdsaSigningMethods: O(1) signing method lookup by curve bits
  • getCurveBits(): Direct curve comparison instead of string parsing
  • getSigningMethod(): Unified algorithm detection used by both KeyLoader and SoftwareSigner

Future Enhancements

Planned features:

  1. Key Caching: In-memory cache to avoid repeated disk reads
  2. Key Watching: Auto-reload on file changes
  3. Cloud KMS: AWS KMS, Azure Key Vault, Google Cloud KMS
  4. Byte Array Input: Load keys from memory instead of files only
  5. Key Rotation: Automatic key rotation support with graceful handoff
  6. Metrics: Track key usage, signature operations, and performance

Testing

See keyloader_test.go for examples of testing key loading functionality.

go test ./pkg/pki/...

Error Handling

KeyLoader provides detailed error messages:

  • "failed to read key file": File not found or permission denied
  • "failed to decode PEM block": Invalid PEM format
  • "failed to parse PKCS#8 private key": Invalid key encoding
  • "unsupported key type": Unknown PEM block type
  • "expected RSA key, got *ecdsa.PrivateKey": Type validation failed

See Also

  • Original key parsing: pki.go
  • JWK operations: jwk.go

Documentation

Overview

Package pki provides interfaces and implementations for cryptographic signing operations. It supports multiple backends including software keys and PKCS#11 hardware security modules.

Example (TransparentKeyLoading)

Example demonstrates transparent key loading with KeyConfig

// Example: Source-transparent key loading using KeyConfig
// This demonstrates how components don't need to care about key sources

package main

import (
	"crypto"
	"crypto/rand"
	"fmt"
	"github.com/SUNET/vc/pkg/pki"
)

// SigningService doesn't care whether keys come from files or HSM
type SigningService struct {
	keyLoader *pki.KeyLoader
	keyConfig *pki.KeyConfig
}

func (s *SigningService) Sign(data []byte) ([]byte, error) {
	// Same code for both file and HSM keys!
	km, err := s.keyLoader.LoadKeyMaterial(s.keyConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to load key: %w", err)
	}

	// Use standard crypto.Signer interface
	signer := km.PrivateKey.(crypto.Signer)
	hash := crypto.SHA256.New()
	hash.Write(data)
	digest := hash.Sum(nil)

	signature, err := signer.Sign(rand.Reader, digest, crypto.SHA256)
	if err != nil {
		return nil, fmt.Errorf("signing failed: %w", err)
	}

	fmt.Printf("Signed with algorithm: %s\n", km.SigningMethod.Alg())
	return signature, nil
}

// Example demonstrates transparent key loading with KeyConfig
func main() {
	data := []byte("data to sign")
	keyLoader := pki.NewKeyLoader()

	// Scenario 1: Development with file-based keys
	fmt.Println("=== Development (File-based keys) ===")
	devConfig := &pki.KeyConfig{
		PrivateKeyPath: "/path/to/dev/key.pem",
		ChainPath:      "/path/to/dev/chain.pem",
	}
	devService := &SigningService{
		keyLoader: keyLoader,
		keyConfig: devConfig,
	}
	sig, err := devService.Sign(data)
	if err != nil {
		fmt.Printf("Dev signing error: %v\n", err)
	} else {
		fmt.Printf("Signature length: %d bytes\n\n", len(sig))
	}

	// Scenario 2: Production with HSM keys
	// SAME component code, just different KeyConfig!
	fmt.Println("=== Production (HSM keys) ===")
	prodConfig := &pki.KeyConfig{
		PrivateKeyPath: "production-signing-key", // HSM label
		PKCS11: &pki.PKCS11Config{
			ModulePath: "/usr/lib/softhsm/libsofthsm2.so",
			SlotID:     0,
			PIN:        "1234",
			KeyID:      "prod-key-1",
		},
	}
	prodService := &SigningService{
		keyLoader: keyLoader,
		keyConfig: prodConfig,
	}
	sig, err = prodService.Sign(data)
	if err != nil {
		fmt.Printf("Production signing error: %v\n", err)
	} else {
		fmt.Printf("Signature length: %d bytes\n", len(sig))
	}

	// Scenario 3: Fallback configuration (try HSM first, fall back to file)
	fmt.Println("=== Fallback (HSM → File) ===")
	fallbackConfig := &pki.KeyConfig{
		PrivateKeyPath: "/backup/key.pem",
		ChainPath:      "/backup/chain.pem",
		PKCS11: &pki.PKCS11Config{
			ModulePath: "/usr/lib/softhsm/libsofthsm2.so",
			SlotID:     0,
			PIN:        "1234",
			KeyID:      "fallback-key",
		},
		Priority: []pki.KeySource{pki.KeySourceHSM, pki.KeySourceFile},
	}
	fallbackService := &SigningService{
		keyLoader: keyLoader,
		keyConfig: fallbackConfig,
	}
	sig, err = fallbackService.Sign(data)
	if err != nil {
		fmt.Printf("Fallback signing error: %v\n", err)
	} else {
		fmt.Printf("Signature length: %d bytes\n", len(sig))
	}

	// Key benefits:
	// 1. SigningService code is identical for all scenarios
	// 2. Switch sources by changing only KeyConfig
	// 3. No conditional logic based on key source
	// 4. Support fallback scenarios with Priority
	// 5. Enable/disable sources with flags
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrPKCS11NotSupported = errors.New("PKCS#11 support not compiled in; rebuild with -tags=pkcs11")

ErrPKCS11NotSupported is returned when PKCS#11 support is not compiled in.

Functions

func DecodeECDSASignature

func DecodeECDSASignature(signature []byte, curve elliptic.Curve) (*big.Int, *big.Int, error)

DecodeECDSASignature decodes an IEEE P1363 format ECDSA signature to (r, s) big integers. This is the inverse of EncodeECDSASignature.

func EncodeECDSASignature

func EncodeECDSASignature(r, s *big.Int, curve elliptic.Curve) ([]byte, error)

EncodeECDSASignature converts ECDSA signature components (r, s) to IEEE P1363 format. This is the fixed-size R||S concatenation format required by JWT (RFC 7518 section 3.4). ASN.1 DER encoding is NOT used for JWT ECDSA signatures.

func GetKeySizeForCurve

func GetKeySizeForCurve(curve elliptic.Curve) int

GetKeySizeForCurve returns the key size in bytes for a given curve.

func ParseCertificate added in v0.5.4

func ParseCertificate(der []byte, ext *cryptoutil.Extensions) (*x509.Certificate, error)

ParseCertificate parses a DER-encoded certificate using extension-aware parsing if ext is non-nil, falling back to standard x509.ParseCertificate.

Types

type KeyConfig

type KeyConfig struct {
	// File-based configuration
	PrivateKeyPath string `yaml:"private_key_path" validate:"required_without=PKCS11"` // Path to PEM key file
	ChainPath      string `yaml:"chain_path"`                                          // Path to certificate chain (optional)

	// HSM-based configuration
	PKCS11 *PKCS11Config `yaml:"pkcs11" validate:"required_without=PrivateKeyPath"` // PKCS#11 HSM config (optional)

	// Source selection (determines which config to use)
	// If empty, tries in order: File (if FilePath set), then HSM (if HSM set)
	Source KeySource `yaml:"source"`

	// EnableFile enables file-based key loading (default: true if FilePath set)
	EnableFile bool `yaml:"enable_file"`
	// EnableHSM enables HSM-based key loading (default: true if HSM set)
	EnableHSM bool `yaml:"enable_hsm"`

	// Priority defines fallback order when both are enabled
	// If nil, uses Source field or auto-detects based on what's configured
	Priority []KeySource `yaml:"priority" doc_example:"[\"hsm\", \"file\"]"`
}

KeyConfig holds configuration for loading keys from various sources. Supports both file-based and HSM-based keys with explicit control.

type KeyLoader

type KeyLoader struct {
	// CryptoExt provides extended algorithm and certificate support
	// (e.g. brainpool curves). Must be set before concurrent use and
	// not mutated afterwards.
	CryptoExt *cryptoutil.Extensions
}

KeyLoader provides centralized key and certificate loading functionality. Methods are safe for concurrent use after initialization.

func NewKeyLoader

func NewKeyLoader() *KeyLoader

NewKeyLoader creates a new KeyLoader instance.

Example
package main

import (
	"fmt"

	"github.com/SUNET/vc/pkg/pki"
)

func main() {
	kl := pki.NewKeyLoader()
	fmt.Printf("%T\n", kl)
}
Output:
*pki.KeyLoader

func NewKeyLoaderWithExtensions added in v0.5.4

func NewKeyLoaderWithExtensions(ext *cryptoutil.Extensions) *KeyLoader

NewKeyLoaderWithExtensions creates a new KeyLoader with crypto extensions.

func (*KeyLoader) GetKeyAlgorithm

func (kl *KeyLoader) GetKeyAlgorithm(key crypto.PrivateKey) (string, error)

GetKeyAlgorithm returns the algorithm name for a given key

func (*KeyLoader) LoadCertificateChain

func (kl *KeyLoader) LoadCertificateChain(path string) ([]*x509.Certificate, []string, error)

LoadCertificateChain loads a certificate chain from a PEM file. Returns the certificates and their base64-encoded DER format for x5c.

func (*KeyLoader) LoadKeyMaterial

func (kl *KeyLoader) LoadKeyMaterial(config *KeyConfig) (*KeyMaterial, error)

LoadKeyMaterial loads a private key based on the provided configuration. The config determines whether to load from file, HSM, or try multiple sources.

func (*KeyLoader) LoadPrivateKey

func (kl *KeyLoader) LoadPrivateKey(path string) (crypto.PrivateKey, error)

LoadPrivateKey loads a private key from a PEM file. Supports PKCS#8, PKCS#1 (RSA), and SEC1 (EC) formats.

func (*KeyLoader) ValidateKeyType

func (kl *KeyLoader) ValidateKeyType(key crypto.PrivateKey, expectedType string) error

ValidateKeyType checks if the private key is of the expected type

type KeyMaterial

type KeyMaterial struct {
	PrivateKey    crypto.PrivateKey
	Cert          *x509.Certificate
	Chain         []string          // Base64-encoded DER certificates for x5c
	SigningMethod jwt.SigningMethod // JWT signing method determined from key type
}

KeyMaterial holds a private key with its optional certificate and chain

type KeyMaterialSigner

type KeyMaterialSigner struct {
	// contains filtered or unexported fields
}

KeyMaterialSigner implements the Signer interface using KeyMaterial. It provides a concrete implementation for services that need signing capabilities.

Example

ExampleKeyMaterialSigner demonstrates using the Signer interface implementation

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/SUNET/vc/pkg/pki"
)

func main() {
	// Load key material
	keyLoader := pki.NewKeyLoader()
	km, err := keyLoader.LoadKeyMaterial(&pki.KeyConfig{
		PrivateKeyPath: "/path/to/private-key.pem",
		ChainPath:      "/path/to/certificate-chain.pem",
	})
	if err != nil {
		log.Fatalf("failed to load key material: %v", err)
	}

	// Create a signer from the key material
	signer := pki.NewKeyMaterialSigner(km)

	// Sign data using the Signer interface
	ctx := context.Background()
	data := []byte("data to sign")
	signature, err := signer.Sign(ctx, data)
	if err != nil {
		log.Fatalf("failed to sign: %v", err)
	}

	fmt.Printf("Algorithm: %s\n", signer.Algorithm())
	fmt.Printf("Key ID: %s\n", signer.KeyID())
	fmt.Printf("Signature: %d bytes\n", len(signature))
}
Example (WithCredentials)

ExampleKeyMaterialSigner_withCredentials demonstrates using KeyMaterialSigner with credential issuance

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/SUNET/vc/pkg/pki"
)

func main() {
	// Load signing key
	keyLoader := pki.NewKeyLoader()
	km, err := keyLoader.LoadKeyMaterial(&pki.KeyConfig{
		PrivateKeyPath: "/etc/issuer/signing-key.pem",
		ChainPath:      "/etc/issuer/chain.pem",
	})
	if err != nil {
		log.Fatalf("failed to load key: %v", err)
	}

	// Create signer implementing the Signer interface
	signer := pki.NewKeyMaterialSigner(km)

	// Function that requires a Signer interface (like credential issuance libraries)
	issueCredential := func(s pki.Signer, claims map[string]any) error {
		ctx := context.Background()
		// Credential issuance would use:
		// - s.Sign(ctx, data) for signing
		// - s.Algorithm() for JWT header
		// - s.KeyID() for kid claim
		// - s.PublicKey() for verification
		fmt.Printf("Issuing credential with key: %s\n", s.KeyID())
		fmt.Printf("Using algorithm: %s\n", s.Algorithm())

		// Example signing
		data := []byte("credential data")
		_, err := s.Sign(ctx, data)
		return err
	}

	// Pass the signer to credential issuance
	err = issueCredential(signer, map[string]any{
		"sub": "did:example:123",
		"vc": map[string]any{
			"type": []string{"VerifiableCredential"},
		},
	})
	if err != nil {
		log.Fatalf("failed to issue credential: %v", err)
	}
}

func NewKeyMaterialSigner

func NewKeyMaterialSigner(km *KeyMaterial) *KeyMaterialSigner

NewKeyMaterialSigner creates a new Signer from KeyMaterial. The keyID is automatically determined from the certificate if available, or generated from the public key hash.

func (*KeyMaterialSigner) Algorithm

func (s *KeyMaterialSigner) Algorithm() string

Algorithm returns the JWT algorithm name based on the key type.

func (*KeyMaterialSigner) GetCertificate

func (s *KeyMaterialSigner) GetCertificate() *x509.Certificate

GetCertificate returns the certificate if available.

func (*KeyMaterialSigner) GetCertificateChain

func (s *KeyMaterialSigner) GetCertificateChain() []string

GetCertificateChain returns the certificate chain if available.

func (*KeyMaterialSigner) KeyID

func (s *KeyMaterialSigner) KeyID() string

KeyID returns the key identifier for JWT headers.

func (*KeyMaterialSigner) PrivateKey

func (s *KeyMaterialSigner) PrivateKey() crypto.PrivateKey

PrivateKey returns the underlying private key. This is useful when integrating with libraries that need the raw crypto.PrivateKey.

func (*KeyMaterialSigner) PublicKey

func (s *KeyMaterialSigner) PublicKey() any

PublicKey returns the public key for verification.

func (*KeyMaterialSigner) Sign

func (s *KeyMaterialSigner) Sign(ctx context.Context, data []byte) ([]byte, error)

Sign signs the provided data using the private key.

func (*KeyMaterialSigner) SignDigest

func (s *KeyMaterialSigner) SignDigest(ctx context.Context, digest []byte) ([]byte, error)

SignDigest signs a pre-computed digest without additional hashing. This is useful for protocols like W3C Data Integrity that control the hashing process.

func (*KeyMaterialSigner) SigningMethod

func (s *KeyMaterialSigner) SigningMethod() jwt.SigningMethod

SigningMethod returns the JWT signing method for this key material.

type KeySource

type KeySource int

KeySource indicates where keys are loaded from

const (
	// KeySourceFile loads from filesystem
	KeySourceFile KeySource = iota
	// KeySourceHSM loads from HSM
	KeySourceHSM
)

type PKCS11Config

type PKCS11Config struct {
	// ModulePath is the path to the PKCS#11 library
	ModulePath string `yaml:"module_path" doc_example:"\"/usr/lib/softhsm/libsofthsm2.so\""`
	// SlotID is the HSM slot ID
	SlotID uint `yaml:"slot_id" doc_example:"0"`
	// PIN is the user PIN for the slot
	PIN string `yaml:"pin" doc_example:"\"1234\""`
	// KeyLabel is the label of the key to use
	KeyLabel string `yaml:"key_label" doc_example:"\"my-signing-key\""`
	// KeyID is the identifier for the JWT kid header
	KeyID string `yaml:"key_id" doc_example:"\"key-1\""`
}

PKCS11Config holds configuration for PKCS#11 HSM connection.

type PKCS11Signer

type PKCS11Signer struct{}

PKCS11Signer is a stub when PKCS#11 support is not compiled in.

func NewPKCS11Signer

func NewPKCS11Signer(config *PKCS11Config) (*PKCS11Signer, error)

NewPKCS11Signer returns an error when PKCS#11 support is not compiled in.

func (*PKCS11Signer) Algorithm

func (s *PKCS11Signer) Algorithm() string

Algorithm is not supported without PKCS#11.

func (*PKCS11Signer) Close

func (s *PKCS11Signer) Close() error

Close is a no-op without PKCS#11.

func (*PKCS11Signer) KeyID

func (s *PKCS11Signer) KeyID() string

KeyID is not supported without PKCS#11.

func (*PKCS11Signer) PublicKey

func (s *PKCS11Signer) PublicKey() any

PublicKey is not supported without PKCS#11.

func (*PKCS11Signer) Sign

func (s *PKCS11Signer) Sign(ctx context.Context, data []byte) ([]byte, error)

Sign is not supported without PKCS#11.

type RawSigner

type RawSigner interface {
	Signer

	// SignDigest signs a pre-computed digest without additional hashing.
	//
	// For ECDSA, the digest should be the appropriate size for the curve:
	// - P-256: SHA-256 (32 bytes)
	// - P-384: SHA-384 (48 bytes)
	// - P-521: SHA-512 (64 bytes)
	//
	// For EdDSA (Ed25519), note that Ed25519 does not use pre-hashing in standard mode.
	// The implementation may need to use Ed25519ph (pre-hashed) or handle this specially.
	//
	// The returned signature is in IEEE P1363 format for ECDSA (R||S concatenation).
	SignDigest(ctx context.Context, digest []byte) ([]byte, error)
}

RawSigner extends Signer with direct signature operations for advanced use cases like W3C Data Integrity proofs where the caller controls hashing.

This interface is particularly useful for: - VC 2.0 Data Integrity cryptosuites (ecdsa-rdfc-2019, eddsa-rdfc-2022, etc.) - Any protocol that requires signing pre-computed digests - Fine-grained control over the hash-then-sign process

type Signer

type Signer interface {
	// Sign signs the provided data and returns the signature.
	//
	// For ECDSA algorithms (ES256, ES384, ES512), the returned signature SHOULD be
	// in IEEE P1363 format (fixed-size R||S concatenation) as required by JWS (RFC 7518 §3.4).
	// If the implementation returns ASN.1 DER-encoded ECDSA signatures instead (as is
	// common with crypto.Signer and some HSM backends), the jose.MakeJWT function will
	// automatically convert them to JWS format. However, returning IEEE P1363 directly
	// avoids the conversion overhead.
	//
	// For RSA algorithms (RS256, RS384, RS512), the signature is PKCS#1 v1.5 encoded
	// and requires no format conversion.
	Sign(ctx context.Context, data []byte) ([]byte, error)

	// Algorithm returns the JWT algorithm name (e.g., "RS256", "ES256").
	Algorithm() string

	// KeyID returns the key identifier for the JWT kid header.
	KeyID() string

	// PublicKey returns the public key for verification purposes.
	PublicKey() any
}

Signer defines the interface for cryptographic signing operations. Implementations can use software keys, HSMs via PKCS#11, cloud KMS, etc.

func LoadSigner

func LoadSigner(cfg *KeyConfig) (Signer, *x509.Certificate, []string, error)

LoadSigner loads key material and creates a pki.Signer from the configuration. Supports both software keys and HSM-backed keys. Returns the signer, signing certificate, and certificate chain.

type SignerConfig

type SignerConfig struct {
	// KeyConfig contains the underlying key configuration
	KeyConfig
	// contains filtered or unexported fields
}

SignerConfig provides a high-level interface for cryptographic operations without requiring services to directly handle KeyMaterial. It supports both file-based keys and HSM-based keys through PKCS#11.

Example (Fallback)

ExampleSignerConfig_fallback demonstrates fallback from HSM to file

package main

import (
	"fmt"
	"log"

	"github.com/SUNET/vc/pkg/pki"
)

func main() {
	// Configure with both HSM and file-based keys, preferring HSM
	config := pki.NewSignerConfig(&pki.KeyConfig{
		PrivateKeyPath: "/path/to/backup-key.pem",
		ChainPath:      "/path/to/backup-chain.pem",
		PKCS11: &pki.PKCS11Config{
			ModulePath: "/usr/lib/softhsm/libsofthsm2.so",
			PIN:        "1234",
			KeyLabel:   "signing-key",
		},
		Priority:   []pki.KeySource{pki.KeySourceHSM, pki.KeySourceFile},
		EnableHSM:  true,
		EnableFile: true,
	})

	// Sign - will try HSM first, fall back to file if HSM fails
	data := []byte("data to sign")
	signature, err := config.Sign(data)
	if err != nil {
		log.Fatalf("failed to sign data: %v", err)
	}
	fmt.Printf("Signed with fallback: %d bytes\n", len(signature))
}
Example (FileBasedSigning)

ExampleSignerConfig_fileBasedSigning demonstrates using SignerConfig with file-based keys

// Create a signer config with file-based keys
config := pki.NewSignerConfig(&pki.KeyConfig{
	PrivateKeyPath: "/path/to/private-key.pem",
	ChainPath:      "/path/to/certificate-chain.pem",
})

// Sign arbitrary data
data := []byte("important data to sign")
signature, err := config.Sign(data)
if err != nil {
	log.Fatalf("failed to sign data: %v", err)
}
fmt.Printf("Signature length: %d bytes\n", len(signature))

// Sign a JWT
claims := jwt.MapClaims{
	"sub": "user123",
	"iss": "my-service",
	"exp": 1234567890,
}
token, err := config.SignJWT(claims)
if err != nil {
	log.Fatalf("failed to sign JWT: %v", err)
}
fmt.Printf("JWT: %s\n", token)

// Get public key as JWK
jwk, err := config.GetJWK()
if err != nil {
	log.Fatalf("failed to get JWK: %v", err)
}
fmt.Printf("JWK Algorithm: %s\n", jwk.Algorithm)
Example (HsmSigning)

ExampleSignerConfig_hsmSigning demonstrates using SignerConfig with HSM

// Create a signer config with HSM
config := pki.NewSignerConfig(&pki.KeyConfig{
	PKCS11: &pki.PKCS11Config{
		ModulePath: "/usr/lib/softhsm/libsofthsm2.so",
		PIN:        "1234",
		KeyLabel:   "signing-key",
	},
})

// Use the same interface for HSM-based signing
claims := jwt.MapClaims{
	"sub": "user456",
	"iss": "secure-service",
}
token, err := config.SignJWT(claims)
if err != nil {
	log.Fatalf("failed to sign JWT with HSM: %v", err)
}
fmt.Printf("HSM-signed JWT: %s\n", token)

// Get certificate from HSM
cert, err := config.GetCertificate()
if err != nil {
	log.Fatalf("failed to get certificate: %v", err)
}
fmt.Printf("Certificate subject: %s\n", cert.Subject)
Example (ServiceUsage)

ExampleSignerConfig_serviceUsage demonstrates typical service usage

// In a real service, this config would come from application config
signerConfig := pki.NewSignerConfig(&pki.KeyConfig{
	PrivateKeyPath: "/etc/myservice/signing-key.pem",
	ChainPath:      "/etc/myservice/signing-chain.pem",
})

// Service method that needs to sign something
generateAccessToken := func(userID string) (string, error) {
	claims := jwt.MapClaims{
		"sub": userID,
		"iss": "my-service",
		"iat": 1234567890,
		"exp": 1234571490,
	}
	return signerConfig.SignJWT(claims)
}

// Use in service
token, err := generateAccessToken("user789")
if err != nil {
	log.Fatalf("failed to generate token: %v", err)
}
fmt.Printf("Access token: %s\n", token)

// Get public key for verification endpoint
jwk, err := signerConfig.GetJWK()
if err != nil {
	log.Fatalf("failed to get public key: %v", err)
}
fmt.Printf("Public key algorithm: %s\n", jwk.Algorithm)

func NewSignerConfig

func NewSignerConfig(config *KeyConfig) *SignerConfig

NewSignerConfig creates a new SignerConfig with the specified configuration. Keys are loaded lazily on first use.

Example
package main

import (
	"fmt"

	"github.com/SUNET/vc/pkg/pki"
)

func main() {
	config := pki.NewSignerConfig(&pki.KeyConfig{
		PrivateKeyPath: "/path/to/key.pem",
		ChainPath:      "/path/to/chain.pem",
	})
	fmt.Printf("%T\n", config)
}
Output:
*pki.SignerConfig

func (*SignerConfig) GetCertificate

func (sc *SignerConfig) GetCertificate() (*x509.Certificate, error)

GetCertificate returns the X.509 certificate associated with the key.

func (*SignerConfig) GetCertificateChain

func (sc *SignerConfig) GetCertificateChain() ([]string, error)

GetCertificateChain returns the full certificate chain as base64-encoded DER.

func (*SignerConfig) GetJWK

func (sc *SignerConfig) GetJWK() (*jose.JSONWebKey, error)

GetJWK returns the public key as a JSON Web Key (JWK). The kid is derived using the same logic as KeyMaterialSigner.KeyID() to ensure consistency between JWT headers and JWKS endpoints.

func (*SignerConfig) GetKeyMaterial

func (sc *SignerConfig) GetKeyMaterial() (*KeyMaterial, error)

GetKeyMaterial returns the underlying KeyMaterial. This method should be used sparingly - prefer using the specific methods instead.

func (*SignerConfig) GetPrivateKey

func (sc *SignerConfig) GetPrivateKey() (crypto.PrivateKey, error)

GetPrivateKey returns the underlying private key. This method should be used sparingly - prefer using Sign() or SignJWT() instead.

func (*SignerConfig) RawSigner

func (sc *SignerConfig) RawSigner() (RawSigner, error)

RawSigner returns a RawSigner interface for advanced signing operations. This is useful for protocols like W3C Data Integrity that need to sign pre-computed digests.

func (*SignerConfig) Sign

func (sc *SignerConfig) Sign(data []byte) ([]byte, error)

Sign signs the provided data using the configured key. The signature algorithm is automatically determined from the key type.

func (*SignerConfig) SignJWT

func (sc *SignerConfig) SignJWT(claims jwt.Claims) (string, error)

SignJWT signs a JWT with the specified claims using the configured key.

type SoftwareSigner

type SoftwareSigner struct {
	// contains filtered or unexported fields
}

SoftwareSigner implements Signer using software-based keys.

func NewSoftwareSigner

func NewSoftwareSigner(privateKey crypto.PrivateKey, keyID string) (*SoftwareSigner, error)

NewSoftwareSigner creates a new SoftwareSigner from a private key.

func (*SoftwareSigner) Algorithm

func (s *SoftwareSigner) Algorithm() string

Algorithm returns the JWT algorithm name.

func (*SoftwareSigner) KeyID

func (s *SoftwareSigner) KeyID() string

KeyID returns the key identifier.

func (*SoftwareSigner) PublicKey

func (s *SoftwareSigner) PublicKey() any

PublicKey returns the public key.

func (*SoftwareSigner) Sign

func (s *SoftwareSigner) Sign(ctx context.Context, data []byte) ([]byte, error)

Sign signs data using the software key.

func (*SoftwareSigner) SignDigest

func (s *SoftwareSigner) SignDigest(ctx context.Context, digest []byte) ([]byte, error)

SignDigest signs a pre-computed digest without additional hashing. This is useful for protocols like W3C Data Integrity that control the hashing process.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL