Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -248,5 +268,5 @@
}
]
},
"generated_at": "2023-09-27T08:06:22Z"
"generated_at": "2024-08-14T13:57:27Z"
}
1 change: 1 addition & 0 deletions changes/20240814105842.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: Add support for signing and verifying messages using ed25519
15 changes: 15 additions & 0 deletions utils/signing/interface.go
Original file line number Diff line number Diff line change
@@ -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)
}
182 changes: 182 additions & 0 deletions utils/signing/signing.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading