diff --git a/pkg/crypto/keys/keys.go b/pkg/crypto/keys/keys.go new file mode 100644 index 000000000..042eae5f8 --- /dev/null +++ b/pkg/crypto/keys/keys.go @@ -0,0 +1,55 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +// Package keys provides utilities for generating cryptographic keys +package keys + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "fmt" +) + +// Type represents the type of cryptographic key +type Type string + +const ( + // TypeEC256 represents ECDSA with P-256 curve + TypeEC256 Type = "EC256" + // TypeEC384 represents ECDSA with P-384 curve + TypeEC384 Type = "EC384" + // TypeRSA2048 represents RSA with 2048-bit key + TypeRSA2048 Type = "RSA2048" + // TypeRSA4096 represents RSA with 4096-bit key + TypeRSA4096 Type = "RSA4096" +) + +// Generate creates a new private key of the specified type +func Generate(keyType Type) (crypto.Signer, error) { + switch keyType { + case TypeEC256: + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case TypeEC384: + return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case TypeRSA2048: + return rsa.GenerateKey(rand.Reader, 2048) + case TypeRSA4096: + return rsa.GenerateKey(rand.Reader, 4096) + default: + return nil, fmt.Errorf("unsupported key type: %s", keyType) + } +} diff --git a/pkg/crypto/keys/keys_test.go b/pkg/crypto/keys/keys_test.go new file mode 100644 index 000000000..6b69a9789 --- /dev/null +++ b/pkg/crypto/keys/keys_test.go @@ -0,0 +1,165 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package keys_test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "errors" + "testing" + + "github.com/getprobo/probo/pkg/crypto/keys" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerate(t *testing.T) { + tests := []struct { + name string + keyType keys.Type + checkFunc func(t *testing.T, key interface{}) + expectError bool + }{ + { + name: "EC256", + keyType: keys.TypeEC256, + checkFunc: func(t *testing.T, key interface{}) { + ecKey, ok := key.(*ecdsa.PrivateKey) + require.True(t, ok, "expected *ecdsa.PrivateKey, got %T", key) + assert.Equal(t, elliptic.P256(), ecKey.Curve, "expected P256 curve") + }, + }, + { + name: "EC384", + keyType: keys.TypeEC384, + checkFunc: func(t *testing.T, key interface{}) { + ecKey, ok := key.(*ecdsa.PrivateKey) + require.True(t, ok, "expected *ecdsa.PrivateKey, got %T", key) + assert.Equal(t, elliptic.P384(), ecKey.Curve, "expected P384 curve") + }, + }, + { + name: "RSA2048", + keyType: keys.TypeRSA2048, + checkFunc: func(t *testing.T, key interface{}) { + rsaKey, ok := key.(*rsa.PrivateKey) + require.True(t, ok, "expected *rsa.PrivateKey, got %T", key) + bitSize := rsaKey.N.BitLen() + assert.GreaterOrEqual(t, bitSize, 2047, "RSA key too small") + assert.LessOrEqual(t, bitSize, 2048, "RSA key too large") + }, + }, + { + name: "RSA4096", + keyType: keys.TypeRSA4096, + checkFunc: func(t *testing.T, key interface{}) { + rsaKey, ok := key.(*rsa.PrivateKey) + require.True(t, ok, "expected *rsa.PrivateKey, got %T", key) + bitSize := rsaKey.N.BitLen() + assert.GreaterOrEqual(t, bitSize, 4095, "RSA key too small") + assert.LessOrEqual(t, bitSize, 4096, "RSA key too large") + }, + }, + { + name: "Invalid key type", + keyType: keys.Type("INVALID"), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key, err := keys.Generate(tt.keyType) + + if tt.expectError { + assert.Error(t, err, "expected error") + return + } + + require.NoError(t, err, "unexpected error") + require.NotNil(t, key, "expected key, got nil") + + // Verify the key can be used for signing (crypto.Signer has Public() method) + assert.NotNil(t, key.Public(), "key.Public() returned nil") + + if tt.checkFunc != nil { + tt.checkFunc(t, key) + } + }) + } +} + +func TestGenerateConcurrency(t *testing.T) { + // Test that key generation is safe for concurrent use + keyTypes := []keys.Type{ + keys.TypeEC256, + keys.TypeEC384, + keys.TypeRSA2048, + keys.TypeRSA4096, + } + + for _, keyType := range keyTypes { + t.Run(string(keyType), func(t *testing.T) { + t.Parallel() + + const numGoroutines = 10 + errorsChan := make(chan error, numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func() { + key, err := keys.Generate(keyType) + if err != nil { + errorsChan <- err + return + } + if key == nil { + errorsChan <- errors.New("generated key is nil") + return + } + errorsChan <- nil + }() + } + + for i := 0; i < numGoroutines; i++ { + err := <-errorsChan + assert.NoError(t, err, "concurrent generation failed") + } + }) + } +} + +func BenchmarkGenerate(b *testing.B) { + benchmarks := []struct { + name string + keyType keys.Type + }{ + {"EC256", keys.TypeEC256}, + {"EC384", keys.TypeEC384}, + {"RSA2048", keys.TypeRSA2048}, + {"RSA4096", keys.TypeRSA4096}, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := keys.Generate(bm.keyType) + if err != nil { + b.Fatal(err) + } + } + }) + } +} \ No newline at end of file