diff --git a/pkg/cascadekit/verify.go b/pkg/cascadekit/verify.go index 74331dde..8fd90beb 100644 --- a/pkg/cascadekit/verify.go +++ b/pkg/cascadekit/verify.go @@ -1,27 +1,57 @@ package cascadekit import ( + "encoding/base64" + "fmt" + + actionkeeper "github.com/LumeraProtocol/lumera/x/action/v1/keeper" "github.com/LumeraProtocol/supernode/v2/pkg/codec" "github.com/LumeraProtocol/supernode/v2/pkg/errors" - "github.com/LumeraProtocol/supernode/v2/pkg/utils" ) -// VerifySingleBlockIDs enforces single-block layouts and verifies that the -// symbols and block hash of ticket and local layouts match for block 0. -func VerifySingleBlockIDs(ticket, local codec.Layout) error { - if len(ticket.Blocks) != 1 || len(local.Blocks) != 1 { - return errors.New("layout must contain exactly one block") +// Verifier is a function that verifies the signature over data using the signer's on-chain pubkey. +// It should return nil if signature is valid; otherwise an error. +type Verifier func(data []byte, signature []byte) error + +// VerifyStringRawOrADR36 verifies a signature over a message string in two passes: +// 1. raw: verify([]byte(message), sigRS) +// 2. ADR-36: build amino-JSON sign bytes with data = base64(message) and verify +// +// The signature is provided as base64 (DER or 64-byte r||s), and coerced to 64-byte r||s. +func VerifyStringRawOrADR36(message string, sigB64 string, signer string, verify Verifier) error { + sigRaw, err := base64.StdEncoding.DecodeString(sigB64) + if err != nil { + return fmt.Errorf("invalid base64 signature: %w", err) } - if err := utils.EqualStrList(ticket.Blocks[0].Symbols, local.Blocks[0].Symbols); err != nil { - return errors.Errorf("symbol identifiers don't match: %w", err) + sigRS, err := actionkeeper.CoerceToRS64(sigRaw) + if err != nil { + return fmt.Errorf("coerce signature: %w", err) } - if ticket.Blocks[0].Hash != local.Blocks[0].Hash { - return errors.New("block hashes don't match") + if err := verify([]byte(message), sigRS); err == nil { + return nil } - return nil + dataB64 := base64.StdEncoding.EncodeToString([]byte(message)) + doc, err := actionkeeper.MakeADR36AminoSignBytes(signer, dataB64) + if err != nil { + return fmt.Errorf("build adr36 doc: %w", err) + } + if err := verify(doc, sigRS); err == nil { + return nil + } + return fmt.Errorf("signature verification failed") +} + +// VerifyIndex verifies the creator's signature over indexB64 (string), using the given verifier. +func VerifyIndex(indexB64 string, sigB64 string, signer string, verify Verifier) error { + return VerifyStringRawOrADR36(indexB64, sigB64, signer, verify) +} + +// VerifyLayout verifies the layout signature over base64(JSON(layout)) bytes. +func VerifyLayout(layoutB64 []byte, sigB64 string, signer string, verify Verifier) error { + return VerifyStringRawOrADR36(string(layoutB64), sigB64, signer, verify) } -// VerifySingleBlock checks that a layout contains exactly one block. +// VerifySingleBlock ensures the RaptorQ layout contains exactly one block. func VerifySingleBlock(layout codec.Layout) error { if len(layout.Blocks) != 1 { return errors.New("layout must contain exactly one block") diff --git a/sdk/action/client.go b/sdk/action/client.go index d5fdf410..81aa806b 100644 --- a/sdk/action/client.go +++ b/sdk/action/client.go @@ -318,13 +318,15 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath // GenerateStartCascadeSignatureFromFile computes blake3(file) and signs it with the configured key. // Returns base64-encoded signature suitable for StartCascade. func (c *ClientImpl) GenerateStartCascadeSignatureFromFile(ctx context.Context, filePath string) (string, error) { + // Compute blake3(file), encode as base64 string, and sign the string bytes h, err := utils.Blake3HashFile(filePath) if err != nil { return "", fmt.Errorf("blake3: %w", err) } - sig, err := keyringpkg.SignBytes(c.keyring, c.config.Account.KeyName, h) + dataHashB64 := base64.StdEncoding.EncodeToString(h) + sig, err := keyringpkg.SignBytes(c.keyring, c.config.Account.KeyName, []byte(dataHashB64)) if err != nil { - return "", fmt.Errorf("sign hash: %w", err) + return "", fmt.Errorf("sign hash string: %w", err) } return base64.StdEncoding.EncodeToString(sig), nil } diff --git a/sdk/task/helpers.go b/sdk/task/helpers.go index 2e9ee4c3..aac0fed1 100644 --- a/sdk/task/helpers.go +++ b/sdk/task/helpers.go @@ -2,12 +2,12 @@ package task import ( "context" - "encoding/base64" "fmt" "math/big" "os" "sort" + "github.com/LumeraProtocol/supernode/v2/pkg/cascadekit" "github.com/LumeraProtocol/supernode/v2/pkg/utils" "github.com/LumeraProtocol/supernode/v2/sdk/adapters/lumera" ) @@ -74,26 +74,13 @@ func (m *ManagerImpl) validateSignature(ctx context.Context, action lumera.Actio return fmt.Errorf("failed to decode cascade metadata: %w", err) } - // Extract the base64-encoded data hash from the metadata - base64EnTcketDataHash := cascadeMetaData.DataHash + // Extract the base64-encoded data hash string from the metadata + dataHashB64 := cascadeMetaData.DataHash - // Decode the data hash from base64 to raw bytes - dataHashBytes, err := base64.StdEncoding.DecodeString(base64EnTcketDataHash) - if err != nil { - return fmt.Errorf("failed to decode data hash: %w", err) - } - - // Decode the provided signature from base64 to raw bytes - signatureBytes, err := base64.StdEncoding.DecodeString(signature) - if err != nil { - return fmt.Errorf("failed to decode signature: %w", err) - } - - // Verify the signature using the Lumera client - // This checks if the signature was produced by the action creator - // for the given data hash - err = m.lumeraClient.VerifySignature(ctx, action.Creator, dataHashBytes, signatureBytes) - if err != nil { + // Verify using cascadekit helper (raw -> ADR-36) + if err := cascadekit.VerifyStringRawOrADR36(dataHashB64, signature, action.Creator, func(data, sig []byte) error { + return m.lumeraClient.VerifySignature(ctx, action.Creator, data, sig) + }); err != nil { m.logger.Error(ctx, "Signature validation failed", "actionID", action.ID, "error", err) return fmt.Errorf("signature validation failed: %w", err) } diff --git a/supernode/cascade/download.go b/supernode/cascade/download.go index 944e7d84..7378f920 100644 --- a/supernode/cascade/download.go +++ b/supernode/cascade/download.go @@ -2,7 +2,6 @@ package cascade import ( "context" - "encoding/base64" "encoding/json" "fmt" "os" @@ -117,11 +116,9 @@ func (task *CascadeRegistrationTask) VerifyDownloadSignature(ctx context.Context return fmt.Errorf("get action for signature verification: %w", err) } creator := act.GetAction().Creator - sigBytes, err := base64.StdEncoding.DecodeString(signature) - if err != nil { - return fmt.Errorf("invalid base64 signature: %w", err) - } - if err := task.LumeraClient.Verify(ctx, creator, []byte(actionID), sigBytes); err != nil { + if err := cascadekit.VerifyStringRawOrADR36(actionID, signature, creator, func(data, sig []byte) error { + return task.LumeraClient.Verify(ctx, creator, data, sig) + }); err != nil { return err } return nil diff --git a/supernode/cascade/helper.go b/supernode/cascade/helper.go index 2d204c52..cbc5699b 100644 --- a/supernode/cascade/helper.go +++ b/supernode/cascade/helper.go @@ -2,7 +2,6 @@ package cascade import ( "context" - "encoding/base64" "strconv" "cosmossdk.io/math" @@ -87,11 +86,9 @@ func (task *CascadeRegistrationTask) validateIndexAndLayout(ctx context.Context, if err != nil { return cascadekit.IndexFile{}, nil, err } - creatorSig, err := base64.StdEncoding.DecodeString(creatorSigB64) - if err != nil { - return cascadekit.IndexFile{}, nil, err - } - if err := task.LumeraClient.Verify(ctx, creator, []byte(indexB64), creatorSig); err != nil { + if err := cascadekit.VerifyIndex(indexB64, creatorSigB64, creator, func(data, sig []byte) error { + return task.LumeraClient.Verify(ctx, creator, data, sig) + }); err != nil { return cascadekit.IndexFile{}, nil, err } // Decode index @@ -104,14 +101,13 @@ func (task *CascadeRegistrationTask) validateIndexAndLayout(ctx context.Context, if err != nil { return cascadekit.IndexFile{}, nil, err } - if err := cascadekit.VerifySingleBlock(layout); err != nil { - return cascadekit.IndexFile{}, nil, err - } - layoutSig, err := base64.StdEncoding.DecodeString(indexFile.LayoutSignature) - if err != nil { - return cascadekit.IndexFile{}, nil, err + // Enforce single-block layout for Cascade + if len(layout.Blocks) != 1 { + return cascadekit.IndexFile{}, nil, errors.New("layout must contain exactly one block") } - if err := task.LumeraClient.Verify(ctx, creator, layoutB64, layoutSig); err != nil { + if err := cascadekit.VerifyLayout(layoutB64, indexFile.LayoutSignature, creator, func(data, sig []byte) error { + return task.LumeraClient.Verify(ctx, creator, data, sig) + }); err != nil { return cascadekit.IndexFile{}, nil, err } return indexFile, layoutB64, nil