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
76 changes: 61 additions & 15 deletions cerebro/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,30 @@ func (o *Operator) createChannelSessionKey(ctx context.Context, sessionKeyAddr,
version = existingStates[0].Version + 1
}

// SessionKeySig requires the session-key private key. Fetch it up-front and bail if the
// stored key doesn't match the address being registered — without the private key, the
// nitronode rejects the submit.
storedPK, pkErr := o.store.GetSessionKeyPrivateKey()
if pkErr != nil {
fmt.Printf("ERROR: Cannot register session key without the matching private key: %v\n", pkErr)
return
}
storedRawSigner, sigErr := sign.NewEthereumRawSigner(storedPK)
if sigErr != nil {
fmt.Printf("ERROR: Failed to load stored session key: %v\n", sigErr)
return
}
if !strings.EqualFold(storedRawSigner.PublicKey().Address().String(), sessionKeyAddr) {
fmt.Printf("ERROR: Stored session key %s does not match the address being registered (%s)\n",
storedRawSigner.PublicKey().Address().String(), sessionKeyAddr)
return
}
sessionKeySigner, sigErr := sign.NewEthereumMsgSignerFromRaw(storedRawSigner)
if sigErr != nil {
fmt.Printf("ERROR: Failed to construct session-key message signer: %v\n", sigErr)
return
}

expiresAt := time.Now().Add(time.Duration(expiresHours) * time.Hour)

state := core.ChannelSessionKeyStateV1{
Expand All @@ -1037,6 +1061,13 @@ func (o *Operator) createChannelSessionKey(ctx context.Context, sessionKeyAddr,
}
state.UserSig = sig

keySig, err := sdk.SignChannelSessionKeyOwnership(state, sessionKeySigner)
if err != nil {
fmt.Printf("ERROR: Failed to sign session key ownership: %v\n", err)
return
}
state.SessionKeySig = keySig

fmt.Println("Submitting channel session key state...")
if err := o.client.SubmitChannelSessionKeyState(ctx, state); err != nil {
fmt.Printf("ERROR: Failed to submit session key state: %v\n", err)
Expand All @@ -1049,21 +1080,7 @@ func (o *Operator) createChannelSessionKey(ctx context.Context, sessionKeyAddr,
fmt.Printf(" Assets: %s\n", strings.Join(assets, ", "))
fmt.Printf(" Expires At: %s\n", expiresAt.Format("2006-01-02 15:04:05"))

// If we have a stored session key matching this address, activate it as the state signer
storedPK, pkErr := o.store.GetSessionKeyPrivateKey()
if pkErr != nil {
return
}
storedSigner, sigErr := sign.NewEthereumRawSigner(storedPK)
if sigErr != nil {
return
}
if !strings.EqualFold(storedSigner.PublicKey().Address().String(), sessionKeyAddr) {
return
}

// Compute metadata hash and store full session key data
metadataHash, err := core.GetChannelSessionKeyAuthMetadataHashV1(version, assets, expiresAt.Unix())
metadataHash, err := core.GetChannelSessionKeyAuthMetadataHashV1(wallet, version, assets, expiresAt.Unix())
if err != nil {
fmt.Printf("WARNING: Failed to compute metadata hash: %v\n", err)
return
Expand Down Expand Up @@ -1148,6 +1165,28 @@ func (o *Operator) createAppSessionKey(ctx context.Context, sessionKeyAddr, expi
}
}

// SessionKeySig requires the session-key private key.
storedPK, pkErr := o.store.GetSessionKeyPrivateKey()
if pkErr != nil {
fmt.Printf("ERROR: Cannot register session key without the matching private key: %v\n", pkErr)
return
}
storedRawSigner, sigErr := sign.NewEthereumRawSigner(storedPK)
if sigErr != nil {
fmt.Printf("ERROR: Failed to load stored session key: %v\n", sigErr)
return
}
if !strings.EqualFold(storedRawSigner.PublicKey().Address().String(), sessionKeyAddr) {
fmt.Printf("ERROR: Stored session key %s does not match the address being registered (%s)\n",
storedRawSigner.PublicKey().Address().String(), sessionKeyAddr)
return
}
sessionKeySigner, sigErr := sign.NewEthereumMsgSignerFromRaw(storedRawSigner)
if sigErr != nil {
fmt.Printf("ERROR: Failed to construct session-key message signer: %v\n", sigErr)
return
}

state := app.AppSessionKeyStateV1{
UserAddress: wallet,
SessionKey: sessionKeyAddr,
Expand All @@ -1165,6 +1204,13 @@ func (o *Operator) createAppSessionKey(ctx context.Context, sessionKeyAddr, expi
}
state.UserSig = sig

keySig, err := sdk.SignAppSessionKeyOwnership(state, sessionKeySigner)
if err != nil {
fmt.Printf("ERROR: Failed to sign session key ownership: %v\n", err)
return
}
state.SessionKeySig = keySig

fmt.Println("Submitting app session key state...")
if err := o.client.SubmitAppSessionKeyState(ctx, state); err != nil {
fmt.Printf("ERROR: Failed to submit session key state: %v\n", err)
Expand Down
6 changes: 6 additions & 0 deletions docs/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,9 @@ types:
- name: user_sig
type: string
description: User's signature over the session key metadata to authorize the registration/update of the session key
- name: session_key_sig
type: string
description: Session-key holder's signature over PackChannelKeyStateV1(session_key, metadata_hash) — the same packed bytes user_sig signs, where metadata_hash binds user_address — proving possession of the key being registered. Required on every submit to prevent registration of keys the submitter does not control.

- app_session_key_state:
description: Represents the state of an app session key
Expand Down Expand Up @@ -409,6 +412,9 @@ types:
- name: user_sig
type: string
description: User's signature over the session key metadata to authorize the registration/update of the session key
- name: session_key_sig
type: string
description: Session-key holder's signature over the same packed state (which already binds user_address) proving possession of the key being registered. Required on every submit to prevent registration of keys the submitter does not control.

- app:
description: Application definition
Expand Down
3 changes: 3 additions & 0 deletions docs/data_models.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ classDiagram
+numeric version
+timestamptz expires_at
+text user_sig
+text session_key_sig
+timestamptz created_at
+timestamptz updated_at
}
Expand All @@ -216,6 +217,7 @@ classDiagram
+char~66~ metadata_hash
+timestamptz expires_at
+text user_sig
+text session_key_sig
+timestamptz created_at
}

Expand All @@ -230,6 +232,7 @@ classDiagram
+smallint kind PK
+numeric version
+timestamptz updated_at
UNIQUE(session_key, kind)
}

%% ===== BLOCKCHAIN TABLES =====
Expand Down
49 changes: 18 additions & 31 deletions nitronode/api/app_session_v1/submit_session_key_state.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package app_session_v1

import (
"errors"
"strings"
"time"

"github.com/ethereum/go-ethereum/common/hexutil"

"github.com/layer-3/nitrolite/nitronode/store/database"
"github.com/layer-3/nitrolite/pkg/app"
"github.com/layer-3/nitrolite/pkg/core"
"github.com/layer-3/nitrolite/pkg/log"
"github.com/layer-3/nitrolite/pkg/rpc"
"github.com/layer-3/nitrolite/pkg/sign"
)

// SubmitSessionKeyState processes session key state submissions for registration and updates.
Expand Down Expand Up @@ -50,6 +48,11 @@ func (h *Handler) SubmitSessionKeyState(c *rpc.Context) {
return
}

if strings.EqualFold(coreState.UserAddress, coreState.SessionKey) {
c.Fail(rpc.Errorf("invalid_session_key_state: session_key must differ from user_address"), "")
return
}

if coreState.Version == 0 {
c.Fail(rpc.Errorf("invalid_session_key_state: version must be greater than 0"), "")
return
Expand All @@ -70,37 +73,14 @@ func (h *Handler) SubmitSessionKeyState(c *rpc.Context) {
c.Fail(rpc.Errorf("invalid_session_key_state: user_sig is required"), "")
return
}

// Pack the session key state for signature verification (ABI encoding)
packedState, err := app.PackAppSessionKeyStateV1(coreState)
if err != nil {
c.Fail(rpc.Errorf("invalid_session_key_state: failed to pack state: %v", err), "")
return
}

// Decode the user signature
sigBytes, err := hexutil.Decode(coreState.UserSig)
if err != nil {
c.Fail(rpc.Errorf("invalid_session_key_state: failed to decode user_sig: %v", err), "")
return
}

// Recover signer address from signature using ECDSA recovery
ethMsgRecoverer, err := sign.NewSigValidator(sign.TypeEthereumMsg)
if err != nil {
c.Fail(rpc.Errorf("internal_error: failed to create signature validator: %v", err), "")
return
}

recoveredAddress, err := ethMsgRecoverer.Recover(packedState, sigBytes)
if err != nil {
c.Fail(rpc.Errorf("invalid_session_key_state: failed to recover signer: %v", err), "")
if coreState.SessionKeySig == "" {
c.Fail(rpc.Errorf("invalid_session_key_state: session_key_sig is required"), "")
return
}

// Verify the recovered address matches user_address
if !strings.EqualFold(recoveredAddress, coreState.UserAddress) {
c.Fail(rpc.Errorf("invalid_session_key_state: signature does not match user_address"), "")
// Validate both signatures: wallet's UserSig and session-key holder's SessionKeySig.
if err := app.ValidateAppSessionKeyStateV1(coreState); err != nil {
c.Fail(rpc.Errorf("invalid_session_key_state: %v", err), "")
return
}

Expand All @@ -111,6 +91,13 @@ func (h *Handler) SubmitSessionKeyState(c *rpc.Context) {
// a proper "expected version" error rather than racing on the history UNIQUE constraint.
latestVersion, err := tx.LockSessionKeyState(coreState.UserAddress, coreState.SessionKey, database.SessionKeyKindAppSession)
if err != nil {
if errors.Is(err, database.ErrSessionKeyNotAllowed) {
logger.Warn("session key registration collision",
"userAddress", coreState.UserAddress,
"sessionKey", coreState.SessionKey,
"kind", database.SessionKeyKindAppSession)
return rpc.Errorf("invalid_session_key_state: session_key not allowed")
}
return rpc.Errorf("failed to lock session key state: %v", err)
}

Expand Down
Loading