Skip to content

Commit a7b3ee0

Browse files
authored
perf(cryptojwt): add optional key caching for RSA and ECDSA operations
perf(cryptojwt): add optional key caching for RSA and ECDSA operations
2 parents 0fcced7 + ac84011 commit a7b3ee0

5 files changed

Lines changed: 496 additions & 10 deletions

File tree

pkg/cryptojwt/cryptojwt_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"testing"
1313
)
1414

15-
func createTempFile(t *testing.T, content []byte) string {
15+
func createTempFile(t testing.TB, content []byte) string {
1616
t.Helper()
1717
tmpFile, err := os.CreateTemp(t.TempDir(), "test-*.pem")
1818
if err != nil {
@@ -27,7 +27,7 @@ func createTempFile(t *testing.T, content []byte) string {
2727
return tmpFile.Name()
2828
}
2929

30-
func generateRSAKeyPair(t *testing.T) (privateKeyPath, publicKeyPath string) {
30+
func generateRSAKeyPair(t testing.TB) (privateKeyPath, publicKeyPath string) {
3131
t.Helper()
3232
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
3333
if err != nil {
@@ -55,7 +55,7 @@ func generateRSAKeyPair(t *testing.T) (privateKeyPath, publicKeyPath string) {
5555
return privateKeyPath, publicKeyPath
5656
}
5757

58-
func generateECDSAKeyPair(t *testing.T, curve elliptic.Curve) (privateKeyPath, publicKeyPath string) {
58+
func generateECDSAKeyPair(t testing.TB, curve elliptic.Curve) (privateKeyPath, publicKeyPath string) {
5959
t.Helper()
6060
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
6161
if err != nil {
@@ -87,12 +87,12 @@ func generateECDSAKeyPair(t *testing.T, curve elliptic.Curve) (privateKeyPath, p
8787
return privateKeyPath, publicKeyPath
8888
}
8989

90-
func createInvalidPEMFile(t *testing.T) string {
90+
func createInvalidPEMFile(t testing.TB) string {
9191
t.Helper()
9292
return createTempFile(t, []byte("invalid pem content"))
9393
}
9494

95-
func createWrongTypePEMFile(t *testing.T, pemType string) string {
95+
func createWrongTypePEMFile(t testing.TB, pemType string) string {
9696
t.Helper()
9797
block := &pem.Block{
9898
Type: pemType,
@@ -101,7 +101,7 @@ func createWrongTypePEMFile(t *testing.T, pemType string) string {
101101
return createTempFile(t, pem.EncodeToMemory(block))
102102
}
103103

104-
func createMalformedECKeyFile(t *testing.T) string {
104+
func createMalformedECKeyFile(t testing.TB) string {
105105
t.Helper()
106106
block := &pem.Block{
107107
Type: "EC PRIVATE KEY",
@@ -110,7 +110,7 @@ func createMalformedECKeyFile(t *testing.T) string {
110110
return createTempFile(t, pem.EncodeToMemory(block))
111111
}
112112

113-
func createMalformedRSAKeyFile(t *testing.T) string {
113+
func createMalformedRSAKeyFile(t testing.TB) string {
114114
t.Helper()
115115
block := &pem.Block{
116116
Type: "RSA PRIVATE KEY",
@@ -119,7 +119,7 @@ func createMalformedRSAKeyFile(t *testing.T) string {
119119
return createTempFile(t, pem.EncodeToMemory(block))
120120
}
121121

122-
func getNonExistentPath(t *testing.T) string {
122+
func getNonExistentPath(t testing.TB) string {
123123
t.Helper()
124124
return filepath.Join(t.TempDir(), "non-existent-file.pem")
125125
}

pkg/cryptojwt/esjwt.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ type esjwtEncoderWithPrivateKeyFile struct {
1717
method jwt.SigningMethod
1818
}
1919

20+
type esjwtEncoderWithCachedPrivateKey struct {
21+
encoder encoder
22+
privateKey crypto.PrivateKey
23+
method jwt.SigningMethod
24+
}
25+
2026
type esjwtDecoderWithPrivateKeyFile struct {
2127
decoder decoder
2228
privateKeyFile string
@@ -29,6 +35,12 @@ type esjwtDecoderWithPublicKeyFile struct {
2935
method jwt.SigningMethod
3036
}
3137

38+
type esjwtDecoderWithCachedPublicKey struct {
39+
decoder decoder
40+
publicKey crypto.PublicKey
41+
method jwt.SigningMethod
42+
}
43+
3244
// NewES256Encoder creates a new ECDSA-SHA256 JWT encoder with a private key file.
3345
func NewES256Encoder(privateKeyFile string) Encoder {
3446
return &esjwtEncoderWithPrivateKeyFile{
@@ -109,6 +121,61 @@ func NewES256DecoderWithPublicKeyFileAndValidation(publicKeyFile string, validat
109121
}
110122
}
111123

124+
// NewES256EncoderWithCache creates a new ECDSA-SHA256 JWT encoder with cached private key.
125+
// The private key is loaded once at creation time, improving performance for repeated operations.
126+
func NewES256EncoderWithCache(privateKeyFile string) (Encoder, error) {
127+
privateKey, _, err := readECDSAPrivateKey(privateKeyFile)
128+
if err != nil {
129+
return nil, err
130+
}
131+
return &esjwtEncoderWithCachedPrivateKey{
132+
method: jwt.SigningMethodES256,
133+
privateKey: privateKey,
134+
}, nil
135+
}
136+
137+
// NewES256DecoderWithPrivateKeyFileAndCache creates a new ECDSA-SHA256 JWT decoder with cached public key from private key file.
138+
// The key is loaded once at creation time, improving performance for repeated operations.
139+
func NewES256DecoderWithPrivateKeyFileAndCache(privateKeyFile string) (Decoder, error) {
140+
return NewES256DecoderWithPrivateKeyFileAndCacheAndValidation(privateKeyFile, ValidationOptions{})
141+
}
142+
143+
// NewES256DecoderWithPrivateKeyFileAndCacheAndValidation creates a new ECDSA-SHA256 JWT decoder with cached public key and validation options.
144+
func NewES256DecoderWithPrivateKeyFileAndCacheAndValidation(privateKeyFile string, validationOpts ValidationOptions) (Decoder, error) {
145+
_, publicKey, err := readECDSAPrivateKey(privateKeyFile)
146+
if err != nil {
147+
return nil, err
148+
}
149+
return &esjwtDecoderWithCachedPublicKey{
150+
method: jwt.SigningMethodES256,
151+
publicKey: publicKey,
152+
decoder: decoder{validationOpts: validationOpts},
153+
}, nil
154+
}
155+
156+
// NewES256DecoderWithPublicKeyFileAndCache creates a new ECDSA-SHA256 JWT decoder with cached public key from public key file.
157+
// The key is loaded once at creation time, improving performance for repeated operations.
158+
func NewES256DecoderWithPublicKeyFileAndCache(publicKeyFile string) (Decoder, error) {
159+
return NewES256DecoderWithPublicKeyFileAndCacheAndValidation(publicKeyFile, ValidationOptions{})
160+
}
161+
162+
// NewES256DecoderWithPublicKeyFileAndCacheAndValidation creates a new ECDSA-SHA256 JWT decoder with cached public key and validation options.
163+
func NewES256DecoderWithPublicKeyFileAndCacheAndValidation(publicKeyFile string, validationOpts ValidationOptions) (Decoder, error) {
164+
publicKeyBytes, err := os.ReadFile(publicKeyFile) // #nosec G304 -- user-provided file path
165+
if err != nil {
166+
return nil, fmt.Errorf("error reading public key file: %w", err)
167+
}
168+
publicKey, err := jwt.ParseECPublicKeyFromPEM(publicKeyBytes)
169+
if err != nil {
170+
return nil, fmt.Errorf("error parsing EC public key: %w", err)
171+
}
172+
return &esjwtDecoderWithCachedPublicKey{
173+
method: jwt.SigningMethodES256,
174+
publicKey: publicKey,
175+
decoder: decoder{validationOpts: validationOpts},
176+
}, nil
177+
}
178+
112179
// NewES384DecoderWithPublicKeyFile creates a new ECDSA-SHA384 JWT decoder with a public key file.
113180
func NewES384DecoderWithPublicKeyFile(publicKeyFile string) Decoder {
114181
return NewES384DecoderWithPublicKeyFileAndValidation(publicKeyFile, ValidationOptions{})
@@ -123,6 +190,61 @@ func NewES384DecoderWithPublicKeyFileAndValidation(publicKeyFile string, validat
123190
}
124191
}
125192

193+
// NewES384EncoderWithCache creates a new ECDSA-SHA384 JWT encoder with cached private key.
194+
// The private key is loaded once at creation time, improving performance for repeated operations.
195+
func NewES384EncoderWithCache(privateKeyFile string) (Encoder, error) {
196+
privateKey, _, err := readECDSAPrivateKey(privateKeyFile)
197+
if err != nil {
198+
return nil, err
199+
}
200+
return &esjwtEncoderWithCachedPrivateKey{
201+
method: jwt.SigningMethodES384,
202+
privateKey: privateKey,
203+
}, nil
204+
}
205+
206+
// NewES384DecoderWithPrivateKeyFileAndCache creates a new ECDSA-SHA384 JWT decoder with cached public key from private key file.
207+
// The key is loaded once at creation time, improving performance for repeated operations.
208+
func NewES384DecoderWithPrivateKeyFileAndCache(privateKeyFile string) (Decoder, error) {
209+
return NewES384DecoderWithPrivateKeyFileAndCacheAndValidation(privateKeyFile, ValidationOptions{})
210+
}
211+
212+
// NewES384DecoderWithPrivateKeyFileAndCacheAndValidation creates a new ECDSA-SHA384 JWT decoder with cached public key and validation options.
213+
func NewES384DecoderWithPrivateKeyFileAndCacheAndValidation(privateKeyFile string, validationOpts ValidationOptions) (Decoder, error) {
214+
_, publicKey, err := readECDSAPrivateKey(privateKeyFile)
215+
if err != nil {
216+
return nil, err
217+
}
218+
return &esjwtDecoderWithCachedPublicKey{
219+
method: jwt.SigningMethodES384,
220+
publicKey: publicKey,
221+
decoder: decoder{validationOpts: validationOpts},
222+
}, nil
223+
}
224+
225+
// NewES384DecoderWithPublicKeyFileAndCache creates a new ECDSA-SHA384 JWT decoder with cached public key from public key file.
226+
// The key is loaded once at creation time, improving performance for repeated operations.
227+
func NewES384DecoderWithPublicKeyFileAndCache(publicKeyFile string) (Decoder, error) {
228+
return NewES384DecoderWithPublicKeyFileAndCacheAndValidation(publicKeyFile, ValidationOptions{})
229+
}
230+
231+
// NewES384DecoderWithPublicKeyFileAndCacheAndValidation creates a new ECDSA-SHA384 JWT decoder with cached public key and validation options.
232+
func NewES384DecoderWithPublicKeyFileAndCacheAndValidation(publicKeyFile string, validationOpts ValidationOptions) (Decoder, error) {
233+
publicKeyBytes, err := os.ReadFile(publicKeyFile) // #nosec G304 -- user-provided file path
234+
if err != nil {
235+
return nil, fmt.Errorf("error reading public key file: %w", err)
236+
}
237+
publicKey, err := jwt.ParseECPublicKeyFromPEM(publicKeyBytes)
238+
if err != nil {
239+
return nil, fmt.Errorf("error parsing EC public key: %w", err)
240+
}
241+
return &esjwtDecoderWithCachedPublicKey{
242+
method: jwt.SigningMethodES384,
243+
publicKey: publicKey,
244+
decoder: decoder{validationOpts: validationOpts},
245+
}, nil
246+
}
247+
126248
// NewES512DecoderWithPublicKeyFile creates a new ECDSA-SHA512 JWT decoder with a public key file.
127249
func NewES512DecoderWithPublicKeyFile(publicKeyFile string) Decoder {
128250
return NewES512DecoderWithPublicKeyFileAndValidation(publicKeyFile, ValidationOptions{})
@@ -137,6 +259,61 @@ func NewES512DecoderWithPublicKeyFileAndValidation(publicKeyFile string, validat
137259
}
138260
}
139261

262+
// NewES512EncoderWithCache creates a new ECDSA-SHA512 JWT encoder with cached private key.
263+
// The private key is loaded once at creation time, improving performance for repeated operations.
264+
func NewES512EncoderWithCache(privateKeyFile string) (Encoder, error) {
265+
privateKey, _, err := readECDSAPrivateKey(privateKeyFile)
266+
if err != nil {
267+
return nil, err
268+
}
269+
return &esjwtEncoderWithCachedPrivateKey{
270+
method: jwt.SigningMethodES512,
271+
privateKey: privateKey,
272+
}, nil
273+
}
274+
275+
// NewES512DecoderWithPrivateKeyFileAndCache creates a new ECDSA-SHA512 JWT decoder with cached public key from private key file.
276+
// The key is loaded once at creation time, improving performance for repeated operations.
277+
func NewES512DecoderWithPrivateKeyFileAndCache(privateKeyFile string) (Decoder, error) {
278+
return NewES512DecoderWithPrivateKeyFileAndCacheAndValidation(privateKeyFile, ValidationOptions{})
279+
}
280+
281+
// NewES512DecoderWithPrivateKeyFileAndCacheAndValidation creates a new ECDSA-SHA512 JWT decoder with cached public key and validation options.
282+
func NewES512DecoderWithPrivateKeyFileAndCacheAndValidation(privateKeyFile string, validationOpts ValidationOptions) (Decoder, error) {
283+
_, publicKey, err := readECDSAPrivateKey(privateKeyFile)
284+
if err != nil {
285+
return nil, err
286+
}
287+
return &esjwtDecoderWithCachedPublicKey{
288+
method: jwt.SigningMethodES512,
289+
publicKey: publicKey,
290+
decoder: decoder{validationOpts: validationOpts},
291+
}, nil
292+
}
293+
294+
// NewES512DecoderWithPublicKeyFileAndCache creates a new ECDSA-SHA512 JWT decoder with cached public key from public key file.
295+
// The key is loaded once at creation time, improving performance for repeated operations.
296+
func NewES512DecoderWithPublicKeyFileAndCache(publicKeyFile string) (Decoder, error) {
297+
return NewES512DecoderWithPublicKeyFileAndCacheAndValidation(publicKeyFile, ValidationOptions{})
298+
}
299+
300+
// NewES512DecoderWithPublicKeyFileAndCacheAndValidation creates a new ECDSA-SHA512 JWT decoder with cached public key and validation options.
301+
func NewES512DecoderWithPublicKeyFileAndCacheAndValidation(publicKeyFile string, validationOpts ValidationOptions) (Decoder, error) {
302+
publicKeyBytes, err := os.ReadFile(publicKeyFile) // #nosec G304 -- user-provided file path
303+
if err != nil {
304+
return nil, fmt.Errorf("error reading public key file: %w", err)
305+
}
306+
publicKey, err := jwt.ParseECPublicKeyFromPEM(publicKeyBytes)
307+
if err != nil {
308+
return nil, fmt.Errorf("error parsing EC public key: %w", err)
309+
}
310+
return &esjwtDecoderWithCachedPublicKey{
311+
method: jwt.SigningMethodES512,
312+
publicKey: publicKey,
313+
decoder: decoder{validationOpts: validationOpts},
314+
}, nil
315+
}
316+
140317
func readECDSAPrivateKey(privateKeyFile string) (crypto.PrivateKey, crypto.PublicKey, error) {
141318
contentKeyFile, err := os.ReadFile(privateKeyFile) // #nosec G304 -- user-provided file path
142319
if err != nil {
@@ -184,3 +361,11 @@ func (j *esjwtDecoderWithPublicKeyFile) Decode(token string) (string, error) {
184361
}
185362
return j.decoder.DecodeJWT(key, token)
186363
}
364+
365+
func (j *esjwtEncoderWithCachedPrivateKey) Encode(payload string) (string, error) {
366+
return j.encoder.EncodeJWT(j.privateKey, j.method, payload)
367+
}
368+
369+
func (j *esjwtDecoderWithCachedPublicKey) Decode(token string) (string, error) {
370+
return j.decoder.DecodeJWT(j.publicKey, token)
371+
}

pkg/cryptojwt/esjwt_test.go

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ func TestECDSAComplexPayloads(t *testing.T) {
483483
t.Run("large payload", func(t *testing.T) {
484484
privateKeyPath, publicKeyPath := generateECDSAKeyPair(t, elliptic.P256())
485485
largePayload := `{"data":[0,1,2,3,4,5,6,7,8,9],"more":"test data"}`
486-
486+
487487
encoder := cryptojwt.NewES256Encoder(privateKeyPath)
488488
token, err := encoder.Encode(largePayload)
489489
if err != nil {
@@ -499,4 +499,62 @@ func TestECDSAComplexPayloads(t *testing.T) {
499499
t.Errorf("Expected decoded payload to contain data field")
500500
}
501501
})
502+
}
503+
504+
// Benchmarks comparing cached vs uncached performance
505+
506+
func BenchmarkES256EncodeWithoutCache(b *testing.B) {
507+
privateKeyPath, _ := generateECDSAKeyPair(b, elliptic.P256())
508+
encoder := cryptojwt.NewES256Encoder(privateKeyPath)
509+
510+
b.ResetTimer()
511+
for i := 0; i < b.N; i++ {
512+
_, _ = encoder.Encode(validPayload)
513+
}
514+
}
515+
516+
func BenchmarkES256EncodeWithCache(b *testing.B) {
517+
privateKeyPath, _ := generateECDSAKeyPair(b, elliptic.P256())
518+
encoder, err := cryptojwt.NewES256EncoderWithCache(privateKeyPath)
519+
if err != nil {
520+
b.Fatalf("Failed to create cached encoder: %v", err)
521+
}
522+
523+
b.ResetTimer()
524+
for i := 0; i < b.N; i++ {
525+
_, _ = encoder.Encode(validPayload)
526+
}
527+
}
528+
529+
func BenchmarkES256DecodeWithoutCache(b *testing.B) {
530+
privateKeyPath, publicKeyPath := generateECDSAKeyPair(b, elliptic.P256())
531+
encoder := cryptojwt.NewES256Encoder(privateKeyPath)
532+
token, err := encoder.Encode(validPayload)
533+
if err != nil {
534+
b.Fatalf("Failed to encode: %v", err)
535+
}
536+
decoder := cryptojwt.NewES256DecoderWithPublicKeyFile(publicKeyPath)
537+
538+
b.ResetTimer()
539+
for i := 0; i < b.N; i++ {
540+
_, _ = decoder.Decode(token)
541+
}
542+
}
543+
544+
func BenchmarkES256DecodeWithCache(b *testing.B) {
545+
privateKeyPath, publicKeyPath := generateECDSAKeyPair(b, elliptic.P256())
546+
encoder := cryptojwt.NewES256Encoder(privateKeyPath)
547+
token, err := encoder.Encode(validPayload)
548+
if err != nil {
549+
b.Fatalf("Failed to encode: %v", err)
550+
}
551+
decoder, err := cryptojwt.NewES256DecoderWithPublicKeyFileAndCache(publicKeyPath)
552+
if err != nil {
553+
b.Fatalf("Failed to create cached decoder: %v", err)
554+
}
555+
556+
b.ResetTimer()
557+
for i := 0; i < b.N; i++ {
558+
_, _ = decoder.Decode(token)
559+
}
502560
}

0 commit comments

Comments
 (0)