Skip to content

Commit a6a2d61

Browse files
committed
Authorize signatures in event consumer
1 parent 17e7e51 commit a6a2d61

7 files changed

Lines changed: 313 additions & 8 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"os/user"
9+
"path/filepath"
10+
"regexp"
11+
"runtime"
12+
"slices"
13+
"time"
14+
15+
"filippo.io/age"
16+
"github.com/fystack/mpcium/pkg/common/pathutil"
17+
"github.com/fystack/mpcium/pkg/encryption"
18+
"github.com/fystack/mpcium/pkg/types"
19+
"github.com/urfave/cli/v3"
20+
)
21+
22+
// AuthorizerIdentity struct to store authorizer metadata
23+
type AuthorizerIdentity struct {
24+
Name string `json:"name"`
25+
Algorithm string `json:"algorithm,omitempty"`
26+
PublicKey string `json:"public_key"`
27+
CreatedAt string `json:"created_at"`
28+
CreatedBy string `json:"created_by"`
29+
MachineOS string `json:"machine_os"`
30+
MachineName string `json:"machine_name"`
31+
}
32+
33+
// validateAuthorizerName checks if the authorizer name is valid (no spaces, special characters)
34+
func validateAuthorizerName(name string) error {
35+
if name == "" {
36+
return fmt.Errorf("name cannot be empty")
37+
}
38+
39+
// Only allow alphanumeric characters, hyphens, and underscores
40+
validNamePattern := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
41+
if !validNamePattern.MatchString(name) {
42+
return fmt.Errorf("name can only contain alphanumeric characters, hyphens, and underscores (no spaces or special characters)")
43+
}
44+
45+
return nil
46+
}
47+
48+
func generateAuthorizerIdentity(ctx context.Context, c *cli.Command) error {
49+
name := c.String("name")
50+
outputDir := c.String("output-dir")
51+
encrypt := c.Bool("encrypt")
52+
overwrite := c.Bool("overwrite")
53+
algorithm := c.String("algorithm")
54+
55+
// Validate authorizer name
56+
if err := validateAuthorizerName(name); err != nil {
57+
return err
58+
}
59+
60+
if algorithm == "" {
61+
algorithm = string(types.EventInitiatorKeyTypeEd25519)
62+
}
63+
64+
if !slices.Contains(
65+
[]string{string(types.EventInitiatorKeyTypeEd25519), string(types.EventInitiatorKeyTypeP256)},
66+
algorithm,
67+
) {
68+
return fmt.Errorf("invalid algorithm: %s. Must be %s or %s",
69+
algorithm,
70+
types.EventInitiatorKeyTypeEd25519,
71+
types.EventInitiatorKeyTypeP256,
72+
)
73+
}
74+
75+
// Create output directory if it doesn't exist
76+
if err := os.MkdirAll(outputDir, 0750); err != nil {
77+
return fmt.Errorf("failed to create output directory: %w", err)
78+
}
79+
80+
// Check if files already exist before proceeding
81+
identityPath := filepath.Join(outputDir, fmt.Sprintf("%s.authorizer.identity.json", name))
82+
keyPath := filepath.Join(outputDir, fmt.Sprintf("%s.authorizer.key", name))
83+
encKeyPath := keyPath + ".age"
84+
85+
// Check for existing identity file
86+
if _, err := os.Stat(identityPath); err == nil && !overwrite {
87+
return fmt.Errorf(
88+
"identity file already exists: %s (use --overwrite to force)",
89+
identityPath,
90+
)
91+
}
92+
93+
// Check for existing key files
94+
if _, err := os.Stat(keyPath); err == nil && !overwrite {
95+
return fmt.Errorf("key file already exists: %s (use --overwrite to force)", keyPath)
96+
}
97+
98+
if encrypt {
99+
if _, err := os.Stat(encKeyPath); err == nil && !overwrite {
100+
return fmt.Errorf(
101+
"encrypted key file already exists: %s (use --overwrite to force)",
102+
encKeyPath,
103+
)
104+
}
105+
}
106+
107+
// Generate keys based on algorithm
108+
var keyData encryption.KeyData
109+
var err error
110+
111+
if algorithm == string(types.EventInitiatorKeyTypeEd25519) {
112+
keyData, err = encryption.GenerateEd25519Keys()
113+
} else if algorithm == string(types.EventInitiatorKeyTypeP256) {
114+
keyData, err = encryption.GenerateP256Keys()
115+
}
116+
117+
if err != nil {
118+
return fmt.Errorf("failed to generate %s keys: %w", algorithm, err)
119+
}
120+
121+
// Get current user
122+
currentUser, err := user.Current()
123+
if err != nil {
124+
return fmt.Errorf("failed to get current user: %w", err)
125+
}
126+
127+
// Get hostname
128+
hostname, err := os.Hostname()
129+
if err != nil {
130+
hostname = "unknown"
131+
}
132+
133+
// Create Identity object
134+
identity := AuthorizerIdentity{
135+
Name: name,
136+
Algorithm: algorithm,
137+
PublicKey: keyData.PublicKeyHex,
138+
CreatedAt: time.Now().UTC().Format(time.RFC3339),
139+
CreatedBy: currentUser.Username,
140+
MachineOS: runtime.GOOS,
141+
MachineName: hostname,
142+
}
143+
144+
// Save identity JSON
145+
identityBytes, err := json.MarshalIndent(identity, "", " ")
146+
if err != nil {
147+
return fmt.Errorf("failed to marshal identity JSON: %w", err)
148+
}
149+
150+
if err := os.WriteFile(identityPath, identityBytes, 0600); err != nil {
151+
return fmt.Errorf("failed to save identity file: %w", err)
152+
}
153+
154+
// Handle private key (with optional encryption)
155+
if encrypt {
156+
// Use requestPassword function instead of inline password handling
157+
passphrase, err := requestPassword()
158+
if err != nil {
159+
return err
160+
}
161+
162+
// Create encrypted key file
163+
encKeyPath := keyPath + ".age"
164+
165+
// Validate the encrypted key path for security
166+
if err := pathutil.ValidateFilePath(encKeyPath); err != nil {
167+
return fmt.Errorf("invalid encrypted key file path: %w", err)
168+
}
169+
170+
outFile, err := os.Create(encKeyPath)
171+
if err != nil {
172+
return fmt.Errorf("failed to create encrypted private key file: %w", err)
173+
}
174+
defer outFile.Close()
175+
176+
// Set up age encryption
177+
recipient, err := age.NewScryptRecipient(passphrase)
178+
if err != nil {
179+
return fmt.Errorf("failed to create scrypt recipient: %w", err)
180+
}
181+
182+
identityWriter, err := age.Encrypt(outFile, recipient)
183+
if err != nil {
184+
return fmt.Errorf("failed to create age encryption writer: %w", err)
185+
}
186+
187+
// Write the encrypted private key
188+
if _, err := identityWriter.Write([]byte(keyData.PrivateKeyHex)); err != nil {
189+
return fmt.Errorf("failed to write encrypted private key: %w", err)
190+
}
191+
192+
if err := identityWriter.Close(); err != nil {
193+
return fmt.Errorf("failed to finalize age encryption: %w", err)
194+
}
195+
196+
fmt.Println("✅ Successfully generated authorizer identity:")
197+
fmt.Println("- Encrypted Private Key:", encKeyPath)
198+
fmt.Println("- Identity JSON:", identityPath)
199+
return nil
200+
} else {
201+
fmt.Println("WARNING: You are generating the private key without encryption.")
202+
fmt.Println("This is less secure. Consider using --encrypt flag for better security.")
203+
204+
if err := os.WriteFile(keyPath, []byte(keyData.PrivateKeyHex), 0600); err != nil {
205+
return fmt.Errorf("failed to save private key: %w", err)
206+
}
207+
}
208+
209+
fmt.Println("✅ Successfully generated authorizer identity:")
210+
fmt.Println("- Private Key:", keyPath)
211+
fmt.Println("- Identity JSON:", identityPath)
212+
return nil
213+
}

cmd/mpcium-cli/main.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,43 @@ func main() {
139139
},
140140
Action: generateInitiatorIdentity,
141141
},
142+
{
143+
Name: "generate-authorizer",
144+
Usage: "Generate identity files for an authorizer node",
145+
Flags: []cli.Flag{
146+
&cli.StringFlag{
147+
Name: "name",
148+
Aliases: []string{"n"},
149+
Usage: "Name for the authorizer (alphanumeric, hyphens, underscores only)",
150+
Required: true,
151+
},
152+
&cli.StringFlag{
153+
Name: "output-dir",
154+
Aliases: []string{"o"},
155+
Value: ".",
156+
Usage: "Output directory for identity files",
157+
},
158+
&cli.BoolFlag{
159+
Name: "encrypt",
160+
Aliases: []string{"e"},
161+
Value: false,
162+
Usage: "Encrypt private key with Age (recommended for production)",
163+
},
164+
&cli.BoolFlag{
165+
Name: "overwrite",
166+
Aliases: []string{"f"},
167+
Value: false,
168+
Usage: "Overwrite identity files if they already exist",
169+
},
170+
&cli.StringFlag{
171+
Name: "algorithm",
172+
Aliases: []string{"a"},
173+
Value: "ed25519",
174+
Usage: "Algorithm to use for key generation (ed25519,p256)",
175+
},
176+
},
177+
Action: generateAuthorizerIdentity,
178+
},
142179
{
143180
Name: "recover",
144181
Usage: "Recover database from encrypted backup files",

pkg/client/client.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const (
2020

2121
type MPCClient interface {
2222
CreateWallet(walletID string) error
23+
CreateWalletWithAuthorizers(walletID string, authorizerSignatures []types.AuthorizerSignature) error
2324
OnWalletCreationResult(callback func(event event.KeygenResultEvent)) error
2425

2526
SignTransaction(msg *types.SignTxMessage) error
@@ -104,9 +105,15 @@ func NewMPCClient(opts Options) MPCClient {
104105

105106
// CreateWallet generates a GenerateKeyMessage, signs it, and publishes it.
106107
func (c *mpcClient) CreateWallet(walletID string) error {
108+
return c.CreateWalletWithAuthorizers(walletID, nil)
109+
}
110+
111+
// CreateWalletWithAuthorizers generates a GenerateKeyMessage with authorizer signatures, signs it, and publishes it.
112+
func (c *mpcClient) CreateWalletWithAuthorizers(walletID string, authorizerSignatures []types.AuthorizerSignature) error {
107113
// build the message
108114
msg := &types.GenerateKeyMessage{
109-
WalletID: walletID,
115+
WalletID: walletID,
116+
AuthorizerSignatures: authorizerSignatures,
110117
}
111118
// compute the canonical raw bytes
112119
raw, err := msg.Raw()

pkg/event/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ const (
5757
ErrorCodePreParamsGeneration ErrorCode = "ERROR_PRE_PARAMS_GENERATION"
5858
ErrorCodeTSSPartyCreation ErrorCode = "ERROR_TSS_PARTY_CREATION"
5959

60+
// Authorization errors
61+
ErrorCodeMissingAuthorizerSignature ErrorCode = "ERROR_MISSING_AUTHORIZER_SIGNATURE"
62+
ErrorCodeInvalidAuthorizerSignature ErrorCode = "ERROR_INVALID_AUTHORIZER_SIGNATURE"
63+
6064
// Data serialization errors
6165
ErrorCodeMarshalFailure ErrorCode = "ERROR_MARSHAL_FAILURE"
6266
ErrorCodeUnmarshalFailure ErrorCode = "ERROR_UNMARSHAL_FAILURE"
@@ -105,6 +109,10 @@ func GetErrorCodeFromError(err error) ErrorCode {
105109

106110
// Check for specific error patterns
107111
switch {
112+
case contains(errStr, "missing required authorizer signature"):
113+
return ErrorCodeMissingAuthorizerSignature
114+
case contains(errStr, "authorizer", "verification failed"):
115+
return ErrorCodeInvalidAuthorizerSignature
108116
case contains(errStr, "validation"):
109117
return ErrorCodeMsgValidation
110118
case contains(errStr, "timeout", "timed out"):

pkg/eventconsumer/event_consumer.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ func (ec *eventConsumer) handleKeyGenEvent(natMsg *nats.Msg) {
166166
return
167167
}
168168

169+
if err := ec.identityStore.AuthorizeInitiatorMessage(&msg); err != nil {
170+
logger.Error("Failed to authorize initiator message", err)
171+
ec.handleKeygenSessionError(msg.WalletID, err, "Failed to authorize initiator message", natMsg)
172+
return
173+
}
174+
169175
walletID := msg.WalletID
170176
ecdsaSession, err := ec.node.CreateKeyGenSession(mpc.SessionTypeECDSA, walletID, ec.mpcThreshold, ec.genKeyResultQueue)
171177
if err != nil {
@@ -353,6 +359,12 @@ func (ec *eventConsumer) handleSigningEvent(natMsg *nats.Msg) {
353359
return
354360
}
355361

362+
err = ec.identityStore.AuthorizeInitiatorMessage(&msg)
363+
if err != nil {
364+
logger.Error("Failed to authorize initiator message", err)
365+
return
366+
}
367+
356368
logger.Info(
357369
"Received signing event",
358370
"waleltID",
@@ -590,6 +602,12 @@ func (ec *eventConsumer) consumeReshareEvent() error {
590602
return
591603
}
592604

605+
if err := ec.identityStore.AuthorizeInitiatorMessage(&msg); err != nil {
606+
logger.Error("Failed to authorize initiator message", err)
607+
ec.handleReshareSessionError(msg.WalletID, msg.KeyType, msg.NewThreshold, err, "Failed to authorize initiator message", natMsg)
608+
return
609+
}
610+
593611
walletID := msg.WalletID
594612
keyType := msg.KeyType
595613

pkg/identity/identity.go

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,13 @@ func NewFileStore(identityDir, nodeName string, decrypt bool, agePasswordFile st
139139
}
140140

141141
store := &fileStore{
142-
identityDir: identityDir,
143-
currentNodeName: nodeName,
144-
publicKeys: make(map[string][]byte),
145-
privateKey: privateKey,
146-
initiatorKey: initiatorKey,
147-
symmetricKeys: make(map[string][]byte),
142+
identityDir: identityDir,
143+
currentNodeName: nodeName,
144+
publicKeys: make(map[string][]byte),
145+
privateKey: privateKey,
146+
initiatorKey: initiatorKey,
147+
symmetricKeys: make(map[string][]byte),
148+
cachedAuthorizerKeys: make(map[AuthorizerID]any),
148149
}
149150

150151
// Check that each node in peers.json has an identity file
@@ -579,9 +580,26 @@ func (s *fileStore) AuthorizeInitiatorMessage(msg types.InitiatorMessage) error
579580
if !s.authConfig.Enabled {
580581
return nil
581582
}
583+
582584
sigs := msg.GetAuthorizerSignatures()
585+
if len(s.authConfig.RequiredAuthorizers) > 0 {
586+
// Build a map of provided signatures for quick lookup
587+
providedSigs := make(map[AuthorizerID]types.AuthorizerSignature)
588+
for _, sig := range sigs {
589+
providedSigs[AuthorizerID(sig.AuthorizerID)] = sig
590+
}
591+
592+
// Verify that ALL required authorizers have provided signatures
593+
for _, requiredID := range s.authConfig.RequiredAuthorizers {
594+
if _, ok := providedSigs[requiredID]; !ok {
595+
return fmt.Errorf("missing required authorizer signature: %s", requiredID)
596+
}
597+
}
598+
}
599+
600+
// If no signatures provided but none required, that's okay
583601
if len(sigs) == 0 {
584-
return nil // skip as no signatures
602+
return nil
585603
}
586604

587605
authorizerRaw, err := types.ComposeAuthorizerRaw(msg)

0 commit comments

Comments
 (0)