From 91827af1b1efb3041ad5fea17d36db8ec953ce71 Mon Sep 17 00:00:00 2001 From: Magnus Svensson Date: Wed, 17 Dec 2025 15:14:08 +0100 Subject: [PATCH 1/6] Re add removed jose package, add some testing and support for different keys. --- pkg/jose/jwk.go | 124 +++++++++++++++++++++ pkg/jose/jwk_test.go | 255 +++++++++++++++++++++++++++++++++++++++++++ pkg/jose/jwt.go | 18 +++ pkg/jose/jwt_test.go | 90 +++++++++++++++ 4 files changed, 487 insertions(+) create mode 100644 pkg/jose/jwk.go create mode 100644 pkg/jose/jwk_test.go create mode 100644 pkg/jose/jwt.go create mode 100644 pkg/jose/jwt_test.go diff --git a/pkg/jose/jwk.go b/pkg/jose/jwk.go new file mode 100644 index 00000000..a62d605a --- /dev/null +++ b/pkg/jose/jwk.go @@ -0,0 +1,124 @@ +package jose + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "os" + "path/filepath" + + "github.com/golang-jwt/jwt/v5" + "github.com/lestrrat-go/jwx/v3/jwk" +) + +// JWK is a JSON Web Key +type JWK struct { + KTY string `json:"kty"` + CRV string `json:"crv"` + X string `json:"x"` + Y string `json:"y"` +} + +// ParseSigningKey parses a private key from a PEM file (supports EC and RSA in various formats) +func ParseSigningKey(signingKeyPath string) (crypto.PrivateKey, error) { + keyByte, err := os.ReadFile(filepath.Clean(signingKeyPath)) + if err != nil { + return nil, err + } + if keyByte == nil { + return nil, errors.New("private key missing") + } + + // Try EC first (SEC1 and PKCS8 formats) + if privateKey, err := jwt.ParseECPrivateKeyFromPEM(keyByte); err == nil { + return privateKey, nil + } + + // Try RSA (PKCS1 and PKCS8 formats) + if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyByte); err == nil { + return privateKey, nil + } + + // Try PKCS8 generic (handles both EC and RSA in PKCS8 format) + block, _ := pem.Decode(keyByte) + if block != nil && block.Type == "PRIVATE KEY" { + if privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { + return privateKey, nil + } + } + + // Try PKCS1 RSA explicitly + if block != nil && block.Type == "RSA PRIVATE KEY" { + if privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { + return privateKey, nil + } + } + + return nil, errors.New("unsupported key type: expected EC or RSA private key in PEM format (SEC1, PKCS1, or PKCS8)") +} + +// ParseECSigningKey parses an EC private key from a PEM file +func ParseECSigningKey(signingKeyPath string) (*ecdsa.PrivateKey, error) { + keyByte, err := os.ReadFile(filepath.Clean(signingKeyPath)) + if err != nil { + return nil, err + } + if keyByte == nil { + return nil, errors.New("private key missing") + } + + privateKey, err := jwt.ParseECPrivateKeyFromPEM(keyByte) + if err != nil { + return nil, err + } + + return privateKey, nil +} + +// ParseRSASigningKey parses an RSA private key from a PEM file +func ParseRSASigningKey(signingKeyPath string) (*rsa.PrivateKey, error) { + keyByte, err := os.ReadFile(filepath.Clean(signingKeyPath)) + if err != nil { + return nil, err + } + if keyByte == nil { + return nil, errors.New("private key missing") + } + + privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyByte) + if err != nil { + return nil, err + } + + return privateKey, nil +} + +// CreateJWK creates a JWK from the signing key +func CreateJWK(signingKeyPath string) (*JWK, *ecdsa.PrivateKey, error) { + privateKey, err := ParseECSigningKey(signingKeyPath) + if err != nil { + return nil, nil, err + } + + key, err := jwk.Import(privateKey) + if err != nil { + return nil, nil, err + } + + // Marshal to JSON and unmarshal to our JWK struct + jwkJSON, err := json.Marshal(key) + if err != nil { + return nil, nil, err + } + + result := &JWK{} + if err := json.Unmarshal(jwkJSON, result); err != nil { + return nil, nil, err + } + + return result, privateKey, nil +} diff --git a/pkg/jose/jwk_test.go b/pkg/jose/jwk_test.go new file mode 100644 index 00000000..feda2d15 --- /dev/null +++ b/pkg/jose/jwk_test.go @@ -0,0 +1,255 @@ +package jose + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func createTestECKey(t *testing.T) string { + t.Helper() + + // Generate ECDSA P-256 key + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + // Encode to PEM (SEC 1 / traditional format) + keyBytes, err := x509.MarshalECPrivateKey(privateKey) + require.NoError(t, err) + + pemBlock := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: keyBytes, + } + + // Write to temp file + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "test_ec_key.pem") + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) + + return keyPath +} + +func createTestECKeyPKCS8(t *testing.T) string { + t.Helper() + + // Generate ECDSA P-256 key + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + // Encode to PKCS8 format + keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + require.NoError(t, err) + + pemBlock := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + } + + // Write to temp file + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "test_ec_key_pkcs8.pem") + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) + + return keyPath +} + +func createTestRSAKey(t *testing.T) string { + t.Helper() + + // Generate RSA 2048 key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + // Encode to PEM (PKCS1 format) + keyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + + pemBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: keyBytes, + } + + // Write to temp file + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "test_rsa_key.pem") + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) + + return keyPath +} + +func createTestRSAKeyPKCS8(t *testing.T) string { + t.Helper() + + // Generate RSA 2048 key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + // Encode to PKCS8 format + keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + require.NoError(t, err) + + pemBlock := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + } + + // Write to temp file + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "test_rsa_key_pkcs8.pem") + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) + + return keyPath +} + +func createInvalidKeyFile(t *testing.T) string { + t.Helper() + + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "invalid_key.pem") + require.NoError(t, os.WriteFile(keyPath, []byte("not a valid key"), 0600)) + + return keyPath +} + +func TestParseSigningKey(t *testing.T) { + t.Run("parses EC key", func(t *testing.T) { + keyPath := createTestECKey(t) + key, err := ParseSigningKey(keyPath) + require.NoError(t, err) + assert.NotNil(t, key) + _, ok := key.(*ecdsa.PrivateKey) + assert.True(t, ok, "expected *ecdsa.PrivateKey") + }) + + t.Run("parses EC key PKCS8", func(t *testing.T) { + keyPath := createTestECKeyPKCS8(t) + key, err := ParseSigningKey(keyPath) + require.NoError(t, err) + assert.NotNil(t, key) + _, ok := key.(*ecdsa.PrivateKey) + assert.True(t, ok, "expected *ecdsa.PrivateKey") + }) + + t.Run("parses RSA key", func(t *testing.T) { + keyPath := createTestRSAKey(t) + key, err := ParseSigningKey(keyPath) + require.NoError(t, err) + assert.NotNil(t, key) + _, ok := key.(*rsa.PrivateKey) + assert.True(t, ok, "expected *rsa.PrivateKey") + }) + + t.Run("parses RSA key PKCS8", func(t *testing.T) { + keyPath := createTestRSAKeyPKCS8(t) + key, err := ParseSigningKey(keyPath) + require.NoError(t, err) + assert.NotNil(t, key) + _, ok := key.(*rsa.PrivateKey) + assert.True(t, ok, "expected *rsa.PrivateKey") + }) + + t.Run("returns error for non-existent file", func(t *testing.T) { + _, err := ParseSigningKey("/non/existent/path.pem") + assert.Error(t, err) + }) + + t.Run("returns error for invalid key", func(t *testing.T) { + keyPath := createInvalidKeyFile(t) + _, err := ParseSigningKey(keyPath) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported key type") + }) +} + +func TestParseECSigningKey(t *testing.T) { + t.Run("parses EC key successfully", func(t *testing.T) { + keyPath := createTestECKey(t) + key, err := ParseECSigningKey(keyPath) + require.NoError(t, err) + assert.NotNil(t, key) + }) + + t.Run("returns error for non-existent file", func(t *testing.T) { + _, err := ParseECSigningKey("/non/existent/path.pem") + assert.Error(t, err) + }) + + t.Run("returns error for RSA key", func(t *testing.T) { + keyPath := createTestRSAKey(t) + _, err := ParseECSigningKey(keyPath) + assert.Error(t, err) + }) + + t.Run("returns error for invalid key", func(t *testing.T) { + keyPath := createInvalidKeyFile(t) + _, err := ParseECSigningKey(keyPath) + assert.Error(t, err) + }) +} + +func TestParseRSASigningKey(t *testing.T) { + t.Run("parses RSA key successfully", func(t *testing.T) { + keyPath := createTestRSAKey(t) + key, err := ParseRSASigningKey(keyPath) + require.NoError(t, err) + assert.NotNil(t, key) + }) + + t.Run("returns error for non-existent file", func(t *testing.T) { + _, err := ParseRSASigningKey("/non/existent/path.pem") + assert.Error(t, err) + }) + + t.Run("returns error for EC key", func(t *testing.T) { + keyPath := createTestECKey(t) + _, err := ParseRSASigningKey(keyPath) + assert.Error(t, err) + }) + + t.Run("returns error for invalid key", func(t *testing.T) { + keyPath := createInvalidKeyFile(t) + _, err := ParseRSASigningKey(keyPath) + assert.Error(t, err) + }) +} + +func TestCreateJWK(t *testing.T) { + t.Run("creates JWK from EC key", func(t *testing.T) { + keyPath := createTestECKey(t) + + jwk, privateKey, err := CreateJWK(keyPath) + require.NoError(t, err) + + assert.Equal(t, "EC", jwk.KTY) + assert.Equal(t, "P-256", jwk.CRV) + assert.NotEmpty(t, jwk.X) + assert.NotEmpty(t, jwk.Y) + assert.NotNil(t, privateKey) + }) + + t.Run("returns error for non-existent file", func(t *testing.T) { + _, _, err := CreateJWK("/non/existent/path.pem") + assert.Error(t, err) + }) + + t.Run("returns error for RSA key", func(t *testing.T) { + keyPath := createTestRSAKey(t) + _, _, err := CreateJWK(keyPath) + assert.Error(t, err) + }) + + t.Run("returns error for invalid key", func(t *testing.T) { + keyPath := createInvalidKeyFile(t) + _, _, err := CreateJWK(keyPath) + assert.Error(t, err) + }) +} diff --git a/pkg/jose/jwt.go b/pkg/jose/jwt.go new file mode 100644 index 00000000..3c4263df --- /dev/null +++ b/pkg/jose/jwt.go @@ -0,0 +1,18 @@ +package jose + +import ( + "github.com/golang-jwt/jwt/v5" +) + +// MakeJWT creates a signed JWT with the given header, body, signing method, and key +func MakeJWT(header, body jwt.MapClaims, signingMethod jwt.SigningMethod, signingKey any) (string, error) { + token := jwt.NewWithClaims(signingMethod, body) + token.Header = header + + signedToken, err := token.SignedString(signingKey) + if err != nil { + return "", err + } + + return signedToken, nil +} diff --git a/pkg/jose/jwt_test.go b/pkg/jose/jwt_test.go new file mode 100644 index 00000000..953be2dc --- /dev/null +++ b/pkg/jose/jwt_test.go @@ -0,0 +1,90 @@ +package jose + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func createTestKeyForJWT(t *testing.T) string { + t.Helper() + + // Generate ECDSA P-256 key + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + // Encode to PEM + keyBytes, err := x509.MarshalECPrivateKey(privateKey) + require.NoError(t, err) + + pemBlock := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: keyBytes, + } + + // Write to temp file + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "test_key.pem") + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) + + return keyPath +} + +func TestMakeJWT(t *testing.T) { + t.Run("creates signed JWT successfully", func(t *testing.T) { + keyPath := createTestKeyForJWT(t) + + jwk, privateKey, err := CreateJWK(keyPath) + require.NoError(t, err) + + header := jwt.MapClaims{ + "alg": "ES256", + "typ": "openid4vci-proof+jwt", + "kid": "key-1", + } + body := jwt.MapClaims{ + "iss": "joe", + "aud": "https://example.com", + "iat": 1300819380, + "nonce": "n-0S6_WzA2Mj", + "jwk": jwk, + } + + signedToken, err := MakeJWT(header, body, jwt.SigningMethodES256, privateKey) + require.NoError(t, err) + assert.NotEmpty(t, signedToken) + + // Verify the token can be parsed + token, err := jwt.Parse(signedToken, func(token *jwt.Token) (interface{}, error) { + return &privateKey.PublicKey, nil + }) + require.NoError(t, err) + assert.True(t, token.Valid) + }) + + t.Run("returns error for nil key", func(t *testing.T) { + header := jwt.MapClaims{"alg": "ES256"} + body := jwt.MapClaims{"iss": "test"} + + _, err := MakeJWT(header, body, jwt.SigningMethodES256, nil) + assert.Error(t, err) + }) + + t.Run("returns error for wrong key type", func(t *testing.T) { + header := jwt.MapClaims{"alg": "ES256"} + body := jwt.MapClaims{"iss": "test"} + + // Use a string instead of a key + _, err := MakeJWT(header, body, jwt.SigningMethodES256, "not-a-key") + assert.Error(t, err) + }) +} From 21fe3789ed5901967973a692403a0fd11fc3d89d Mon Sep 17 00:00:00 2001 From: Magnus Svensson Date: Wed, 17 Dec 2025 15:25:38 +0100 Subject: [PATCH 2/6] Remove duplicated code. --- pkg/jose/jwk.go | 24 ++++-------------------- pkg/jose/jwk_test.go | 24 ++++++++++++++++++++---- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pkg/jose/jwk.go b/pkg/jose/jwk.go index a62d605a..76705a78 100644 --- a/pkg/jose/jwk.go +++ b/pkg/jose/jwk.go @@ -4,9 +4,7 @@ import ( "crypto" "crypto/ecdsa" "crypto/rsa" - "crypto/x509" "encoding/json" - "encoding/pem" "errors" "os" "path/filepath" @@ -24,6 +22,7 @@ type JWK struct { } // ParseSigningKey parses a private key from a PEM file (supports EC and RSA in various formats) +// Handles SEC1, PKCS1, and PKCS8 formats automatically. func ParseSigningKey(signingKeyPath string) (crypto.PrivateKey, error) { keyByte, err := os.ReadFile(filepath.Clean(signingKeyPath)) if err != nil { @@ -33,32 +32,17 @@ func ParseSigningKey(signingKeyPath string) (crypto.PrivateKey, error) { return nil, errors.New("private key missing") } - // Try EC first (SEC1 and PKCS8 formats) + // Try EC (handles SEC1 and PKCS8 formats) if privateKey, err := jwt.ParseECPrivateKeyFromPEM(keyByte); err == nil { return privateKey, nil } - // Try RSA (PKCS1 and PKCS8 formats) + // Try RSA (handles PKCS1 and PKCS8 formats) if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyByte); err == nil { return privateKey, nil } - // Try PKCS8 generic (handles both EC and RSA in PKCS8 format) - block, _ := pem.Decode(keyByte) - if block != nil && block.Type == "PRIVATE KEY" { - if privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { - return privateKey, nil - } - } - - // Try PKCS1 RSA explicitly - if block != nil && block.Type == "RSA PRIVATE KEY" { - if privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { - return privateKey, nil - } - } - - return nil, errors.New("unsupported key type: expected EC or RSA private key in PEM format (SEC1, PKCS1, or PKCS8)") + return nil, errors.New("unsupported key type: expected EC or RSA private key in PEM format") } // ParseECSigningKey parses an EC private key from a PEM file diff --git a/pkg/jose/jwk_test.go b/pkg/jose/jwk_test.go index feda2d15..8a0c8bdc 100644 --- a/pkg/jose/jwk_test.go +++ b/pkg/jose/jwk_test.go @@ -121,7 +121,7 @@ func createInvalidKeyFile(t *testing.T) string { } func TestParseSigningKey(t *testing.T) { - t.Run("parses EC key", func(t *testing.T) { + t.Run("parses EC key SEC1 format", func(t *testing.T) { keyPath := createTestECKey(t) key, err := ParseSigningKey(keyPath) require.NoError(t, err) @@ -130,7 +130,7 @@ func TestParseSigningKey(t *testing.T) { assert.True(t, ok, "expected *ecdsa.PrivateKey") }) - t.Run("parses EC key PKCS8", func(t *testing.T) { + t.Run("parses EC key PKCS8 format", func(t *testing.T) { keyPath := createTestECKeyPKCS8(t) key, err := ParseSigningKey(keyPath) require.NoError(t, err) @@ -139,8 +139,16 @@ func TestParseSigningKey(t *testing.T) { assert.True(t, ok, "expected *ecdsa.PrivateKey") }) - t.Run("parses RSA key", func(t *testing.T) { + t.Run("parses RSA key PKCS1 format (RSA PRIVATE KEY)", func(t *testing.T) { keyPath := createTestRSAKey(t) + + // Verify the key file has the expected PEM block type + keyBytes, err := os.ReadFile(keyPath) + require.NoError(t, err) + block, _ := pem.Decode(keyBytes) + require.NotNil(t, block) + assert.Equal(t, "RSA PRIVATE KEY", block.Type, "expected PKCS1 format with RSA PRIVATE KEY block type") + key, err := ParseSigningKey(keyPath) require.NoError(t, err) assert.NotNil(t, key) @@ -148,8 +156,16 @@ func TestParseSigningKey(t *testing.T) { assert.True(t, ok, "expected *rsa.PrivateKey") }) - t.Run("parses RSA key PKCS8", func(t *testing.T) { + t.Run("parses RSA key PKCS8 format (PRIVATE KEY)", func(t *testing.T) { keyPath := createTestRSAKeyPKCS8(t) + + // Verify the key file has the expected PEM block type + keyBytes, err := os.ReadFile(keyPath) + require.NoError(t, err) + block, _ := pem.Decode(keyBytes) + require.NotNil(t, block) + assert.Equal(t, "PRIVATE KEY", block.Type, "expected PKCS8 format with PRIVATE KEY block type") + key, err := ParseSigningKey(keyPath) require.NoError(t, err) assert.NotNil(t, key) From 9358535a015c856ab0b1177a328a32834ecf13c9 Mon Sep 17 00:00:00 2001 From: Magnus Svensson Date: Wed, 17 Dec 2025 15:28:27 +0100 Subject: [PATCH 3/6] Add support for other keys in jwk. --- pkg/jose/jwk.go | 68 ++++++++++++++++++++++++++++++++++++++++---- pkg/jose/jwk_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++-- pkg/jose/jwt_test.go | 2 +- 3 files changed, 129 insertions(+), 9 deletions(-) diff --git a/pkg/jose/jwk.go b/pkg/jose/jwk.go index 76705a78..0ef8da72 100644 --- a/pkg/jose/jwk.go +++ b/pkg/jose/jwk.go @@ -13,12 +13,16 @@ import ( "github.com/lestrrat-go/jwx/v3/jwk" ) -// JWK is a JSON Web Key +// JWK is a JSON Web Key supporting EC and RSA key types type JWK struct { KTY string `json:"kty"` - CRV string `json:"crv"` - X string `json:"x"` - Y string `json:"y"` + // EC key fields + CRV string `json:"crv,omitempty"` + X string `json:"x,omitempty"` + Y string `json:"y,omitempty"` + // RSA key fields + N string `json:"n,omitempty"` + E string `json:"e,omitempty"` } // ParseSigningKey parses a private key from a PEM file (supports EC and RSA in various formats) @@ -81,8 +85,34 @@ func ParseRSASigningKey(signingKeyPath string) (*rsa.PrivateKey, error) { return privateKey, nil } -// CreateJWK creates a JWK from the signing key -func CreateJWK(signingKeyPath string) (*JWK, *ecdsa.PrivateKey, error) { +// CreateJWK creates a JWK from a signing key file (supports EC and RSA) +func CreateJWK(signingKeyPath string) (*JWK, crypto.PrivateKey, error) { + privateKey, err := ParseSigningKey(signingKeyPath) + if err != nil { + return nil, nil, err + } + + key, err := jwk.Import(privateKey) + if err != nil { + return nil, nil, err + } + + // Marshal to JSON and unmarshal to our JWK struct + jwkJSON, err := json.Marshal(key) + if err != nil { + return nil, nil, err + } + + result := &JWK{} + if err := json.Unmarshal(jwkJSON, result); err != nil { + return nil, nil, err + } + + return result, privateKey, nil +} + +// CreateECJWK creates a JWK from an EC signing key file +func CreateECJWK(signingKeyPath string) (*JWK, *ecdsa.PrivateKey, error) { privateKey, err := ParseECSigningKey(signingKeyPath) if err != nil { return nil, nil, err @@ -106,3 +136,29 @@ func CreateJWK(signingKeyPath string) (*JWK, *ecdsa.PrivateKey, error) { return result, privateKey, nil } + +// CreateRSAJWK creates a JWK from an RSA signing key file +func CreateRSAJWK(signingKeyPath string) (*JWK, *rsa.PrivateKey, error) { + privateKey, err := ParseRSASigningKey(signingKeyPath) + if err != nil { + return nil, nil, err + } + + key, err := jwk.Import(privateKey) + if err != nil { + return nil, nil, err + } + + // Marshal to JSON and unmarshal to our JWK struct + jwkJSON, err := json.Marshal(key) + if err != nil { + return nil, nil, err + } + + result := &JWK{} + if err := json.Unmarshal(jwkJSON, result); err != nil { + return nil, nil, err + } + + return result, privateKey, nil +} diff --git a/pkg/jose/jwk_test.go b/pkg/jose/jwk_test.go index 8a0c8bdc..a9ee3fbb 100644 --- a/pkg/jose/jwk_test.go +++ b/pkg/jose/jwk_test.go @@ -249,6 +249,23 @@ func TestCreateJWK(t *testing.T) { assert.Equal(t, "P-256", jwk.CRV) assert.NotEmpty(t, jwk.X) assert.NotEmpty(t, jwk.Y) + assert.Empty(t, jwk.N) + assert.Empty(t, jwk.E) + assert.NotNil(t, privateKey) + }) + + t.Run("creates JWK from RSA key", func(t *testing.T) { + keyPath := createTestRSAKey(t) + + jwk, privateKey, err := CreateJWK(keyPath) + require.NoError(t, err) + + assert.Equal(t, "RSA", jwk.KTY) + assert.Empty(t, jwk.CRV) + assert.Empty(t, jwk.X) + assert.Empty(t, jwk.Y) + assert.NotEmpty(t, jwk.N) + assert.NotEmpty(t, jwk.E) assert.NotNil(t, privateKey) }) @@ -257,15 +274,62 @@ func TestCreateJWK(t *testing.T) { assert.Error(t, err) }) + t.Run("returns error for invalid key", func(t *testing.T) { + keyPath := createInvalidKeyFile(t) + _, _, err := CreateJWK(keyPath) + assert.Error(t, err) + }) +} + +func TestCreateECJWK(t *testing.T) { + t.Run("creates JWK from EC key", func(t *testing.T) { + keyPath := createTestECKey(t) + + jwk, privateKey, err := CreateECJWK(keyPath) + require.NoError(t, err) + + assert.Equal(t, "EC", jwk.KTY) + assert.Equal(t, "P-256", jwk.CRV) + assert.NotEmpty(t, jwk.X) + assert.NotEmpty(t, jwk.Y) + assert.NotNil(t, privateKey) + }) + t.Run("returns error for RSA key", func(t *testing.T) { keyPath := createTestRSAKey(t) - _, _, err := CreateJWK(keyPath) + _, _, err := CreateECJWK(keyPath) assert.Error(t, err) }) t.Run("returns error for invalid key", func(t *testing.T) { keyPath := createInvalidKeyFile(t) - _, _, err := CreateJWK(keyPath) + _, _, err := CreateECJWK(keyPath) + assert.Error(t, err) + }) +} + +func TestCreateRSAJWK(t *testing.T) { + t.Run("creates JWK from RSA key", func(t *testing.T) { + keyPath := createTestRSAKey(t) + + jwk, privateKey, err := CreateRSAJWK(keyPath) + require.NoError(t, err) + + assert.Equal(t, "RSA", jwk.KTY) + assert.NotEmpty(t, jwk.N) + assert.NotEmpty(t, jwk.E) + assert.NotNil(t, privateKey) + }) + + t.Run("returns error for EC key", func(t *testing.T) { + keyPath := createTestECKey(t) + _, _, err := CreateRSAJWK(keyPath) + assert.Error(t, err) + }) + + t.Run("returns error for invalid key", func(t *testing.T) { + keyPath := createInvalidKeyFile(t) + _, _, err := CreateRSAJWK(keyPath) assert.Error(t, err) }) } diff --git a/pkg/jose/jwt_test.go b/pkg/jose/jwt_test.go index 953be2dc..20c15f91 100644 --- a/pkg/jose/jwt_test.go +++ b/pkg/jose/jwt_test.go @@ -43,7 +43,7 @@ func TestMakeJWT(t *testing.T) { t.Run("creates signed JWT successfully", func(t *testing.T) { keyPath := createTestKeyForJWT(t) - jwk, privateKey, err := CreateJWK(keyPath) + jwk, privateKey, err := CreateECJWK(keyPath) require.NoError(t, err) header := jwt.MapClaims{ From 2efde047f21d40e8b281259d3d744cf01583e7fd Mon Sep 17 00:00:00 2001 From: Magnus Svensson Date: Wed, 17 Dec 2025 15:37:28 +0100 Subject: [PATCH 4/6] Cleaned up a bit. --- pkg/jose/jwk.go | 90 ------------------------------------- pkg/jose/jwk_test.go | 105 ------------------------------------------- pkg/jose/jwt_test.go | 65 +++++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 199 deletions(-) diff --git a/pkg/jose/jwk.go b/pkg/jose/jwk.go index 0ef8da72..b9acf2c3 100644 --- a/pkg/jose/jwk.go +++ b/pkg/jose/jwk.go @@ -2,8 +2,6 @@ package jose import ( "crypto" - "crypto/ecdsa" - "crypto/rsa" "encoding/json" "errors" "os" @@ -49,42 +47,6 @@ func ParseSigningKey(signingKeyPath string) (crypto.PrivateKey, error) { return nil, errors.New("unsupported key type: expected EC or RSA private key in PEM format") } -// ParseECSigningKey parses an EC private key from a PEM file -func ParseECSigningKey(signingKeyPath string) (*ecdsa.PrivateKey, error) { - keyByte, err := os.ReadFile(filepath.Clean(signingKeyPath)) - if err != nil { - return nil, err - } - if keyByte == nil { - return nil, errors.New("private key missing") - } - - privateKey, err := jwt.ParseECPrivateKeyFromPEM(keyByte) - if err != nil { - return nil, err - } - - return privateKey, nil -} - -// ParseRSASigningKey parses an RSA private key from a PEM file -func ParseRSASigningKey(signingKeyPath string) (*rsa.PrivateKey, error) { - keyByte, err := os.ReadFile(filepath.Clean(signingKeyPath)) - if err != nil { - return nil, err - } - if keyByte == nil { - return nil, errors.New("private key missing") - } - - privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyByte) - if err != nil { - return nil, err - } - - return privateKey, nil -} - // CreateJWK creates a JWK from a signing key file (supports EC and RSA) func CreateJWK(signingKeyPath string) (*JWK, crypto.PrivateKey, error) { privateKey, err := ParseSigningKey(signingKeyPath) @@ -110,55 +72,3 @@ func CreateJWK(signingKeyPath string) (*JWK, crypto.PrivateKey, error) { return result, privateKey, nil } - -// CreateECJWK creates a JWK from an EC signing key file -func CreateECJWK(signingKeyPath string) (*JWK, *ecdsa.PrivateKey, error) { - privateKey, err := ParseECSigningKey(signingKeyPath) - if err != nil { - return nil, nil, err - } - - key, err := jwk.Import(privateKey) - if err != nil { - return nil, nil, err - } - - // Marshal to JSON and unmarshal to our JWK struct - jwkJSON, err := json.Marshal(key) - if err != nil { - return nil, nil, err - } - - result := &JWK{} - if err := json.Unmarshal(jwkJSON, result); err != nil { - return nil, nil, err - } - - return result, privateKey, nil -} - -// CreateRSAJWK creates a JWK from an RSA signing key file -func CreateRSAJWK(signingKeyPath string) (*JWK, *rsa.PrivateKey, error) { - privateKey, err := ParseRSASigningKey(signingKeyPath) - if err != nil { - return nil, nil, err - } - - key, err := jwk.Import(privateKey) - if err != nil { - return nil, nil, err - } - - // Marshal to JSON and unmarshal to our JWK struct - jwkJSON, err := json.Marshal(key) - if err != nil { - return nil, nil, err - } - - result := &JWK{} - if err := json.Unmarshal(jwkJSON, result); err != nil { - return nil, nil, err - } - - return result, privateKey, nil -} diff --git a/pkg/jose/jwk_test.go b/pkg/jose/jwk_test.go index a9ee3fbb..3f66077b 100644 --- a/pkg/jose/jwk_test.go +++ b/pkg/jose/jwk_test.go @@ -186,58 +186,6 @@ func TestParseSigningKey(t *testing.T) { }) } -func TestParseECSigningKey(t *testing.T) { - t.Run("parses EC key successfully", func(t *testing.T) { - keyPath := createTestECKey(t) - key, err := ParseECSigningKey(keyPath) - require.NoError(t, err) - assert.NotNil(t, key) - }) - - t.Run("returns error for non-existent file", func(t *testing.T) { - _, err := ParseECSigningKey("/non/existent/path.pem") - assert.Error(t, err) - }) - - t.Run("returns error for RSA key", func(t *testing.T) { - keyPath := createTestRSAKey(t) - _, err := ParseECSigningKey(keyPath) - assert.Error(t, err) - }) - - t.Run("returns error for invalid key", func(t *testing.T) { - keyPath := createInvalidKeyFile(t) - _, err := ParseECSigningKey(keyPath) - assert.Error(t, err) - }) -} - -func TestParseRSASigningKey(t *testing.T) { - t.Run("parses RSA key successfully", func(t *testing.T) { - keyPath := createTestRSAKey(t) - key, err := ParseRSASigningKey(keyPath) - require.NoError(t, err) - assert.NotNil(t, key) - }) - - t.Run("returns error for non-existent file", func(t *testing.T) { - _, err := ParseRSASigningKey("/non/existent/path.pem") - assert.Error(t, err) - }) - - t.Run("returns error for EC key", func(t *testing.T) { - keyPath := createTestECKey(t) - _, err := ParseRSASigningKey(keyPath) - assert.Error(t, err) - }) - - t.Run("returns error for invalid key", func(t *testing.T) { - keyPath := createInvalidKeyFile(t) - _, err := ParseRSASigningKey(keyPath) - assert.Error(t, err) - }) -} - func TestCreateJWK(t *testing.T) { t.Run("creates JWK from EC key", func(t *testing.T) { keyPath := createTestECKey(t) @@ -280,56 +228,3 @@ func TestCreateJWK(t *testing.T) { assert.Error(t, err) }) } - -func TestCreateECJWK(t *testing.T) { - t.Run("creates JWK from EC key", func(t *testing.T) { - keyPath := createTestECKey(t) - - jwk, privateKey, err := CreateECJWK(keyPath) - require.NoError(t, err) - - assert.Equal(t, "EC", jwk.KTY) - assert.Equal(t, "P-256", jwk.CRV) - assert.NotEmpty(t, jwk.X) - assert.NotEmpty(t, jwk.Y) - assert.NotNil(t, privateKey) - }) - - t.Run("returns error for RSA key", func(t *testing.T) { - keyPath := createTestRSAKey(t) - _, _, err := CreateECJWK(keyPath) - assert.Error(t, err) - }) - - t.Run("returns error for invalid key", func(t *testing.T) { - keyPath := createInvalidKeyFile(t) - _, _, err := CreateECJWK(keyPath) - assert.Error(t, err) - }) -} - -func TestCreateRSAJWK(t *testing.T) { - t.Run("creates JWK from RSA key", func(t *testing.T) { - keyPath := createTestRSAKey(t) - - jwk, privateKey, err := CreateRSAJWK(keyPath) - require.NoError(t, err) - - assert.Equal(t, "RSA", jwk.KTY) - assert.NotEmpty(t, jwk.N) - assert.NotEmpty(t, jwk.E) - assert.NotNil(t, privateKey) - }) - - t.Run("returns error for EC key", func(t *testing.T) { - keyPath := createTestECKey(t) - _, _, err := CreateRSAJWK(keyPath) - assert.Error(t, err) - }) - - t.Run("returns error for invalid key", func(t *testing.T) { - keyPath := createInvalidKeyFile(t) - _, _, err := CreateRSAJWK(keyPath) - assert.Error(t, err) - }) -} diff --git a/pkg/jose/jwt_test.go b/pkg/jose/jwt_test.go index 20c15f91..07c91056 100644 --- a/pkg/jose/jwt_test.go +++ b/pkg/jose/jwt_test.go @@ -4,6 +4,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/x509" "encoding/pem" "os" @@ -39,13 +40,36 @@ func createTestKeyForJWT(t *testing.T) string { return keyPath } +func createTestRSAKeyForJWT(t *testing.T) string { + t.Helper() + + // Generate RSA 2048-bit key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + // Encode to PEM (PKCS1 format - "RSA PRIVATE KEY") + pemBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + } + + // Write to temp file + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "test_rsa_key.pem") + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) + + return keyPath +} + func TestMakeJWT(t *testing.T) { - t.Run("creates signed JWT successfully", func(t *testing.T) { + t.Run("creates signed JWT with EC key", func(t *testing.T) { keyPath := createTestKeyForJWT(t) - jwk, privateKey, err := CreateECJWK(keyPath) + jwk, privateKey, err := CreateJWK(keyPath) require.NoError(t, err) + ecKey := privateKey.(*ecdsa.PrivateKey) + header := jwt.MapClaims{ "alg": "ES256", "typ": "openid4vci-proof+jwt", @@ -59,13 +83,46 @@ func TestMakeJWT(t *testing.T) { "jwk": jwk, } - signedToken, err := MakeJWT(header, body, jwt.SigningMethodES256, privateKey) + signedToken, err := MakeJWT(header, body, jwt.SigningMethodES256, ecKey) + require.NoError(t, err) + assert.NotEmpty(t, signedToken) + + // Verify the token can be parsed + token, err := jwt.Parse(signedToken, func(token *jwt.Token) (interface{}, error) { + return &ecKey.PublicKey, nil + }) + require.NoError(t, err) + assert.True(t, token.Valid) + }) + + t.Run("creates signed JWT with RSA key", func(t *testing.T) { + keyPath := createTestRSAKeyForJWT(t) + + jwk, privateKey, err := CreateJWK(keyPath) + require.NoError(t, err) + + rsaKey := privateKey.(*rsa.PrivateKey) + + header := jwt.MapClaims{ + "alg": "RS256", + "typ": "JWT", + "kid": "rsa-key-1", + } + body := jwt.MapClaims{ + "iss": "joe", + "aud": "https://example.com", + "iat": 1300819380, + "nonce": "n-0S6_WzA2Mj", + "jwk": jwk, + } + + signedToken, err := MakeJWT(header, body, jwt.SigningMethodRS256, rsaKey) require.NoError(t, err) assert.NotEmpty(t, signedToken) // Verify the token can be parsed token, err := jwt.Parse(signedToken, func(token *jwt.Token) (interface{}, error) { - return &privateKey.PublicKey, nil + return &rsaKey.PublicKey, nil }) require.NoError(t, err) assert.True(t, token.Valid) From 35d67562c674b6769ba88aa3b375f99c8cf32aad Mon Sep 17 00:00:00 2001 From: Magnus Svensson Date: Wed, 17 Dec 2025 15:42:37 +0100 Subject: [PATCH 5/6] Merge headers and remove redudant nil check. --- pkg/jose/jwk.go | 3 --- pkg/jose/jwt.go | 9 +++++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/jose/jwk.go b/pkg/jose/jwk.go index b9acf2c3..4475abfa 100644 --- a/pkg/jose/jwk.go +++ b/pkg/jose/jwk.go @@ -30,9 +30,6 @@ func ParseSigningKey(signingKeyPath string) (crypto.PrivateKey, error) { if err != nil { return nil, err } - if keyByte == nil { - return nil, errors.New("private key missing") - } // Try EC (handles SEC1 and PKCS8 formats) if privateKey, err := jwt.ParseECPrivateKeyFromPEM(keyByte); err == nil { diff --git a/pkg/jose/jwt.go b/pkg/jose/jwt.go index 3c4263df..ef7b54a8 100644 --- a/pkg/jose/jwt.go +++ b/pkg/jose/jwt.go @@ -1,13 +1,18 @@ package jose import ( + "maps" + "github.com/golang-jwt/jwt/v5" ) -// MakeJWT creates a signed JWT with the given header, body, signing method, and key +// MakeJWT creates a signed JWT with the given header, body, signing method, and key. +// The header parameter is merged with default headers set by the signing method. func MakeJWT(header, body jwt.MapClaims, signingMethod jwt.SigningMethod, signingKey any) (string, error) { token := jwt.NewWithClaims(signingMethod, body) - token.Header = header + + // Merge provided header fields with defaults (provided values override defaults) + maps.Copy(token.Header, header) signedToken, err := token.SignedString(signingKey) if err != nil { From 2557bebec12625ac77c4a49950f9e5ce07ba06fc Mon Sep 17 00:00:00 2001 From: Magnus Svensson Date: Wed, 17 Dec 2025 18:32:54 +0100 Subject: [PATCH 6/6] Remove duplicated test functions. --- pkg/jose/jwk_test.go | 109 ------------------------------------ pkg/jose/jwt_test.go | 55 +------------------ pkg/jose/testutil_test.go | 113 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 162 deletions(-) create mode 100644 pkg/jose/testutil_test.go diff --git a/pkg/jose/jwk_test.go b/pkg/jose/jwk_test.go index 3f66077b..29d3e40a 100644 --- a/pkg/jose/jwk_test.go +++ b/pkg/jose/jwk_test.go @@ -2,124 +2,15 @@ package jose import ( "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" "crypto/rsa" - "crypto/x509" "encoding/pem" "os" - "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func createTestECKey(t *testing.T) string { - t.Helper() - - // Generate ECDSA P-256 key - privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - // Encode to PEM (SEC 1 / traditional format) - keyBytes, err := x509.MarshalECPrivateKey(privateKey) - require.NoError(t, err) - - pemBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: keyBytes, - } - - // Write to temp file - tmpDir := t.TempDir() - keyPath := filepath.Join(tmpDir, "test_ec_key.pem") - require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) - - return keyPath -} - -func createTestECKeyPKCS8(t *testing.T) string { - t.Helper() - - // Generate ECDSA P-256 key - privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - // Encode to PKCS8 format - keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) - require.NoError(t, err) - - pemBlock := &pem.Block{ - Type: "PRIVATE KEY", - Bytes: keyBytes, - } - - // Write to temp file - tmpDir := t.TempDir() - keyPath := filepath.Join(tmpDir, "test_ec_key_pkcs8.pem") - require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) - - return keyPath -} - -func createTestRSAKey(t *testing.T) string { - t.Helper() - - // Generate RSA 2048 key - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - // Encode to PEM (PKCS1 format) - keyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - pemBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: keyBytes, - } - - // Write to temp file - tmpDir := t.TempDir() - keyPath := filepath.Join(tmpDir, "test_rsa_key.pem") - require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) - - return keyPath -} - -func createTestRSAKeyPKCS8(t *testing.T) string { - t.Helper() - - // Generate RSA 2048 key - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - // Encode to PKCS8 format - keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) - require.NoError(t, err) - - pemBlock := &pem.Block{ - Type: "PRIVATE KEY", - Bytes: keyBytes, - } - - // Write to temp file - tmpDir := t.TempDir() - keyPath := filepath.Join(tmpDir, "test_rsa_key_pkcs8.pem") - require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) - - return keyPath -} - -func createInvalidKeyFile(t *testing.T) string { - t.Helper() - - tmpDir := t.TempDir() - keyPath := filepath.Join(tmpDir, "invalid_key.pem") - require.NoError(t, os.WriteFile(keyPath, []byte("not a valid key"), 0600)) - - return keyPath -} - func TestParseSigningKey(t *testing.T) { t.Run("parses EC key SEC1 format", func(t *testing.T) { keyPath := createTestECKey(t) diff --git a/pkg/jose/jwt_test.go b/pkg/jose/jwt_test.go index 07c91056..5ab1dcec 100644 --- a/pkg/jose/jwt_test.go +++ b/pkg/jose/jwt_test.go @@ -2,13 +2,7 @@ package jose import ( "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" "crypto/rsa" - "crypto/x509" - "encoding/pem" - "os" - "path/filepath" "testing" "github.com/golang-jwt/jwt/v5" @@ -16,54 +10,9 @@ import ( "github.com/stretchr/testify/require" ) -func createTestKeyForJWT(t *testing.T) string { - t.Helper() - - // Generate ECDSA P-256 key - privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - // Encode to PEM - keyBytes, err := x509.MarshalECPrivateKey(privateKey) - require.NoError(t, err) - - pemBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: keyBytes, - } - - // Write to temp file - tmpDir := t.TempDir() - keyPath := filepath.Join(tmpDir, "test_key.pem") - require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) - - return keyPath -} - -func createTestRSAKeyForJWT(t *testing.T) string { - t.Helper() - - // Generate RSA 2048-bit key - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - // Encode to PEM (PKCS1 format - "RSA PRIVATE KEY") - pemBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey), - } - - // Write to temp file - tmpDir := t.TempDir() - keyPath := filepath.Join(tmpDir, "test_rsa_key.pem") - require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) - - return keyPath -} - func TestMakeJWT(t *testing.T) { t.Run("creates signed JWT with EC key", func(t *testing.T) { - keyPath := createTestKeyForJWT(t) + keyPath := createTestECKey(t) jwk, privateKey, err := CreateJWK(keyPath) require.NoError(t, err) @@ -96,7 +45,7 @@ func TestMakeJWT(t *testing.T) { }) t.Run("creates signed JWT with RSA key", func(t *testing.T) { - keyPath := createTestRSAKeyForJWT(t) + keyPath := createTestRSAKey(t) jwk, privateKey, err := CreateJWK(keyPath) require.NoError(t, err) diff --git a/pkg/jose/testutil_test.go b/pkg/jose/testutil_test.go new file mode 100644 index 00000000..55e7b64c --- /dev/null +++ b/pkg/jose/testutil_test.go @@ -0,0 +1,113 @@ +package jose + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +// createTestECKey generates an EC P-256 key in SEC1 format and returns the file path +func createTestECKey(t *testing.T) string { + t.Helper() + + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + keyBytes, err := x509.MarshalECPrivateKey(privateKey) + require.NoError(t, err) + + pemBlock := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: keyBytes, + } + + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "test_ec_key.pem") + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) + + return keyPath +} + +// createTestECKeyPKCS8 generates an EC P-256 key in PKCS8 format and returns the file path +func createTestECKeyPKCS8(t *testing.T) string { + t.Helper() + + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + require.NoError(t, err) + + pemBlock := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + } + + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "test_ec_key_pkcs8.pem") + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) + + return keyPath +} + +// createTestRSAKey generates an RSA 2048-bit key in PKCS1 format and returns the file path +func createTestRSAKey(t *testing.T) string { + t.Helper() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + keyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + + pemBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: keyBytes, + } + + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "test_rsa_key.pem") + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) + + return keyPath +} + +// createTestRSAKeyPKCS8 generates an RSA 2048-bit key in PKCS8 format and returns the file path +func createTestRSAKeyPKCS8(t *testing.T) string { + t.Helper() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + require.NoError(t, err) + + pemBlock := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + } + + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "test_rsa_key_pkcs8.pem") + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(pemBlock), 0600)) + + return keyPath +} + +// createInvalidKeyFile creates an invalid key file and returns the file path +func createInvalidKeyFile(t *testing.T) string { + t.Helper() + + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "invalid_key.pem") + require.NoError(t, os.WriteFile(keyPath, []byte("not a valid key"), 0600)) + + return keyPath +}