diff --git a/core/auth.go b/core/auth.go new file mode 100644 index 0000000000..558cd834a5 --- /dev/null +++ b/core/auth.go @@ -0,0 +1,10 @@ +package core + +type BlobRequestAuthenticator interface { + AuthenticateBlobRequest(header BlobAuthHeader) error +} + +type BlobRequestSigner interface { + SignBlobRequest(header BlobAuthHeader) ([]byte, error) + GetAccountID() string +} diff --git a/core/auth/auth_test.go b/core/auth/auth_test.go new file mode 100644 index 0000000000..5a0953ebe1 --- /dev/null +++ b/core/auth/auth_test.go @@ -0,0 +1,67 @@ +package auth_test + +import ( + "math/rand" + "testing" + + "github.com/Layr-Labs/eigenda/core" + "github.com/Layr-Labs/eigenda/core/auth" + "github.com/stretchr/testify/assert" +) + +func TestAuthentication(t *testing.T) { + + // Make the authenticator + authenticator := auth.NewAuthenticator(auth.AuthConfig{}) + + // Make the signer + privateKeyHex := "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + signer := auth.NewSigner(privateKeyHex) + + testHeader := core.BlobAuthHeader{ + BlobCommitments: core.BlobCommitments{}, + AccountID: signer.GetAccountID(), + Nonce: rand.Uint32(), + AuthenticationData: []byte{}, + } + + // Sign the header + signature, err := signer.SignBlobRequest(testHeader) + assert.NoError(t, err) + + testHeader.AuthenticationData = signature + + err = authenticator.AuthenticateBlobRequest(testHeader) + assert.NoError(t, err) + +} + +func TestAuthenticationFail(t *testing.T) { + + // Make the authenticator + authenticator := auth.NewAuthenticator(auth.AuthConfig{}) + + // Make the signer + privateKeyHex := "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + signer := auth.NewSigner(privateKeyHex) + + testHeader := core.BlobAuthHeader{ + BlobCommitments: core.BlobCommitments{}, + AccountID: signer.GetAccountID(), + Nonce: rand.Uint32(), + AuthenticationData: []byte{}, + } + + privateKeyHex = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcded" + signer = auth.NewSigner(privateKeyHex) + + // Sign the header + signature, err := signer.SignBlobRequest(testHeader) + assert.NoError(t, err) + + testHeader.AuthenticationData = signature + + err = authenticator.AuthenticateBlobRequest(testHeader) + assert.Error(t, err) + +} diff --git a/core/auth/authenticator.go b/core/auth/authenticator.go new file mode 100644 index 0000000000..a92842fc2c --- /dev/null +++ b/core/auth/authenticator.go @@ -0,0 +1,64 @@ +package auth + +import ( + "bytes" + "encoding/binary" + "fmt" + + "github.com/Layr-Labs/eigenda/core" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" +) + +type AuthConfig struct { +} + +type authenticator struct { + config AuthConfig +} + +func NewAuthenticator(config AuthConfig) core.BlobRequestAuthenticator { + + return &authenticator{ + config: config, + } +} + +func (*authenticator) AuthenticateBlobRequest(header core.BlobAuthHeader) error { + + buf := make([]byte, 4) + binary.BigEndian.PutUint32(buf, header.Nonce) + hash := crypto.Keccak256(buf) + + publicKeyBytes, err := hexutil.Decode(header.AccountID) + if err != nil { + return fmt.Errorf("failed to decode public key (%v): %v", header.AccountID, err) + } + + sig := header.AuthenticationData + + // Decode public key + pubKey, err := crypto.UnmarshalPubkey(publicKeyBytes) + if err != nil { + return fmt.Errorf("failed to decode public key (%v): %v", header.AccountID, err) + } + + // Ensure the signature is 65 bytes (Recovery ID is the last byte) + if sig == nil || len(sig) != 65 { + return fmt.Errorf("signature length is unexpected: %d", len(sig)) + } + + // Verify the signature + sigPublicKeyECDSA, err := crypto.SigToPub(hash, sig) + if err != nil { + return fmt.Errorf("failed to recover public key from signature: %v", err) + } + + if !bytes.Equal(pubKey.X.Bytes(), sigPublicKeyECDSA.X.Bytes()) || !bytes.Equal(pubKey.Y.Bytes(), sigPublicKeyECDSA.Y.Bytes()) { + return fmt.Errorf("signature doesn't match with provided public key") + } + + return nil + +} diff --git a/core/auth/signer.go b/core/auth/signer.go new file mode 100644 index 0000000000..69baec49cf --- /dev/null +++ b/core/auth/signer.go @@ -0,0 +1,53 @@ +package auth + +import ( + "crypto/ecdsa" + "encoding/binary" + "fmt" + "log" + + "github.com/Layr-Labs/eigenda/core" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" +) + +type signer struct { + PrivateKey *ecdsa.PrivateKey +} + +func NewSigner(privateKeyHex string) core.BlobRequestSigner { + + privateKeyBytes := common.FromHex(privateKeyHex) + privateKey, err := crypto.ToECDSA(privateKeyBytes) + if err != nil { + log.Fatalf("Failed to parse private key: %v", err) + } + + return &signer{ + PrivateKey: privateKey, + } +} + +func (s *signer) SignBlobRequest(header core.BlobAuthHeader) ([]byte, error) { + + // Message you want to sign + buf := make([]byte, 4) + binary.BigEndian.PutUint32(buf, header.Nonce) + hash := crypto.Keccak256(buf) + + // Sign the hash using the private key + sig, err := crypto.Sign(hash, s.PrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to sign hash: %v", err) + } + + return sig, nil +} + +func (s *signer) GetAccountID() string { + + publicKeyBytes := crypto.FromECDSAPub(&s.PrivateKey.PublicKey) + return hexutil.Encode(publicKeyBytes) + +} diff --git a/core/data.go b/core/data.go index bd7a453def..820a32f883 100644 --- a/core/data.go +++ b/core/data.go @@ -40,6 +40,21 @@ type Blob struct { Data []byte } +// BlobAuthHeader contains the data that a user must sign to authenticate a blob request. +// Signing the combination of the Nonce and the BlobCommitments prohibits the disperser from +// using the signature to charge the user for a different blob or for dispersing the same blob +// multiple times (Replay attack). +type BlobAuthHeader struct { + // Commitments + BlobCommitments `json:"commitments"` + // AccountID is the account that is paying for the blob to be stored. AccountID is hexadecimal representation of the ECDSA public key + AccountID AccountID `json:"account_id"` + // Nonce + Nonce uint32 `json:"nonce"` + // AuthenticationData is the signature of the blob header by the account ID + AuthenticationData []byte `json:"authentication_data"` +} + // BlobRequestHeader contains the original data size of a blob and the security required type BlobRequestHeader struct { // Commitments