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
14 changes: 0 additions & 14 deletions pkg/cascadekit/keyring_signatures.go

This file was deleted.

85 changes: 79 additions & 6 deletions pkg/cascadekit/signatures.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type Signer func(msg []byte) ([]byte, error)

// SignLayoutB64 validates single-block layout, marshals to JSON, base64-encodes it,
// and signs the base64 payload, returning both the layout base64 and signature base64.
//
// Message signed = layoutB64 string (same as JS layoutBytesB64 if layout JSON matches).
func SignLayoutB64(layout codec.Layout, signer Signer) (layoutB64 string, layoutSigB64 string, err error) {
if len(layout.Blocks) != 1 {
return "", "", errors.New("layout must contain exactly one block")
Expand All @@ -40,27 +42,40 @@ func SignLayoutB64(layout codec.Layout, signer Signer) (layoutB64 string, layout
}

// SignIndexB64 marshals the index to JSON, base64-encodes it, and signs the
// base64 payload, returning both the index base64 and creator-signature base64.
// JSON string (not the base64), returning both the index base64 and creator-signature base64.
//
// IMPORTANT:
// - Message signed = index JSON string (same as JS signArbitrary(indexFileString))
// - indexB64 is still base64(JSON(index)), used in metadata and RQID generation.
func SignIndexB64(idx IndexFile, signer Signer) (indexB64 string, creatorSigB64 string, err error) {
raw, err := json.Marshal(idx)
if err != nil {
return "", "", errors.Errorf("marshal index file: %w", err)
}
indexB64 = base64.StdEncoding.EncodeToString(raw)

sig, err := signer([]byte(indexB64))
indexJSON := string(raw)

// Sign the JSON string (JS-style)
sig, err := signer([]byte(indexJSON))
if err != nil {
return "", "", errors.Errorf("sign index: %w", err)
}
creatorSigB64 = base64.StdEncoding.EncodeToString(sig)

// Base64(JSON(index)) used as the first segment of indexSignatureFormat
indexB64 = base64.StdEncoding.EncodeToString(raw)
return indexB64, creatorSigB64, nil
}

// CreateSignatures produces the index signature format and index IDs:
//
// Base64(index_json).Base64(creator_signature)
// indexSignatureFormat = Base64(index_json) + "." + Base64(creator_signature)
//
// It validates the layout has exactly one block.
//
// The "signer" can be:
// - raw: directly sign msg bytes (legacy Go path)
// - ADR-36: wrap msg into an ADR-36 sign doc, then sign (JS-compatible path)
func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (indexSignatureFormat string, indexIDs []string, err error) {
layoutB64, layoutSigB64, err := SignLayoutB64(layout, signer)
if err != nil {
Expand All @@ -74,7 +89,7 @@ func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (index
return "", nil, err
}

// Build and sign the index file
// Build and sign the index file (JS-style: message = index JSON string)
idx := BuildIndex(layoutIDs, layoutSigB64)
indexB64, creatorSigB64, err := SignIndexB64(idx, signer)
if err != nil {
Expand All @@ -90,9 +105,27 @@ func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (index
return indexSignatureFormat, indexIDs, nil
}

// CreateSignaturesWithKeyring signs layout and index using a Cosmos keyring (legacy path).
// Message signed = raw bytes passed by SignLayoutB64 / SignIndexB64:
// - layout: layoutB64 string
// - index: index JSON string
//
// The verification pipeline already handles both raw and ADR-36, so this remains valid.
func CreateSignaturesWithKeyring(
layout codec.Layout,
kr sdkkeyring.Keyring,
keyName string,
ic, max uint32,
) (string, []string, error) {
signer := func(msg []byte) ([]byte, error) {
return keyringpkg.SignBytes(kr, keyName, msg)
}
return CreateSignatures(layout, signer, ic, max)
}

// adr36SignerForKeyring creates a signer that signs ADR-36 doc bytes
// for the given signer address. The "msg" we pass in is the *message*
// (layoutB64, indexJSON, etc.), and this helper wraps it into ADR-36.
// (layoutB64, index JSON, etc.), and this helper wraps it into ADR-36.
func adr36SignerForKeyring(
kr sdkkeyring.Keyring,
keyName string,
Expand All @@ -113,6 +146,13 @@ func adr36SignerForKeyring(
}
}

// CreateSignaturesWithKeyringADR36 creates signatures in the SAME way as the JS SDK:
//
// - layout: Keplr-like ADR-36 signature over layoutB64 string
// - index: Keplr-like ADR-36 signature over index JSON string
//
// The resulting indexSignatureFormat string will match what JS produces for the same
// layout, signer, ic, and max.
func CreateSignaturesWithKeyringADR36(
layout codec.Layout,
kr sdkkeyring.Keyring,
Expand All @@ -129,3 +169,36 @@ func CreateSignaturesWithKeyringADR36(

return CreateSignatures(layout, signer, ic, max)
}

// SignADR36String signs a message string using the ADR-36 scheme that Keplr uses.
// "message" must be the same string you'd pass to Keplr's signArbitrary, e.g.:
// - layoutB64
// - index JSON
// - dataHash (base64 blake3)
func SignADR36String(
kr sdkkeyring.Keyring,
keyName string,
signerAddr string,
message string,
) (string, error) {
// 1) message -> []byte
msgBytes := []byte(message)

// 2) base64(UTF-8(message))
dataB64 := base64.StdEncoding.EncodeToString(msgBytes)

// 3) Build ADR-36 sign bytes (Keplr-accurate)
docBytes, err := actionkeeper.MakeADR36AminoSignBytes(signerAddr, dataB64)
if err != nil {
return "", fmt.Errorf("build adr36 sign bytes: %w", err)
}

// 4) Sign with Cosmos keyring
sig, err := keyringpkg.SignBytes(kr, keyName, docBytes)
if err != nil {
return "", fmt.Errorf("sign adr36 doc: %w", err)
}

// 5) Wire format: base64(rsSignature)
return base64.StdEncoding.EncodeToString(sig), nil
}
29 changes: 27 additions & 2 deletions pkg/cascadekit/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
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)
// 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.
Expand All @@ -27,9 +27,11 @@ func VerifyStringRawOrADR36(message string, sigB64 string, signer string, verify
if err != nil {
return fmt.Errorf("coerce signature: %w", err)
}
// 1) raw
if err := verify([]byte(message), sigRS); err == nil {
return nil
}
// 2) ADR-36
dataB64 := base64.StdEncoding.EncodeToString([]byte(message))
doc, err := actionkeeper.MakeADR36AminoSignBytes(signer, dataB64)
if err != nil {
Expand All @@ -42,6 +44,9 @@ func VerifyStringRawOrADR36(message string, sigB64 string, signer string, verify
}

// VerifyIndex verifies the creator's signature over indexB64 (string), using the given verifier.
// It supports both:
// - legacy: message = indexB64
// - new (JS-style): message = index JSON string (decoded from indexB64)
func VerifyIndex(indexB64 string, sigB64 string, signer string, verify Verifier) error {
// 1) Legacy: message = indexB64
if err := VerifyStringRawOrADR36(indexB64, sigB64, signer, verify); err == nil {
Expand All @@ -63,8 +68,28 @@ func VerifyIndex(indexB64 string, sigB64 string, signer string, verify Verifier)
}

// VerifyLayout verifies the layout signature over base64(JSON(layout)) bytes.
//
// It supports both:
// - legacy: message = base64(JSON(layout))
// - new: message = JSON(layout) (decoded from base64)
func VerifyLayout(layoutB64 []byte, sigB64 string, signer string, verify Verifier) error {
return VerifyStringRawOrADR36(string(layoutB64), sigB64, signer, verify)
msg := string(layoutB64)

// 1) Legacy: message = base64(layoutBytes)
if err := VerifyStringRawOrADR36(msg, sigB64, signer, verify); err == nil {
return nil
}

// 2) New-style: message = layout JSON (decoded from base64)
raw, err := base64.StdEncoding.DecodeString(msg)
if err == nil {
layoutJSON := string(raw)
if err2 := VerifyStringRawOrADR36(layoutJSON, sigB64, signer, verify); err2 == nil {
return nil
}
}

return fmt.Errorf("layout signature verification failed for both b64 and JSON schemes")
}

// VerifySingleBlock ensures the RaptorQ layout contains exactly one block.
Expand Down
42 changes: 31 additions & 11 deletions sdk/action/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package action

import (
"context"
crand "crypto/rand"
"encoding/base64"
"fmt"
"math/big"
"os"
"path/filepath"
"strconv"
Expand Down Expand Up @@ -284,19 +282,17 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath
max = 50
}
// Pick a random initial counter in [1,100]
rnd, _ := crand.Int(crand.Reader, big.NewInt(100))
ic := uint32(rnd.Int64() + 1) // 1..100
// Create signatures from the layout struct
// get bech32 address for this key
//rnd, _ := crand.Int(crand.Reader, big.NewInt(100))
ic := uint32(6)
Comment on lines +285 to +286
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

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

The initial counter ic is now hardcoded to 6 instead of being randomly selected from [1, 100]. This change reduces the randomness of generated RQIDs and could lead to predictable signature patterns. While the commented-out line suggests this might be temporary for testing/debugging purposes, it should either be removed or the random generation should be restored for production use.

If this is intentional for JS compatibility, please add a comment explaining why this specific value is required.

Suggested change
//rnd, _ := crand.Int(crand.Reader, big.NewInt(100))
ic := uint32(6)
rnd, err := crand.Int(crand.Reader, big.NewInt(100))
if err != nil {
return actiontypes.CascadeMetadata{}, "", "", fmt.Errorf("failed to generate random initial counter: %w", err)
}
ic := uint32(rnd.Int64() + 1)

Copilot uses AI. Check for mistakes.

indexSignatureFormat, _, err := cascadekit.CreateSignaturesWithKeyring(
// Create signatures from the layout struct using ADR-36 scheme (JS compatible).
indexSignatureFormat, _, err := cascadekit.CreateSignaturesWithKeyringADR36(
layout,
c.keyring,
c.config.Account.KeyName,
ic,
max,
)

if err != nil {
return actiontypes.CascadeMetadata{}, "", "", fmt.Errorf("create signatures: %w", err)
}
Expand All @@ -319,7 +315,6 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath
exp := paramsResp.Params.ExpirationDuration

// Compute data size in KB for fee, rounding up to avoid underpaying
// Keep consistent with supernode verification which uses ceil(bytes/1024)
sizeBytes := fi.Size()
kb := (sizeBytes + 1023) / 1024 // int64 division
feeResp, err := c.lumeraClient.GetActionFee(ctx, strconv.FormatInt(kb, 10))
Expand All @@ -335,9 +330,9 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath
return meta, price, expirationTime, nil
}

// GenerateStartCascadeSignatureFromFile computes blake3(file) and signs it with the configured key.
// GenerateStartCascadeSignatureFromFileDeprecated 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) {
func (c *ClientImpl) GenerateStartCascadeSignatureFromFileDeprecated(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 {
Expand All @@ -351,6 +346,31 @@ func (c *ClientImpl) GenerateStartCascadeSignatureFromFile(ctx context.Context,
return base64.StdEncoding.EncodeToString(sig), nil
}

// GenerateStartCascadeSignatureFromFile computes blake3(file) and signs it with the configured key
// using the ADR-36 scheme, matching Keplr's signArbitrary(dataHash) behavior.
// 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
h, err := utils.Blake3HashFile(filePath)
if err != nil {
return "", fmt.Errorf("blake3: %w", err)
}
dataHashB64 := base64.StdEncoding.EncodeToString(h)

// Sign the dataHashB64 string using ADR-36 (same as JS / Keplr).
sigB64, err := cascadekit.SignADR36String(
c.keyring,
c.config.Account.KeyName,
c.signerAddr, // bech32 address resolved in NewClient
dataHashB64,
)
if err != nil {
return "", fmt.Errorf("sign adr36 hash string: %w", err)
}

return sigB64, nil
}

// GenerateDownloadSignature signs the payload "actionID" and returns base64 signature.
func (c *ClientImpl) GenerateDownloadSignature(ctx context.Context, actionID, creatorAddr string) (string, error) {
if actionID == "" {
Expand Down
Loading