diff --git a/.secrets.baseline b/.secrets.baseline index 18172b6ec7..ede9692d9c 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -66,6 +66,10 @@ { "path": "detect_secrets.filters.allowlist.is_line_allowlisted" }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", "min_level": 2 @@ -238,6 +242,22 @@ "line_number": 84 } ], + "utils/signing/signing_test.go": [ + { + "type": "Base64 High Entropy String", + "filename": "utils/signing/signing_test.go", + "hashed_secret": "46d495b98efbb227351ba4b3bac4e2fe537270ae", + "is_verified": false, + "line_number": 76 + }, + { + "type": "Base64 High Entropy String", + "filename": "utils/signing/signing_test.go", + "hashed_secret": "48fb0bae9145db4e31dbe2e7c450e0fca3f8d530", + "is_verified": false, + "line_number": 268 + } + ], "utils/strings/strings_test.go": [ { "type": "Base64 High Entropy String", @@ -248,5 +268,5 @@ } ] }, - "generated_at": "2023-09-27T08:06:22Z" + "generated_at": "2024-08-14T13:57:27Z" } diff --git a/changes/20240814105842.feature b/changes/20240814105842.feature new file mode 100644 index 0000000000..fe969fc690 --- /dev/null +++ b/changes/20240814105842.feature @@ -0,0 +1 @@ +:sparkles: Add support for signing and verifying messages using ed25519 diff --git a/utils/signing/interface.go b/utils/signing/interface.go new file mode 100644 index 0000000000..8ddd424ceb --- /dev/null +++ b/utils/signing/interface.go @@ -0,0 +1,15 @@ +package signing + +import "github.com/ARM-software/golang-utils/utils/encryption" + +type ICodeSigner interface { + encryption.IKeyPair + // Sign will sign a message and return a signature + Sign(message []byte) (signature []byte, err error) + // Verify will take a message and a signature and verify whether the signature is a valid signature of the message based on the signers public key + Verify(message, signature []byte) (ok bool, err error) + // GenerateSignature will sign a message but return a base64 encoded signature for ease of use + GenerateSignature(message []byte) (signatureBase64 string, err error) + // Verify will take a message and a base64 encoded signature and verify whether the signature is a valid signature of the message based on the signers public key + VerifySignature(message []byte, signatureBase64 string) (ok bool, err error) +} diff --git a/utils/signing/signing.go b/utils/signing/signing.go new file mode 100644 index 0000000000..c5a6410489 --- /dev/null +++ b/utils/signing/signing.go @@ -0,0 +1,182 @@ +package signing + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + + "github.com/ARM-software/golang-utils/utils/commonerrors" +) + +type Ed25519Signer struct { + Public ed25519.PublicKey `json:"public"` + private ed25519.PrivateKey `json:"-"` + seed []byte `json:"-"` +} + +func (k *Ed25519Signer) String() string { + return fmt.Sprintf("{Public: %v}", k.GetPublicKey()) +} + +func (k *Ed25519Signer) GoString() string { + return fmt.Sprintf("KeyPair(%q)", k.String()) +} + +func (k *Ed25519Signer) MarshalJSON() (jsonBytes []byte, err error) { + return []byte(fmt.Sprintf("{\"public\":%q}", k.GetPublicKey())), nil +} + +func (k *Ed25519Signer) GetPublicKey() string { + return base64.StdEncoding.EncodeToString(k.Public) +} + +func (k *Ed25519Signer) GetPrivateKey() string { + return base64.StdEncoding.EncodeToString(k.private) +} + +func (k *Ed25519Signer) Sign(message []byte) (signature []byte, err error) { + if len(k.private) == 0 { + err = fmt.Errorf("%w: missing private key", commonerrors.ErrUndefined) + return + } + if len(k.private) != ed25519.PrivateKeySize { + err = fmt.Errorf("%w: invalid private key length %v", commonerrors.ErrInvalid, len(k.private)) + return + } + + signature, err = k.private.Sign(nil, message, &ed25519.Options{}) + if err != nil { + err = fmt.Errorf("%w: error occured whilst signing: %v", commonerrors.ErrUnexpected, err.Error()) + return + } + + return +} + +func (k *Ed25519Signer) GenerateSignature(message []byte) (signatureBase64 string, err error) { + signature, err := k.Sign(message) + if err != nil { + return + } + signatureBase64 = base64.StdEncoding.EncodeToString(signature) + return +} + +func (k *Ed25519Signer) Verify(message, signature []byte) (ok bool, err error) { + if len(k.Public) == 0 { + err = fmt.Errorf("%w: missing public key", commonerrors.ErrUndefined) + return + } + if len(k.Public) != ed25519.PublicKeySize { + err = fmt.Errorf("%w: invalid public key length %v", commonerrors.ErrInvalid, len(k.Public)) + return + } + + ok = ed25519.Verify(k.Public, message, signature) + return +} + +func (k *Ed25519Signer) VerifySignature(message []byte, signatureBase64 string) (ok bool, err error) { + signature, err := base64.StdEncoding.DecodeString(signatureBase64) + if err != nil { + return + } + + ok, err = k.Verify(message, signature) + return +} + +// NewEd25519Signer will create a Ed25519Signer that can both sign new messages as well as verify them +func NewEd25519Signer(privateKey ed25519.PrivateKey) (signer *Ed25519Signer, err error) { + if privateKey == nil { + err = fmt.Errorf("%w: privateKey must be defined", commonerrors.ErrUndefined) + return + } + if len(privateKey) != ed25519.PrivateKeySize { + err = fmt.Errorf("%w: private key must have length %v, it has length %v", commonerrors.ErrInvalid, ed25519.PrivateKeySize, len(privateKey)) + return + } + + publicKey, ok := privateKey.Public().(ed25519.PublicKey) + if !ok { + err = fmt.Errorf("%w: could not extract public key from private key", commonerrors.ErrUnexpected) + return + } + + signer = &Ed25519Signer{ + Public: publicKey, + private: privateKey, + seed: privateKey.Seed(), + } + + return +} + +// NewEd25519Verifier will create a Ed25519Signer with only a public key meaning it can only verify messages +func NewEd25519Verifier(publicKey ed25519.PublicKey) (signer *Ed25519Signer, err error) { + if publicKey == nil { + err = fmt.Errorf("%w: publicKey must be defined", commonerrors.ErrUndefined) + return + } + if len(publicKey) != ed25519.PublicKeySize { + err = fmt.Errorf("%w: public key must have length %v, it has length %v", commonerrors.ErrInvalid, ed25519.PrivateKeySize, len(publicKey)) + return + } + + signer = &Ed25519Signer{ + Public: publicKey, + } + + return +} + +// NewEd25519SignerFromBase64 will create a Ed25519Signer that can both sign new messages as well as verify them +// It will take a private key encoded as base64 +func NewEd25519SignerFromBase64(privateKeyB64 string) (signer *Ed25519Signer, err error) { + privateKey, err := base64.StdEncoding.DecodeString(privateKeyB64) + if err != nil { + err = fmt.Errorf("%w: could not decode private key from base64: %v", commonerrors.ErrInvalid, err.Error()) + return + } + + return NewEd25519Signer(privateKey) +} + +// NewEd25519VerifierFromBase64 will create a Ed25519Signer with only a public key meaning it can only verify messages +// It will take a public key encoded as base64 +func NewEd25519VerifierFromBase64(publicKeyB64 string) (signer *Ed25519Signer, err error) { + publicKey, err := base64.StdEncoding.DecodeString(publicKeyB64) + if err != nil { + err = fmt.Errorf("%w: could not decode public key from base64: %v", commonerrors.ErrInvalid, err.Error()) + return + } + + return NewEd25519Verifier(publicKey) +} + +// NewEd25519SignerFromSeed will create an Ed25519Signer based on a seed. It will automatically pad the seed to the correct length +// A seed for Ed25519 should be 32 characters long. Anything shorter will be padded with zeros and anything longer will be truncated +func NewEd25519SignerFromSeed(inputSeed string) (pair *Ed25519Signer, err error) { + seed := make([]byte, ed25519.SeedSize) + if inputSeed == "" { + _, err = io.ReadFull(rand.Reader, seed) + if err != nil { + return + } + } else { + for i := range seed { + if i < len(inputSeed) { + seed[i] = inputSeed[i] + } else { + seed[i] = '0' + } + } + } + + privateKey := ed25519.NewKeyFromSeed(seed) + pair, err = NewEd25519Signer(privateKey) + pair.seed = seed + return +} diff --git a/utils/signing/signing_test.go b/utils/signing/signing_test.go new file mode 100644 index 0000000000..f00d27b626 --- /dev/null +++ b/utils/signing/signing_test.go @@ -0,0 +1,270 @@ +package signing + +import ( + "crypto/ed25519" + "encoding/base64" + "testing" + + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/commonerrors/errortest" +) + +func TestSigning(t *testing.T) { + t.Run("Successful sign", func(t *testing.T) { + message := []byte(faker.Word()) + + signer, err := NewEd25519SignerFromSeed(faker.Word()) + require.NoError(t, err) + + signature, err := signer.Sign(message) + require.NoError(t, err) + + ok, err := signer.Verify(message, signature) + require.NoError(t, err) + assert.True(t, ok) + }) + + t.Run("Invalid message", func(t *testing.T) { + message := []byte(faker.Word()) + + signer, err := NewEd25519SignerFromSeed(faker.Word()) + require.NoError(t, err) + + signature, err := signer.Sign(message) + require.NoError(t, err) + + ok, err := signer.Verify([]byte(faker.Word()), signature) + require.NoError(t, err) + assert.False(t, ok) + }) + + t.Run("Invalid signature", func(t *testing.T) { + message := []byte(faker.Word()) + + signer, err := NewEd25519SignerFromSeed(faker.Word()) + require.NoError(t, err) + + wrongSignature, err := signer.Sign([]byte(faker.Word())) + require.NoError(t, err) + + ok, err := signer.Verify(message, wrongSignature) + require.NoError(t, err) + assert.False(t, ok) + }) +} + +func TestNewSigner(t *testing.T) { + t.Run("Test signing key from seed", func(t *testing.T) { + k, err := NewEd25519SignerFromSeed("1234") + require.NoError(t, err) + assert.Equal(t, k.seed, []byte("12340000000000000000000000000000")) + assert.EqualValues(t, k.private, []byte{0x31, 0x32, 0x33, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf1, 0x8, 0x8e, 0x21, 0xe9, 0x5c, 0xc9, 0xf6, 0x34, 0xf5, 0x73, 0xbe, 0xc4, 0x6, 0xd7, 0xa6, 0xd, 0xc1, 0x7b, 0x9d, 0xb3, 0x9a, 0xa0, 0x92, 0xdb, 0x70, 0x8e, 0x6, 0x5, 0x1e, 0x16, 0xfb}) + assert.EqualValues(t, k.Public, []byte{0xf1, 0x8, 0x8e, 0x21, 0xe9, 0x5c, 0xc9, 0xf6, 0x34, 0xf5, 0x73, 0xbe, 0xc4, 0x6, 0xd7, 0xa6, 0xd, 0xc1, 0x7b, 0x9d, 0xb3, 0x9a, 0xa0, 0x92, 0xdb, 0x70, 0x8e, 0x6, 0x5, 0x1e, 0x16, 0xfb}) + assert.Equal(t, k.Public, k.private.Public()) + }) + + t.Run("Test NewEd25519Signer", func(t *testing.T) { + testValidKey, err := NewEd25519SignerFromSeed("12234778") + require.NoError(t, err) + k, err := NewEd25519Signer(testValidKey.private) + assert.NoError(t, err) + assert.Equal(t, testValidKey, k) + assert.Equal(t, "MTIyMzQ3NzgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAODBfpjfvG3GlkUsrmSBcFnmUSSh63RXhczGbdd9oUfQ==", k.GetPrivateKey()) + }) + + t.Run("Test NewEd25519Signer (not valid)", func(t *testing.T) { + k, err := NewEd25519Signer([]byte("not a private key")) + errortest.AssertError(t, err, commonerrors.ErrInvalid) + assert.Nil(t, k) + }) + + t.Run("Test NewEd25519Signer (private empty)", func(t *testing.T) { + k, err := NewEd25519Signer([]byte{}) + errortest.AssertError(t, err, commonerrors.ErrInvalid) + assert.Nil(t, k) + }) + + t.Run("test base64 private key", func(t *testing.T) { + testKey, err := NewEd25519SignerFromSeed("1234") + require.NoError(t, err) + + _, err = NewEd25519SignerFromBase64(base64.StdEncoding.EncodeToString(testKey.private)) + require.NoError(t, err) + }) + + t.Run("test invalid base64 public key", func(t *testing.T) { + testKey, err := NewEd25519SignerFromBase64("i am not base64 8907987_^?%+") + errortest.AssertError(t, err, commonerrors.ErrInvalid) + assert.Nil(t, testKey) + }) +} + +func TestSign(t *testing.T) { + t.Run("test valid private key", func(t *testing.T) { + testKey, err := NewEd25519SignerFromSeed("1234") + require.NoError(t, err) + _, err = testKey.Sign([]byte(faker.Sentence())) + assert.NoError(t, err) + }) + + t.Run("test invalid private key", func(t *testing.T) { + testKey, err := NewEd25519SignerFromSeed("1234") + require.NoError(t, err) + testKey.private = ed25519.PrivateKey([]byte("thiswontbevalid")) + _, err = testKey.Sign([]byte(faker.Sentence())) + errortest.AssertError(t, err, commonerrors.ErrInvalid) + }) + + t.Run("test no private key", func(t *testing.T) { + testKey, err := NewEd25519SignerFromSeed("1234") + require.NoError(t, err) + testKey.private = ed25519.PrivateKey{} + _, err = testKey.Sign([]byte(faker.Sentence())) + errortest.AssertError(t, err, commonerrors.ErrUndefined) + }) + + t.Run("test base64", func(t *testing.T) { + testKey, err := NewEd25519SignerFromSeed("1234") + require.NoError(t, err) + + testMessage := []byte(faker.Sentence()) + + sigNormal, err := testKey.Sign(testMessage) + require.NoError(t, err) + + sigBase64, err := testKey.GenerateSignature(testMessage) + require.NoError(t, err) + + assert.Equal(t, sigBase64, base64.StdEncoding.EncodeToString(sigNormal)) + }) +} + +func TestVerify(t *testing.T) { + testKey, err := NewEd25519SignerFromSeed("1234") + require.NoError(t, err) + + publicKey := testKey.Public + message := []byte(faker.Sentence()) + signature, err := testKey.Sign(message) + require.NoError(t, err) + signatureBase64 := base64.StdEncoding.EncodeToString(signature) + + t.Run("test valid private key", func(t *testing.T) { + testKey, err := NewEd25519Verifier(publicKey) + require.NoError(t, err) + ok, err := testKey.Verify(message, signature) + assert.NoError(t, err) + assert.True(t, ok) + }) + + t.Run("test missing public key", func(t *testing.T) { + testKey, err := NewEd25519Verifier(publicKey) + require.NoError(t, err) + testKey.Public = ed25519.PublicKey{} + ok, err := testKey.Verify(message, signature) + errortest.AssertError(t, err, commonerrors.ErrUndefined) + assert.False(t, ok) + }) + + t.Run("test invalid length public key", func(t *testing.T) { + testKey, err := NewEd25519Verifier(publicKey) + require.NoError(t, err) + testKey.Public = testKey.Public[2:18] + ok, err := testKey.Verify(message, signature) + errortest.AssertError(t, err, commonerrors.ErrInvalid) + assert.False(t, ok) + }) + + t.Run("test valid private key (using base64)", func(t *testing.T) { + testKey, err := NewEd25519Verifier(publicKey) + require.NoError(t, err) + ok, err := testKey.VerifySignature(message, signatureBase64) + assert.NoError(t, err) + assert.True(t, ok) + }) + + t.Run("test missing public key (using base64)", func(t *testing.T) { + testKey, err := NewEd25519Verifier(publicKey) + require.NoError(t, err) + testKey.Public = ed25519.PublicKey{} + ok, err := testKey.VerifySignature(message, signatureBase64) + errortest.AssertError(t, err, commonerrors.ErrUndefined) + assert.False(t, ok) + }) + + t.Run("test invalid length public key (using base64)", func(t *testing.T) { + testKey, err := NewEd25519Verifier(publicKey) + require.NoError(t, err) + testKey.Public = testKey.Public[2:18] + ok, err := testKey.VerifySignature(message, signatureBase64) + errortest.AssertError(t, err, commonerrors.ErrInvalid) + assert.False(t, ok) + }) +} + +func TestNewVerifier(t *testing.T) { + t.Run("test valid public key", func(t *testing.T) { + testKey, err := NewEd25519SignerFromSeed("1234") + require.NoError(t, err) + + publicKey := testKey.Public + _, err = NewEd25519Verifier(publicKey) + require.NoError(t, err) + }) + + t.Run("test missing public key", func(t *testing.T) { + testKey, err := NewEd25519Verifier(ed25519.PublicKey{}) + errortest.AssertError(t, err, commonerrors.ErrInvalid) + assert.Nil(t, testKey) + }) + + t.Run("test invalid length public key", func(t *testing.T) { + testKey, err := NewEd25519SignerFromSeed("1234") + require.NoError(t, err) + + publicKey := testKey.Public + + testKey, err = NewEd25519Verifier(publicKey[2:18]) + errortest.AssertError(t, err, commonerrors.ErrInvalid) + assert.Nil(t, testKey) + }) + + t.Run("test base64 public key", func(t *testing.T) { + testKey, err := NewEd25519SignerFromSeed("1234") + require.NoError(t, err) + + _, err = NewEd25519VerifierFromBase64(base64.StdEncoding.EncodeToString(testKey.Public)) + require.NoError(t, err) + }) + + t.Run("test invalid base64 public key", func(t *testing.T) { + testKey, err := NewEd25519VerifierFromBase64("i am not base64 8907987_^?%+") + errortest.AssertError(t, err, commonerrors.ErrInvalid) + assert.Nil(t, testKey) + }) +} + +func TestEncryptionRequiredMethods(t *testing.T) { + testKey, err := NewEd25519SignerFromSeed("1234791289218") + require.NoError(t, err) + + t.Run("String", func(t *testing.T) { + s := testKey.String() + assert.Equal(t, "{Public: fK8uaDt/2+1RoVwnj4JotkAu3SGm7cf5RhpZNWbLPSA=}", s) + }) + + t.Run("GoString", func(t *testing.T) { + s := testKey.GoString() + assert.Equal(t, "KeyPair(\"{Public: fK8uaDt/2+1RoVwnj4JotkAu3SGm7cf5RhpZNWbLPSA=}\")", s) + }) + + t.Run("MarshalJSON", func(t *testing.T) { + b, err := testKey.MarshalJSON() + require.NoError(t, err) + assert.Equal(t, "{\"public\":\"fK8uaDt/2+1RoVwnj4JotkAu3SGm7cf5RhpZNWbLPSA=\"}", string(b)) + }) +}