Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disperser auth #984

Open
wants to merge 58 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
0936f5f
Enable TLS connections between the disperser and the DA nodes.
cody-littley Dec 4, 2024
d36424e
Incremental progress.
cody-littley Dec 4, 2024
342d500
Moar cowsay
cody-littley Dec 4, 2024
fd2ab69
formatting
cody-littley Dec 4, 2024
e2e3fef
Started working on unit test.
cody-littley Dec 4, 2024
4373095
Incremental progress.
cody-littley Dec 4, 2024
df38980
It works now, kind of
cody-littley Dec 4, 2024
d54c7f7
Incremental progress.
cody-littley Dec 5, 2024
083d67d
Start experimenting with ecdsa.
cody-littley Dec 6, 2024
0a5c91a
Authorize StoreChunks() requests.
cody-littley Dec 10, 2024
8e9c16b
Merge branch 'master' into disperser-auth
cody-littley Dec 10, 2024
f348550
Update docs.
cody-littley Dec 10, 2024
01b7ece
Incremental progress.
cody-littley Dec 11, 2024
e09a77c
Incremental progress.
cody-littley Dec 11, 2024
290c93c
Incremental progress.
cody-littley Dec 11, 2024
5ba4b7b
Incremental progress.
cody-littley Dec 11, 2024
e094371
Added test placeholder.
cody-littley Dec 11, 2024
db0885a
shuffle stuff around
cody-littley Dec 12, 2024
da2ca6c
Merge branch 'master' into disperser-auth
cody-littley Dec 12, 2024
b04fcfa
Unit tests for request signing.
cody-littley Dec 12, 2024
29e7e7d
Delete stale code.
cody-littley Dec 12, 2024
fb1ebfb
Get things kind of working
cody-littley Dec 12, 2024
6ca7c9b
Finished unit tests.
cody-littley Dec 13, 2024
3348883
Cleanup.
cody-littley Dec 13, 2024
e5966f0
Cleanup
cody-littley Dec 13, 2024
f28a5d4
Merge branch 'master' into disperser-auth
cody-littley Dec 16, 2024
f0c6f4e
Cleanup.
cody-littley Dec 16, 2024
6a00256
Added request signer.
cody-littley Dec 16, 2024
27d5b8a
Incremental progress.
cody-littley Dec 16, 2024
bbd82ec
Update flags.
cody-littley Dec 16, 2024
157c2ea
Started work on unit test, not yet working.
cody-littley Dec 16, 2024
d793501
Incremental progress in kludging something together
cody-littley Dec 16, 2024
b81e430
Incremental test iteration.
cody-littley Dec 16, 2024
7327db1
Added debug printing.
cody-littley Dec 17, 2024
8cfa971
IT'S ALIVE!
cody-littley Dec 17, 2024
306746e
Partial cleanup.
cody-littley Dec 17, 2024
b41d729
Move code to proper locations.
cody-littley Dec 17, 2024
d709520
Copy eigensdk-go implementation of kms parsing.
cody-littley Dec 17, 2024
2e44b02
Update kms.go
anupsv Dec 18, 2024
aebda81
Create kms_fuzz_test.go
anupsv Dec 18, 2024
260bf79
Update Makefile for fuzz tests
anupsv Dec 18, 2024
dbd0d24
Merge pull request #1 from anupsv/disperser-auth
cody-littley Dec 18, 2024
5dbb178
Merge branch 'master' into disperser-auth
cody-littley Dec 18, 2024
dcd3e06
Disable request signing if KMS key name is not provided.
cody-littley Dec 18, 2024
11cf45a
Merge branch 'master' into disperser-auth
cody-littley Dec 18, 2024
3580bed
Merge branch 'master' into disperser-auth
cody-littley Dec 18, 2024
f6b008f
Tie into bindings.
cody-littley Dec 18, 2024
1493ded
Incremental progress.
cody-littley Dec 18, 2024
feab7a1
Merge branch 'master' into disperser-auth
cody-littley Dec 19, 2024
52f2354
Add debug code.
cody-littley Dec 19, 2024
7c5cc93
Fix unit test.
cody-littley Dec 19, 2024
c83db79
Disable request signing for inabox e2e test.
cody-littley Dec 19, 2024
e5cb69c
Added flag for disabling signing.
cody-littley Dec 19, 2024
20216c4
Cleanup.
cody-littley Dec 19, 2024
3c325ef
Made suggested changes.
cody-littley Dec 20, 2024
02ed38a
tweak docker build
cody-littley Dec 20, 2024
80c7e6d
Make requested changes.
cody-littley Dec 20, 2024
2b6cbb2
Made suggested changes.
cody-littley Dec 20, 2024
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
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \

# Controller build stage
FROM common-builder AS controller-builder
COPY node/auth /app/node/auth
WORKDIR /app/disperser
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ dataapi-build:
unit-tests:
./test.sh

fuzz-tests:
go test --fuzz=FuzzParseSignatureKMS -fuzztime=5m ./common

integration-tests-churner:
go test -v ./churner/tests

Expand Down
30 changes: 30 additions & 0 deletions api/clients/mock/static_request_signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package mock

import (
"context"
"crypto/ecdsa"
"github.com/Layr-Labs/eigenda/api/clients/v2"
v2 "github.com/Layr-Labs/eigenda/api/grpc/node/v2"
"github.com/Layr-Labs/eigenda/node/auth"
)

var _ clients.DispersalRequestSigner = &staticRequestSigner{}

// StaticRequestSigner is a DispersalRequestSigner that signs requests with a static key (i.e. it doesn't use AWS KMS).
// Useful for testing.
type staticRequestSigner struct {
key *ecdsa.PrivateKey
}

func NewStaticRequestSigner(key *ecdsa.PrivateKey) clients.DispersalRequestSigner {
return &staticRequestSigner{
key: key,
}
}

func (s *staticRequestSigner) SignStoreChunksRequest(
ctx context.Context,
request *v2.StoreChunksRequest) ([]byte, error) {

return auth.SignStoreChunksRequest(s.key, request)
}
62 changes: 62 additions & 0 deletions api/clients/v2/dispersal_request_signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package clients

import (
"context"
"crypto/ecdsa"
"fmt"
grpc "github.com/Layr-Labs/eigenda/api/grpc/node/v2"
"github.com/Layr-Labs/eigenda/api/hashing"
"github.com/Layr-Labs/eigenda/common"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/kms"
)

// DispersalRequestSigner encapsulates the logic for signing GetChunks requests.
type DispersalRequestSigner interface {
// SignStoreChunksRequest signs a StoreChunksRequest. Does not modify the request
// (i.e. it does not insert the signature).
SignStoreChunksRequest(ctx context.Context, request *grpc.StoreChunksRequest) ([]byte, error)
}

var _ DispersalRequestSigner = &requestSigner{}

type requestSigner struct {
keyID string
publicKey *ecdsa.PublicKey
keyManager *kms.Client
}

// NewDispersalRequestSigner creates a new DispersalRequestSigner.
func NewDispersalRequestSigner(
ctx context.Context,
region string,
endpoint string,
keyID string) (DispersalRequestSigner, error) {

keyManager := kms.New(kms.Options{
Region: region,
BaseEndpoint: aws.String(endpoint),
})

key, err := common.LoadPublicKeyKMS(ctx, keyManager, keyID)
if err != nil {
return nil, fmt.Errorf("failed to get ecdsa public key: %w", err)
}

return &requestSigner{
keyID: keyID,
publicKey: key,
keyManager: keyManager,
}, nil
}

func (s *requestSigner) SignStoreChunksRequest(ctx context.Context, request *grpc.StoreChunksRequest) ([]byte, error) {
hash := hashing.HashStoreChunksRequest(request)

signature, err := common.SignKMS(ctx, s.keyManager, s.keyID, s.publicKey, hash)
if err != nil {
return nil, fmt.Errorf("failed to sign request: %w", err)
}

return signature, nil
}
129 changes: 129 additions & 0 deletions api/clients/v2/dispersal_request_signer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package clients

import (
"context"
"github.com/Layr-Labs/eigenda/common"
"github.com/Layr-Labs/eigenda/common/testutils/random"
"github.com/Layr-Labs/eigenda/inabox/deploy"
"github.com/Layr-Labs/eigenda/node/auth"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/aws/aws-sdk-go-v2/service/kms/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ory/dockertest/v3"
"github.com/stretchr/testify/require"
"log"
"os"
"path/filepath"
"runtime"
"testing"
)

var (
dockertestPool *dockertest.Pool
dockertestResource *dockertest.Resource
)

const (
localstackPort = "4570"
localstackHost = "http://0.0.0.0:4570"
region = "us-east-1"
)

func setup(t *testing.T) {
deployLocalStack := !(os.Getenv("DEPLOY_LOCALSTACK") == "false")

_, b, _, _ := runtime.Caller(0)
rootPath := filepath.Join(filepath.Dir(b), "../../..")
changeDirectory(filepath.Join(rootPath, "inabox"))

if deployLocalStack {
var err error
dockertestPool, dockertestResource, err = deploy.StartDockertestWithLocalstackContainer(localstackPort)
require.NoError(t, err)
}
}

func changeDirectory(path string) {
err := os.Chdir(path)
if err != nil {

currentDirectory, err := os.Getwd()
if err != nil {
log.Printf("Failed to get current directory. Error: %s", err)
}

log.Panicf("Failed to change directories. CWD: %s, Error: %s", currentDirectory, err)
}

newDir, err := os.Getwd()
if err != nil {
log.Panicf("Failed to get working directory. Error: %s", err)
}
log.Printf("Current Working Directory: %s\n", newDir)
}

func teardown() {
deployLocalStack := !(os.Getenv("DEPLOY_LOCALSTACK") == "false")

if deployLocalStack {
deploy.PurgeDockertestResources(dockertestPool, dockertestResource)
}
}

func TestRequestSigning(t *testing.T) {
rand := random.NewTestRandom(t)
setup(t)
defer teardown()

keyManager := kms.New(kms.Options{
Region: region,
BaseEndpoint: aws.String(localstackHost),
})

for i := 0; i < 10; i++ {
createKeyOutput, err := keyManager.CreateKey(context.Background(), &kms.CreateKeyInput{
KeySpec: types.KeySpecEccSecgP256k1,
KeyUsage: types.KeyUsageTypeSignVerify,
})
require.NoError(t, err)

keyID := *createKeyOutput.KeyMetadata.KeyId

key, err := common.LoadPublicKeyKMS(context.Background(), keyManager, keyID)
require.NoError(t, err)

publicAddress := crypto.PubkeyToAddress(*key)

for j := 0; j < 10; j++ {
request := auth.RandomStoreChunksRequest(rand)
request.Signature = nil

signer, err := NewDispersalRequestSigner(context.Background(), region, localstackHost, keyID)
require.NoError(t, err)

// Test a valid signature.
signature, err := signer.SignStoreChunksRequest(context.Background(), request)
require.NoError(t, err)

require.Nil(t, request.Signature)
request.Signature = signature
err = auth.VerifyStoreChunksRequest(publicAddress, request)
require.NoError(t, err)

// Changing a byte in the middle of the signature should make the verification fail
badSignature := make([]byte, len(signature))
copy(badSignature, signature)
badSignature[10] = badSignature[10] + 1
request.Signature = badSignature
err = auth.VerifyStoreChunksRequest(publicAddress, request)
require.Error(t, err)

// Changing a byte in the middle of the request should make the verification fail
request.DisperserID = request.DisperserID + 1
request.Signature = signature
err = auth.VerifyStoreChunksRequest(publicAddress, request)
require.Error(t, err)
}
}
}
31 changes: 23 additions & 8 deletions api/clients/v2/node_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package clients
import (
"context"
"fmt"
"github.com/Layr-Labs/eigenda/api"
"sync"

commonpb "github.com/Layr-Labs/eigenda/api/grpc/common/v2"
Expand All @@ -24,21 +25,23 @@ type NodeClient interface {
}

type nodeClient struct {
config *NodeClientConfig
initOnce sync.Once
conn *grpc.ClientConn
config *NodeClientConfig
initOnce sync.Once
conn *grpc.ClientConn
requestSigner DispersalRequestSigner
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A slightly different structuring of it: shall we pass in an interface, not a concrete implementation?
Like we have RequestSigners interface (and passed around), but with a KMS based implementation. This leaves space for potential non-KMS (i.e. non AWS specific) options (in a decentralized scenario it may be needed).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused about this comment. DispersalRequestSigner is already an interface. Is the interface ok in its current form?

// DispersalRequestSigner encapsulates the logic for signing GetChunks requests.
type DispersalRequestSigner interface {
	// SignStoreChunksRequest signs a StoreChunksRequest. Does not modify the request
	// (i.e. it does not insert the signature).
	SignStoreChunksRequest(ctx context.Context, request *grpc.StoreChunksRequest) ([]byte, error)
}


dispersalClient nodegrpc.DispersalClient
}

var _ NodeClient = (*nodeClient)(nil)

func NewNodeClient(config *NodeClientConfig) (*nodeClient, error) {
func NewNodeClient(config *NodeClientConfig, requestSigner DispersalRequestSigner) (NodeClient, error) {
if config == nil || config.Hostname == "" || config.Port == "" {
return nil, fmt.Errorf("invalid config: %v", config)
}
return &nodeClient{
config: config,
config: config,
requestSigner: requestSigner,
}, nil
}

Expand All @@ -60,16 +63,28 @@ func (c *nodeClient) StoreChunks(ctx context.Context, batch *corev2.Batch) (*cor
}
}

// Call the gRPC method to store chunks
response, err := c.dispersalClient.StoreChunks(ctx, &nodegrpc.StoreChunksRequest{
request := &nodegrpc.StoreChunksRequest{
Batch: &commonpb.Batch{
Header: &commonpb.BatchHeader{
BatchRoot: batch.BatchHeader.BatchRoot[:],
ReferenceBlockNumber: batch.BatchHeader.ReferenceBlockNumber,
},
BlobCertificates: blobCerts,
},
})
DisperserID: api.EigenLabsDisperserID, // this will need to be updated when dispersers are decentralized
}

if c.requestSigner != nil {
// Sign the request to store chunks
signature, err := c.requestSigner.SignStoreChunksRequest(ctx, request)
if err != nil {
return nil, fmt.Errorf("failed to sign store chunks request: %v", err)
}
request.Signature = signature
}

// Call the gRPC method to store chunks
response, err := c.dispersalClient.StoreChunks(ctx, request)
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions api/clients/v2/relay_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"context"
"errors"
"fmt"
"github.com/Layr-Labs/eigenda/api/hashing"
"github.com/Layr-Labs/eigenda/core"
"github.com/Layr-Labs/eigenda/relay/auth"
"sync"

relaygrpc "github.com/Layr-Labs/eigenda/api/grpc/relay"
Expand Down Expand Up @@ -115,7 +115,7 @@ func (c *relayClient) signGetChunksRequest(ctx context.Context, request *relaygr
return errors.New("no message signer provided in config, cannot sign get chunks request")
}

hash := auth.HashGetChunksRequest(request)
hash := hashing.HashGetChunksRequest(request)
hashArray := [32]byte{}
copy(hashArray[:], hash)
signature, err := c.config.MessageSigner(ctx, hashArray)
Expand Down
4 changes: 4 additions & 0 deletions api/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package api

// EigenLabsDisperserID is the ID of the disperser that is managed by Eigen Labs.
const EigenLabsDisperserID = uint32(0)
42 changes: 41 additions & 1 deletion api/docs/eigenda-protos.html
Original file line number Diff line number Diff line change
Expand Up @@ -3488,7 +3488,7 @@ <h3 id="node.v2.StoreChunksReply">StoreChunksReply</h3>


<h3 id="node.v2.StoreChunksRequest">StoreChunksRequest</h3>
<p></p>
<p>Request that the Node store a batch of chunks.</p>


<table class="field-table">
Expand All @@ -3504,6 +3504,46 @@ <h3 id="node.v2.StoreChunksRequest">StoreChunksRequest</h3>
<td><p>batch of blobs to store </p></td>
</tr>

<tr>
<td>disperserID</td>
<td><a href="#uint32">uint32</a></td>
<td></td>
<td><p>ID of the disperser that is requesting the storage of the batch. </p></td>
</tr>

<tr>
<td>signature</td>
<td><a href="#bytes">bytes</a></td>
<td></td>
<td><p>Signature using the disperser&#39;s ECDSA key over keccak hash of the batch. The purpose of this signature
is to prevent hooligans from tricking DA nodes into storing data that they shouldn&#39;t be storing.

Algorithm for computing the hash is as follows. All integer values are serialized in big-endian order (unsigned).
A reference implementation (golang) can be found at
https://github.com/Layr-Labs/eigenda/blob/master/disperser/auth/request_signing.go

1. digest batch.BatchHeader.BatchRoot
2. digest batch.BatchHeader.ReferenceBlockNumber (8 bytes, unsigned big endian)
3. for each certificate in batch.BlobCertificates:
a. digest certificate.BlobHeader.Version (4 bytes, unsigned big endian)
b. for each quorum_number in certificate.BlobHeader.QuorumNumbers:
i. digest quorum_number (4 bytes, unsigned big endian)
c. digest certificate.BlobHeader.Commitment.Commitment
d. digest certificate.BlobHeader.Commitment.LengthCommitment
e. digest certificate.BlobHeader.Commitment.LengthProof
f. digest certificate.BlobHeader.Commitment.Length (4 bytes, unsigned big endian)
g. digest certificate.BlobHeader.PaymentHeader.AccountId
h. digest certificate.BlobHeader.PaymentHeader.ReservationPeriod (4 bytes, unsigned big endian)
i. digest certificate.BlobHeader.PaymentHeader.CumulativePayment
j. digest certificate.BlobHeader.PaymentHeader.Salt (4 bytes, unsigned big endian)
k. digest certificate.BlobHeader.Signature
l. for each relay in certificate.Relays:
i. digest relay (4 bytes, unsigned big endian)
4. digest disperserID (4 bytes, unsigned big endian)

Note that this signature is not included in the hash for obvious reasons. </p></td>
</tr>

</tbody>
</table>

Expand Down
Loading
Loading