From c5721da745db3b2b8785239381db0cce9143997d Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 02:28:22 -0400 Subject: [PATCH 01/10] allow homedir to be specified by FREON_DIR env var --- client/internal/persistence.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/internal/persistence.go b/client/internal/persistence.go index b6d1c97..fc40a99 100644 --- a/client/internal/persistence.go +++ b/client/internal/persistence.go @@ -7,7 +7,14 @@ import ( ) func getConfigFile() (string, error) { - homeDir, err := os.UserHomeDir() + homeDir := os.Getenv("FREON_HOME") + var err error + if homeDir == "" { + homeDir, err = os.UserHomeDir() + if err != nil { + return "", err + } + } if err != nil { return "", err } From b45c03ef812f9e468adee5b198c83d3e28321a0f Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 02:56:17 -0400 Subject: [PATCH 02/10] add `freon sign get` --- client/internal/duct.go | 26 +++++++++++++++++++++++++ client/internal/freon.go | 14 ++++++++++++++ client/internal/types.go | 8 ++++++++ client/main.go | 35 ++++++++++++++++++++++++++++++++++ client/usage.go | 20 +++++++++++++++++++ coordinator/go.mod | 2 ++ coordinator/go.sum | 8 ++++++-- coordinator/internal/config.go | 3 +++ coordinator/internal/sign.go | 12 +++++++++++- coordinator/requesttypes.go | 8 ++++++++ coordinator/server.go | 21 ++++++++++++++++++++ go.work | 6 +++--- go.work.sum | 12 ++++++++++++ 13 files changed, 169 insertions(+), 6 deletions(-) diff --git a/client/internal/duct.go b/client/internal/duct.go index ee2c8d9..88c44e1 100644 --- a/client/internal/duct.go +++ b/client/internal/duct.go @@ -62,6 +62,8 @@ func GetApiEndpoint(host string, feature string) (string, error) { u.Path = "/sign/send" case "FinalizeSignMessage": u.Path = "/sign/finalize" + case "GetSignature": + u.Path = "/sign/get" case "TerminateSignCeremony": u.Path = "/sign/terminate" default: @@ -324,3 +326,27 @@ func DuctSignFinalize(host string, req SignFinalRequest) error { _, err = httpClient.Post(uri, "application/json", bytes.NewReader(body)) return err } + +func DuctGetSignature(host string, req GetSignRequest) (GetSignResponse, error) { + err := InitializeHttpClient() + if err != nil { + return GetSignResponse{}, err + } + uri, err := GetApiEndpoint(host, "GetSignature") + if err != nil { + return GetSignResponse{}, err + } + body, _ := json.Marshal(req) + resp, err := httpClient.Post(uri, "application/json", bytes.NewReader(body)) + if err != nil { + return GetSignResponse{}, err + } + defer resp.Body.Close() + + var response GetSignResponse + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return GetSignResponse{}, err + } + return response, nil +} diff --git a/client/internal/freon.go b/client/internal/freon.go index 40546be..ddb3012 100644 --- a/client/internal/freon.go +++ b/client/internal/freon.go @@ -536,6 +536,20 @@ func ListSign(host, groupID string, limit, offset int64) { os.Exit(0) } +// Fetch a signature from the coordinator for a given ceremony +func GetSignSignature(ceremonyID, host string) { + req := GetSignRequest{ + CeremonyID: ceremonyID, + } + res, err := DuctGetSignature(host, req) + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err.Error()) + os.Exit(1) + } + fmt.Printf("Signature:\n%s\n", res.Signature) + os.Exit(0) +} + func TerminateSignCeremony(ceremonyID string) { // TODO - soatok } diff --git a/client/internal/types.go b/client/internal/types.go index e68de6e..0ad2242 100644 --- a/client/internal/types.go +++ b/client/internal/types.go @@ -120,6 +120,14 @@ type SignFinalRequest struct { Signature string `json:"signature"` } +type GetSignRequest struct { + CeremonyID string `json:"ceremony"` +} + +type GetSignResponse struct { + Signature string `json:"signature"` +} + type VapidResponse struct { Status string `json:"status"` } diff --git a/client/main.go b/client/main.go index a2151a5..3582262 100644 --- a/client/main.go +++ b/client/main.go @@ -66,6 +66,8 @@ func main() { FreonSignList(subArgs[1:]) case "join": FreonSignJoin(subArgs[1:]) + case "get": + FreonSignGet(subArgs[1:]) default: fmt.Fprintf(os.Stderr, "Error: unknown sign subcommand: %s\n\n", subcommand) fmt.Fprintf(os.Stderr, "%s\n", signUsage) @@ -363,6 +365,39 @@ func FreonSignList(args []string) { internal.ListSign(*host, *groupID, *limit, *offset) } +// CMD: `freon sign get ...` +func FreonSignGet(args []string) { + // Parse CLI arguments: + fs := flag.NewFlagSet("sign get", flag.ExitOnError) + fs.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", signGetUsage) } + ceremonyID := fs.String("c", "", "Ceremony ID") + ceremonyIDLong := fs.String("ceremony", "", "Ceremony ID") + host := fs.String("h", "", "Coordinator hostname:port") + hostLong := fs.String("host", "", "Coordinator hostname:port") + fs.Parse(args) + + // Merge short/long flags + if *ceremonyIDLong != "" { + *ceremonyID = *ceremonyIDLong + } + if *hostLong != "" { + *host = *hostLong + } + if *host == "" { + fmt.Fprintf(os.Stderr, "Error: -h/--host is required\n") + fs.Usage() + os.Exit(1) + } + if *ceremonyID == "" { + fmt.Fprintf(os.Stderr, "Error: -c/--ceremony is required\n") + fs.Usage() + os.Exit(1) + } + + // The actual logic is implemented here: + internal.GetSignSignature(*ceremonyID, *host) +} + // CMD: `freon sign terminate ...` func FreonTerminate(args []string) { // Parse CLI arguments: diff --git a/client/usage.go b/client/usage.go index 093c1f3..4c839e8 100644 --- a/client/usage.go +++ b/client/usage.go @@ -178,6 +178,26 @@ EXAMPLES: ` +const signGetUsage = `FREON SIGN GET - Get signature from coordinator + +USAGE: + freon sign get [OPTIONS] -c + +DESCRIPTION: + Query the coordinator for the final signature for a concluded + ceremony. + +OPTIONS: + -c, --ceremony Ceremony ID from sign create + -h, --host Coordinator hostname:port + --help Print help information + +EXAMPLES: + freon sign get -c cer_def456 + freon sign get -h coord.example.com:8080 -c cer_def456 message.txt + +` + const terminateUsage = `FREON TERMINATE - Terminate ceremonies USAGE: diff --git a/coordinator/go.mod b/coordinator/go.mod index d31e9bf..cd31c3c 100644 --- a/coordinator/go.mod +++ b/coordinator/go.mod @@ -7,6 +7,7 @@ require github.com/taurusgroup/frost-ed25519 v0.0.0-20210707140332-5abc84a4dba7 require github.com/alexedwards/scs/v2 v2.9.0 require ( + filippo.io/age v1.2.1 github.com/ncruces/go-sqlite3 v0.28.0 github.com/stretchr/testify v1.10.0 ) @@ -17,6 +18,7 @@ require ( github.com/ncruces/julianday v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect + golang.org/x/crypto v0.41.0 // indirect golang.org/x/sys v0.35.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/coordinator/go.sum b/coordinator/go.sum index 74c28ac..1b24e3c 100644 --- a/coordinator/go.sum +++ b/coordinator/go.sum @@ -1,3 +1,7 @@ +c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= +c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= +filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= +filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90= @@ -5,8 +9,6 @@ github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gv github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/ncruces/go-sqlite3 v0.28.0 h1:AQVTUPgfamONl09LS+4rGFbHmLKM8/QrJJJi1UukjEQ= github.com/ncruces/go-sqlite3 v0.28.0/go.mod h1:WqvLhYwtEiZzg1H8BIeahUv/DxbmR+3xG5jDHDiBAGk= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= @@ -26,6 +28,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= diff --git a/coordinator/internal/config.go b/coordinator/internal/config.go index 5feb15f..0291b17 100644 --- a/coordinator/internal/config.go +++ b/coordinator/internal/config.go @@ -13,6 +13,9 @@ type CoordinatorConfig struct { } func getConfigFile() (string, error) { + if path := os.Getenv("FREON_COORDINATOR_CONFIG"); path != "" { + return path, nil + } homeDir, err := os.UserHomeDir() if err != nil { return "", err diff --git a/coordinator/internal/sign.go b/coordinator/internal/sign.go index 13c0205..2565f1c 100644 --- a/coordinator/internal/sign.go +++ b/coordinator/internal/sign.go @@ -71,7 +71,6 @@ func JoinSignCeremony(db *sql.DB, ceremonyID, hash string, myPartyID uint16) (in func PollSignCeremony(db *sql.DB, ceremonyID string, myPartyID uint16) (PollSignResponse, error) { ceremonyData, err := GetCeremonyData(db, ceremonyID) if err != nil { - panic(err) return PollSignResponse{}, err } @@ -141,3 +140,14 @@ func SetSignature(db *sql.DB, ceremonyUid, sig string) error { return FinalizeSignature(db, ceremony, sig) } + +func GetSignature(db *sql.DB, ceremonyUid string) (string, error) { + ceremony, err := GetCeremonyData(db, ceremonyUid) + if err != nil { + return "", err + } + if ceremony.Signature != nil { + return *ceremony.Signature, nil + } + return "", errors.New("signature not found") +} diff --git a/coordinator/requesttypes.go b/coordinator/requesttypes.go index eaf5ef0..6074763 100644 --- a/coordinator/requesttypes.go +++ b/coordinator/requesttypes.go @@ -106,6 +106,14 @@ type SignFinalRequest struct { Signature string `json:"signature"` } +type GetSignRequest struct { + CeremonyID string `json:"ceremony"` +} + +type GetSignResponse struct { + Signature string `json:"signature"` +} + type VapidResponse struct { Status string `json:"status"` } diff --git a/coordinator/server.go b/coordinator/server.go index 28ecc76..c767320 100644 --- a/coordinator/server.go +++ b/coordinator/server.go @@ -64,6 +64,7 @@ func main() { http.HandleFunc("/sign/poll", pollSign) http.HandleFunc("/sign/send", sendSign) http.HandleFunc("/sign/finalize", finalizeSign) + http.HandleFunc("/sign/get", getSign) http.HandleFunc("/terminate", terminateSign) http.ListenAndServe(serverConfig.Hostname, sessionManager.LoadAndSave(mux)) @@ -413,6 +414,26 @@ func listSign(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } +func getSign(w http.ResponseWriter, r *http.Request) { + var req GetSignRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + sendError(w, err) + return + } + signature, err := internal.GetSignature(db, req.CeremonyID) + if err != nil { + sendError(w, err) + return + } + + response := GetSignResponse{ + Signature: signature, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + func terminateSign(w http.ResponseWriter, r *http.Request) { // TODO - soatok } diff --git a/go.work b/go.work index f48171d..a03e97b 100644 --- a/go.work +++ b/go.work @@ -1,6 +1,6 @@ -go 1.25 +go 1.25.0 use ( - ./client - ./coordinator + ./client + ./coordinator ) diff --git a/go.work.sum b/go.work.sum index b99a4c3..2c02a4d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,11 +1,23 @@ +filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/ncruces/aa v0.3.0/go.mod h1:ctOw1LVqfuqzqg2S9LlR045bLAiXtaTiPMCL3zzl7Ik= github.com/ncruces/sort v0.1.5/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk= github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/soatok/freon/coordinator v0.0.0-20250822044951-bf74d56db437/go.mod h1:g4YrSLYKtfoiWHNuUn/MUobeff/xLq/Y4xNJXtVVfiI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw= From adb1d4d83dad3fd79376e0cfdd70a275f3f836eb Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 03:08:32 -0400 Subject: [PATCH 03/10] add `freon terminate` --- client/internal/duct.go | 32 +++++++++++++++++++++++++++++++- client/internal/freon.go | 13 +++++++++++-- client/internal/types.go | 4 ++++ client/main.go | 23 ++++++++++++++++++++--- coordinator/internal/database.go | 21 +++++++++++++++++++++ coordinator/internal/sign.go | 9 +++++++++ coordinator/requesttypes.go | 4 ++++ coordinator/server.go | 19 ++++++++++++++++++- 8 files changed, 118 insertions(+), 7 deletions(-) diff --git a/client/internal/duct.go b/client/internal/duct.go index 88c44e1..c7c1e08 100644 --- a/client/internal/duct.go +++ b/client/internal/duct.go @@ -65,7 +65,7 @@ func GetApiEndpoint(host string, feature string) (string, error) { case "GetSignature": u.Path = "/sign/get" case "TerminateSignCeremony": - u.Path = "/sign/terminate" + u.Path = "/terminate" default: return "", fmt.Errorf("unknown feature: %s", feature) } @@ -350,3 +350,33 @@ func DuctGetSignature(host string, req GetSignRequest) (GetSignResponse, error) } return response, nil } + +func DuctTerminateSignCeremony(host string, req TerminateRequest) error { + err := InitializeHttpClient() + if err != nil { + return err + } + uri, err := GetApiEndpoint(host, "TerminateSignCeremony") + if err != nil { + return err + } + body, err := json.Marshal(req) + if err != nil { + return err + } + resp, err := httpClient.Post(uri, "application/json", bytes.NewReader(body)) + if err != nil { + return err + } + defer resp.Body.Close() + + var response VapidResponse + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return err + } + if response.Status != "OK" { + return fmt.Errorf("termination failed: %s", response.Status) + } + return nil +} diff --git a/client/internal/freon.go b/client/internal/freon.go index ddb3012..475fb76 100644 --- a/client/internal/freon.go +++ b/client/internal/freon.go @@ -550,6 +550,15 @@ func GetSignSignature(ceremonyID, host string) { os.Exit(0) } -func TerminateSignCeremony(ceremonyID string) { - // TODO - soatok +func TerminateSignCeremony(host, ceremonyID string) { + req := TerminateRequest{ + CeremonyID: ceremonyID, + } + err := DuctTerminateSignCeremony(host, req) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) + os.Exit(1) + } + fmt.Println("Ceremony terminated.") + os.Exit(0) } diff --git a/client/internal/types.go b/client/internal/types.go index 0ad2242..0ede987 100644 --- a/client/internal/types.go +++ b/client/internal/types.go @@ -128,6 +128,10 @@ type GetSignResponse struct { Signature string `json:"signature"` } +type TerminateRequest struct { + CeremonyID string `json:"ceremony-id"` +} + type VapidResponse struct { Status string `json:"status"` } diff --git a/client/main.go b/client/main.go index 3582262..43c1d38 100644 --- a/client/main.go +++ b/client/main.go @@ -398,20 +398,37 @@ func FreonSignGet(args []string) { internal.GetSignSignature(*ceremonyID, *host) } -// CMD: `freon sign terminate ...` +// CMD: `freon terminate ...` func FreonTerminate(args []string) { // Parse CLI arguments: - fs := flag.NewFlagSet("sign join", flag.ExitOnError) + fs := flag.NewFlagSet("terminate", flag.ExitOnError) fs.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", terminateUsage) } ceremonyID := fs.String("c", "", "Ceremony ID") ceremonyIDLong := fs.String("ceremony", "", "Ceremony ID") + host := fs.String("h", "", "Coordinator hostname:port") + hostLong := fs.String("host", "", "Coordinator hostname:port") fs.Parse(args) // Merge short/long flags if *ceremonyIDLong != "" { *ceremonyID = *ceremonyIDLong } + if *hostLong != "" { + *host = *hostLong + } + + // Input validation + if *host == "" { + fmt.Fprintf(os.Stderr, "Error: -h/--host is required\n") + fs.Usage() + os.Exit(1) + } + if *ceremonyID == "" { + fmt.Fprintf(os.Stderr, "Error: -c/--ceremony is required\n") + fs.Usage() + os.Exit(1) + } // The actual logic is implemented here: - internal.TerminateSignCeremony(*ceremonyID) + internal.TerminateSignCeremony(*host, *ceremonyID) } diff --git a/coordinator/internal/database.go b/coordinator/internal/database.go index 03308ed..6092dc1 100644 --- a/coordinator/internal/database.go +++ b/coordinator/internal/database.go @@ -522,3 +522,24 @@ func FinalizeSignature(db *sql.DB, c FreonCeremonies, sig string) error { _, err = stmt.Exec(sig, c.DbId) return err } + +func TerminateCeremony(db *sql.DB, ceremonyUid string) error { + stmt, err := db.Prepare(`UPDATE ceremonies SET active = FALSE WHERE uid = ?`) + if err != nil { + return err + } + defer stmt.Close() + + res, err := stmt.Exec(ceremonyUid) + if err != nil { + return err + } + count, err := res.RowsAffected() + if err != nil { + return err + } + if count < 1 { + return errors.New("no ceremony found with that UID") + } + return nil +} diff --git a/coordinator/internal/sign.go b/coordinator/internal/sign.go index 2565f1c..dd9d165 100644 --- a/coordinator/internal/sign.go +++ b/coordinator/internal/sign.go @@ -36,6 +36,9 @@ func JoinSignCeremony(db *sql.DB, ceremonyID, hash string, myPartyID uint16) (in if err != nil { return 0, err } + if !ceremonyData.Active { + return 0, errors.New("ceremony is not active or does not exist") + } stmt, err := db.Prepare(` SELECT par.id @@ -104,6 +107,9 @@ func AddSignMessage(db *sql.DB, ceremonyUid string, myPartyID uint16, message [] if err != nil { return FreonSignMessage{}, err } + if !ceremony.Active { + return FreonSignMessage{}, errors.New("ceremony is not active or does not exist") + } group, err := GetGroupByID(db, ceremony.GroupID) if err != nil { @@ -134,6 +140,9 @@ func SetSignature(db *sql.DB, ceremonyUid, sig string) error { return err } + if !ceremony.Active { + return errors.New("ceremony is not active or does not exist") + } if ceremony.Signature != nil { return errors.New("signature is already defined") } diff --git a/coordinator/requesttypes.go b/coordinator/requesttypes.go index 6074763..5c90193 100644 --- a/coordinator/requesttypes.go +++ b/coordinator/requesttypes.go @@ -114,6 +114,10 @@ type GetSignResponse struct { Signature string `json:"signature"` } +type TerminateRequest struct { + CeremonyID string `json:"ceremony-id"` +} + type VapidResponse struct { Status string `json:"status"` } diff --git a/coordinator/server.go b/coordinator/server.go index c767320..6c5a998 100644 --- a/coordinator/server.go +++ b/coordinator/server.go @@ -435,5 +435,22 @@ func getSign(w http.ResponseWriter, r *http.Request) { } func terminateSign(w http.ResponseWriter, r *http.Request) { - // TODO - soatok + var req TerminateRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + sendError(w, err) + return + } + + err = internal.TerminateCeremony(db, req.CeremonyID) + if err != nil { + sendError(w, err) + return + } + + response := VapidResponse{ + Status: "OK", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) } From d71ce2809ba8821c446a9b9f85a58d93f6087128 Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 03:49:03 -0400 Subject: [PATCH 04/10] fix nits --- .github/workflows/ci.yml | 4 +- .gitignore | 2 + bin/.gitkeep | 1 + client/internal/duct.go | 118 +++++++++++++++++++++++++++++++++++++-- client/internal/types.go | 4 ++ client/main.go | 1 + 6 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 bin/.gitkeep diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5934a3a..9d5bf54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,10 +19,10 @@ jobs: go-version: '1.25' - name: Build Client - run: go build -o ./freon -v ./client + run: go build -o ./bin/client -v ./client - name: Build Coordinator - run: go build -o ./freon-server -v ./coordinator + run: go build -o ./bin/coordinator -v ./coordinator - name: Test Client run: cd client && go test -v -short && cd .. diff --git a/.gitignore b/.gitignore index a09c56d..74e6bf5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /.idea +/bin +!/bin/.gitkeep diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..8401cb3 --- /dev/null +++ b/bin/.gitkeep @@ -0,0 +1 @@ +hewwo uwu diff --git a/client/internal/duct.go b/client/internal/duct.go index c7c1e08..6d1bb70 100644 --- a/client/internal/duct.go +++ b/client/internal/duct.go @@ -15,6 +15,7 @@ import ( "net/http" "net/http/cookiejar" "net/url" + "strings" ) var httpClient *http.Client = nil @@ -34,6 +35,9 @@ func InitializeHttpClient() error { // If we change the backend API, we will change this function to accomodate it func GetApiEndpoint(host string, feature string) (string, error) { + if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") { + host = "http://" + host + } u, err := url.Parse(host) if err != nil { return "", err @@ -90,6 +94,14 @@ func DuctInitKeyGenCeremony(host string, req InitKeyGenRequest) (InitKeyGenRespo } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return InitKeyGenResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return InitKeyGenResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } + var response InitKeyGenResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { @@ -115,6 +127,14 @@ func DuctJoinKeyGenCeremony(host string, req JoinKeyGenRequest) (JoinKeyGenRespo } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return JoinKeyGenResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return JoinKeyGenResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } + var response JoinKeyGenResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { @@ -140,6 +160,14 @@ func DuctPollKeyGenCeremony(host string, req PollKeyGenRequest) (PollKeyGenRespo } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return PollKeyGenResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return PollKeyGenResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } + var response PollKeyGenResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { @@ -165,6 +193,13 @@ func DuctInitSignCeremony(host string, req InitSignRequest) (InitSignResponse, e } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return InitSignResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return InitSignResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } var response InitSignResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { @@ -190,6 +225,13 @@ func DuctJoinSignCeremony(host string, req JoinSignRequest) (JoinSignResponse, e } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return JoinSignResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return JoinSignResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } var response JoinSignResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { @@ -214,6 +256,13 @@ func DuctPollSignCeremony(host string, req PollSignRequest) (PollSignResponse, e } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return PollSignResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return PollSignResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } var response PollSignResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { @@ -241,6 +290,13 @@ func DuctSignList(host string, req ListSignRequest) (ListSignResponse, error) { } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return ListSignResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return ListSignResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } var response ListSignResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { @@ -266,6 +322,13 @@ func DuctKeygenProtocolMessage(host string, req KeyGenMessageRequest) (KeyGenMes } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return KeyGenMessageResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return KeyGenMessageResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } var response KeyGenMessageResponse json.NewDecoder(resp.Body).Decode(&response) return response, nil @@ -288,6 +351,13 @@ func DuctSignProtocolMessage(host string, req SignMessageRequest) (SignMessageRe } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return SignMessageResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return SignMessageResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } var response SignMessageResponse json.NewDecoder(resp.Body).Decode(&response) return response, nil @@ -306,8 +376,20 @@ func DuctKeygenFinalize(host string, req KeygenFinalRequest) error { if err != nil { return err } - _, err = httpClient.Post(uri, "application/json", bytes.NewReader(body)) - return err + resp, err := httpClient.Post(uri, "application/json", bytes.NewReader(body)) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return fmt.Errorf("request failed: %s", errResp.Error) + } + return fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } + return nil } func DuctSignFinalize(host string, req SignFinalRequest) error { @@ -323,8 +405,20 @@ func DuctSignFinalize(host string, req SignFinalRequest) error { if err != nil { return err } - _, err = httpClient.Post(uri, "application/json", bytes.NewReader(body)) - return err + resp, err := httpClient.Post(uri, "application/json", bytes.NewReader(body)) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return fmt.Errorf("request failed: %s", errResp.Error) + } + return fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } + return nil } func DuctGetSignature(host string, req GetSignRequest) (GetSignResponse, error) { @@ -343,6 +437,14 @@ func DuctGetSignature(host string, req GetSignRequest) (GetSignResponse, error) } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return GetSignResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return GetSignResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } + var response GetSignResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { @@ -370,6 +472,14 @@ func DuctTerminateSignCeremony(host string, req TerminateRequest) error { } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return fmt.Errorf("request failed: %s", errResp.Error) + } + return fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } + var response VapidResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { diff --git a/client/internal/types.go b/client/internal/types.go index 0ede987..2d2f7b2 100644 --- a/client/internal/types.go +++ b/client/internal/types.go @@ -132,6 +132,10 @@ type TerminateRequest struct { CeremonyID string `json:"ceremony-id"` } +type ResponseErrorPage struct { + Error string `json:"message"` +} + type VapidResponse struct { Status string `json:"status"` } diff --git a/client/main.go b/client/main.go index 43c1d38..0b2b2db 100644 --- a/client/main.go +++ b/client/main.go @@ -19,6 +19,7 @@ func main() { os.Exit(1) } + flag.Parse() args := flag.Args() if len(args) == 0 { flag.Usage() From d81e59029f1a7f6b64260212aaa23b11f18ed4aa Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 12:29:03 -0400 Subject: [PATCH 05/10] bugfixes --- client/internal/duct.go | 70 ++++++++++++++++++++++++++++++++ client/main.go | 9 +--- coordinator/internal/database.go | 15 +++++-- coordinator/internal/keygen.go | 17 ++++++-- coordinator/server.go | 70 ++++++++++++++++++++++++++++++-- 5 files changed, 164 insertions(+), 17 deletions(-) diff --git a/client/internal/duct.go b/client/internal/duct.go index 6d1bb70..0bbbc65 100644 --- a/client/internal/duct.go +++ b/client/internal/duct.go @@ -52,6 +52,8 @@ func GetApiEndpoint(host string, feature string) (string, error) { u.Path = "/keygen/poll" case "SendKeygenMessage": u.Path = "/keygen/send" + case "GetKeygenMessages": + u.Path = "/keygen/get-messages" case "FinalizeKeygenMessage": u.Path = "/keygen/finalize" case "InitSignCeremony": @@ -64,6 +66,8 @@ func GetApiEndpoint(host string, feature string) (string, error) { u.Path = "/sign/list" case "SendSignMessage": u.Path = "/sign/send" + case "GetSignMessages": + u.Path = "/sign/get-messages" case "FinalizeSignMessage": u.Path = "/sign/finalize" case "GetSignature": @@ -305,6 +309,39 @@ func DuctSignList(host string, req ListSignRequest) (ListSignResponse, error) { return response, nil } +// Get keygen protocol messages +func DuctKeygenGetMessages(host string, groupID string, lastSeen int64) (KeyGenMessageResponse, error) { + err := InitializeHttpClient() + if err != nil { + return KeyGenMessageResponse{}, err + } + uri, err := GetApiEndpoint(host, "GetKeygenMessages") + if err != nil { + return KeyGenMessageResponse{}, err + } + req := KeyGenMessageRequest{ + GroupID: groupID, + LastSeen: lastSeen, + } + body, _ := json.Marshal(req) + resp, err := httpClient.Post(uri, "application/json", bytes.NewReader(body)) + if err != nil { + return KeyGenMessageResponse{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return KeyGenMessageResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return KeyGenMessageResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } + var response KeyGenMessageResponse + json.NewDecoder(resp.Body).Decode(&response) + return response, nil +} + // Send keygen protocol messages func DuctKeygenProtocolMessage(host string, req KeyGenMessageRequest) (KeyGenMessageResponse, error) { err := InitializeHttpClient() @@ -334,6 +371,39 @@ func DuctKeygenProtocolMessage(host string, req KeyGenMessageRequest) (KeyGenMes return response, nil } +// Get sign protocol messages +func DuctSignGetMessages(host string, ceremonyID string, lastSeen int64) (SignMessageResponse, error) { + err := InitializeHttpClient() + if err != nil { + return SignMessageResponse{}, err + } + uri, err := GetApiEndpoint(host, "GetSignMessages") + if err != nil { + return SignMessageResponse{}, err + } + req := SignMessageRequest{ + CeremonyID: ceremonyID, + LastSeen: lastSeen, + } + body, _ := json.Marshal(req) + resp, err := httpClient.Post(uri, "application/json", bytes.NewReader(body)) + if err != nil { + return SignMessageResponse{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var errResp ResponseErrorPage + if json.NewDecoder(resp.Body).Decode(&errResp) == nil { + return SignMessageResponse{}, fmt.Errorf("request failed: %s", errResp.Error) + } + return SignMessageResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } + var response SignMessageResponse + json.NewDecoder(resp.Body).Decode(&response) + return response, nil +} + // Send sign protocol messages func DuctSignProtocolMessage(host string, req SignMessageRequest) (SignMessageResponse, error) { err := InitializeHttpClient() diff --git a/client/main.go b/client/main.go index 0b2b2db..0dbe77e 100644 --- a/client/main.go +++ b/client/main.go @@ -19,12 +19,7 @@ func main() { os.Exit(1) } - flag.Parse() - args := flag.Args() - if len(args) == 0 { - flag.Usage() - os.Exit(1) - } + args := os.Args[1:] // This is where commands are processed. // Note that the first verb after `freon` is case insensitive. @@ -290,7 +285,7 @@ func FreonSignCreate(args []string) { } // The actual logic is implemented here: - internal.InitSignCeremony(*groupID, *host, message, *openssh, *namespace) + internal.InitSignCeremony(*host, *groupID, message, *openssh, *namespace) } // CMD: `freon sign join ...` diff --git a/coordinator/internal/database.go b/coordinator/internal/database.go index 6092dc1..6ddfc34 100644 --- a/coordinator/internal/database.go +++ b/coordinator/internal/database.go @@ -6,6 +6,13 @@ import ( "errors" ) +type DBTX interface { + Exec(query string, args ...interface{}) (sql.Result, error) + Prepare(query string) (*sql.Stmt, error) + Query(query string, args ...interface{}) (*sql.Rows, error) + QueryRow(query string, args ...interface{}) *sql.Row +} + func DbEnsureTablesExist(db *sql.DB) error { createTable := ` CREATE TABLE IF NOT EXISTS keygroups ( @@ -58,7 +65,7 @@ func DbEnsureTablesExist(db *sql.DB) error { } // Get the row ID for a given group -func GetGroupRowId(db *sql.DB, groupUid string) (int, error) { +func GetGroupRowId(db DBTX, groupUid string) (int, error) { stmt, err := db.Prepare("SELECT id FROM keygroups WHERE uid = ?") if err != nil { return 0, err @@ -74,7 +81,7 @@ func GetGroupRowId(db *sql.DB, groupUid string) (int, error) { } // Get the row ID for a given group -func GetGroupData(db *sql.DB, groupUid string) (FreonGroup, error) { +func GetGroupData(db DBTX, groupUid string) (FreonGroup, error) { stmt, err := db.Prepare("SELECT id, threshold, participants, publicKey FROM keygroups WHERE uid = ?") if err != nil { return FreonGroup{}, err @@ -123,7 +130,7 @@ func GetGroupByID(db *sql.DB, groupID int64) (FreonGroup, error) { } // Get all of the participants for a group -func GetGroupParticipants(db *sql.DB, groupUid string) ([]FreonParticipant, error) { +func GetGroupParticipants(db DBTX, groupUid string) ([]FreonParticipant, error) { stmt, err := db.Prepare(` SELECT p.id, @@ -430,7 +437,7 @@ func InsertCeremony(db *sql.DB, c FreonCeremonies) (int64, error) { return id, nil } -func InsertParticipant(db *sql.DB, p FreonParticipant) (int64, error) { +func InsertParticipant(db DBTX, p FreonParticipant) (int64, error) { stmt, err := db.Prepare(`INSERT INTO participants (groupid, uid, partyid) VALUES (?, ?, ?)`) if err != nil { return 0, err diff --git a/coordinator/internal/keygen.go b/coordinator/internal/keygen.go index 0e81565..171e53a 100644 --- a/coordinator/internal/keygen.go +++ b/coordinator/internal/keygen.go @@ -28,11 +28,17 @@ func NewKeyGroup(db *sql.DB, n, t uint16) (string, error) { // Create a blank slate participant ID func AddParticipant(db *sql.DB, groupUid string) (FreonParticipant, error) { - groupData, err := GetGroupData(db, groupUid) + tx, err := db.Begin() if err != nil { return FreonParticipant{}, err } - participants, err := GetGroupParticipants(db, groupUid) + defer tx.Rollback() // Rollback on error + + groupData, err := GetGroupData(tx, groupUid) + if err != nil { + return FreonParticipant{}, err + } + participants, err := GetGroupParticipants(tx, groupUid) if err != nil { return FreonParticipant{}, err } @@ -66,11 +72,16 @@ func AddParticipant(db *sql.DB, groupUid string) (FreonParticipant, error) { PartyID: nextMaxId, State: []byte{}, } - id, err := InsertParticipant(db, p) + id, err := InsertParticipant(tx, p) if err != nil { return FreonParticipant{}, err } p.DbId = id + + if err = tx.Commit(); err != nil { + return FreonParticipant{}, err + } + return p, nil } diff --git a/coordinator/server.go b/coordinator/server.go index 6c5a998..181de0e 100644 --- a/coordinator/server.go +++ b/coordinator/server.go @@ -48,14 +48,13 @@ func main() { sessionManager = scs.New() sessionManager.Lifetime = 12 * time.Hour - mux := http.NewServeMux() - http.HandleFunc("/", indexPage) http.HandleFunc("/keygen/create", createKeygen) http.HandleFunc("/keygen/join", joinKeygen) http.HandleFunc("/keygen/poll", pollKeygen) http.HandleFunc("/keygen/send", sendKeygen) + http.HandleFunc("/keygen/get-messages", getKeygenMessages) http.HandleFunc("/keygen/finalize", finalizeKeygen) http.HandleFunc("/sign/create", createSign) @@ -63,11 +62,12 @@ func main() { http.HandleFunc("/sign/join", joinSign) http.HandleFunc("/sign/poll", pollSign) http.HandleFunc("/sign/send", sendSign) + http.HandleFunc("/sign/get-messages", getSignMessages) http.HandleFunc("/sign/finalize", finalizeSign) http.HandleFunc("/sign/get", getSign) http.HandleFunc("/terminate", terminateSign) - http.ListenAndServe(serverConfig.Hostname, sessionManager.LoadAndSave(mux)) + http.ListenAndServe(serverConfig.Hostname, sessionManager.LoadAndSave(http.DefaultServeMux)) } // Handler for error pages @@ -174,6 +174,38 @@ func pollKeygen(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } +// Get messages for a keygen ceremony +func getKeygenMessages(w http.ResponseWriter, r *http.Request) { + var req KeyGenMessageRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + sendError(w, err) + return + } + inbox, err := internal.GetKeygenMessagesSince(db, req.GroupID, req.LastSeen) + if err != nil { + sendError(w, err) + return + } + // Get a new maximum + var max = req.LastSeen + var messages []string + for _, m := range inbox { + if m.DbId >= max { + max = m.DbId + } + messages = append(messages, hex.EncodeToString(m.Message)) + } + + // Let's queue up the messages + response := KeyGenMessageResponse{ + LatestMessageID: max, + Messages: messages, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + // Send a message to participate in a keygen ceremony func sendKeygen(w http.ResponseWriter, r *http.Request) { var req KeyGenMessageRequest @@ -283,6 +315,38 @@ func pollSign(w http.ResponseWriter, r *http.Request) { } +// Get messages for a signing ceremony +func getSignMessages(w http.ResponseWriter, r *http.Request) { + var req SignMessageRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + sendError(w, err) + return + } + inbox, err := internal.GetSignMessagesSince(db, req.CeremonyID, req.LastSeen) + if err != nil { + sendError(w, err) + return + } + // Get a new maximum + var max = req.LastSeen + var messages []string + for _, m := range inbox { + if m.DbId >= max { + max = m.DbId + } + messages = append(messages, hex.EncodeToString(m.Message)) + } + + // Let's queue up the messages + response := SignMessageResponse{ + LatestMessageID: max, + Messages: messages, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + // Send a message to a signing ceremony func sendSign(w http.ResponseWriter, r *http.Request) { var req SignMessageRequest From a64dc3148cb15ea8e44d6d612aee9221cc9841d0 Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 16:04:01 -0400 Subject: [PATCH 06/10] ensure party id is passed --- client/internal/duct.go | 30 +++++++--- client/internal/freon.go | 2 +- client/internal/persistence.go | 3 +- client/internal/persistence_test.go | 2 +- client/internal/types.go | 1 + coordinator/server.go | 87 +++++++++++++++-------------- 6 files changed, 71 insertions(+), 54 deletions(-) diff --git a/client/internal/duct.go b/client/internal/duct.go index 0bbbc65..1fc517d 100644 --- a/client/internal/duct.go +++ b/client/internal/duct.go @@ -310,7 +310,7 @@ func DuctSignList(host string, req ListSignRequest) (ListSignResponse, error) { } // Get keygen protocol messages -func DuctKeygenGetMessages(host string, groupID string, lastSeen int64) (KeyGenMessageResponse, error) { +func DuctKeygenGetMessages(host string, groupID string, myPartyID uint16, lastSeen int64) (KeyGenMessageResponse, error) { err := InitializeHttpClient() if err != nil { return KeyGenMessageResponse{}, err @@ -320,8 +320,9 @@ func DuctKeygenGetMessages(host string, groupID string, lastSeen int64) (KeyGenM return KeyGenMessageResponse{}, err } req := KeyGenMessageRequest{ - GroupID: groupID, - LastSeen: lastSeen, + GroupID: groupID, + MyPartyID: myPartyID, + LastSeen: lastSeen, } body, _ := json.Marshal(req) resp, err := httpClient.Post(uri, "application/json", bytes.NewReader(body)) @@ -338,7 +339,10 @@ func DuctKeygenGetMessages(host string, groupID string, lastSeen int64) (KeyGenM return KeyGenMessageResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) } var response KeyGenMessageResponse - json.NewDecoder(resp.Body).Decode(&response) + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return KeyGenMessageResponse{}, err + } return response, nil } @@ -367,12 +371,15 @@ func DuctKeygenProtocolMessage(host string, req KeyGenMessageRequest) (KeyGenMes return KeyGenMessageResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) } var response KeyGenMessageResponse - json.NewDecoder(resp.Body).Decode(&response) + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return KeyGenMessageResponse{}, err + } return response, nil } // Get sign protocol messages -func DuctSignGetMessages(host string, ceremonyID string, lastSeen int64) (SignMessageResponse, error) { +func DuctSignGetMessages(host string, ceremonyID string, myPartyID uint16, lastSeen int64) (SignMessageResponse, error) { err := InitializeHttpClient() if err != nil { return SignMessageResponse{}, err @@ -383,6 +390,7 @@ func DuctSignGetMessages(host string, ceremonyID string, lastSeen int64) (SignMe } req := SignMessageRequest{ CeremonyID: ceremonyID, + MyPartyID: myPartyID, LastSeen: lastSeen, } body, _ := json.Marshal(req) @@ -400,7 +408,10 @@ func DuctSignGetMessages(host string, ceremonyID string, lastSeen int64) (SignMe return SignMessageResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) } var response SignMessageResponse - json.NewDecoder(resp.Body).Decode(&response) + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return SignMessageResponse{}, err + } return response, nil } @@ -429,7 +440,10 @@ func DuctSignProtocolMessage(host string, req SignMessageRequest) (SignMessageRe return SignMessageResponse{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) } var response SignMessageResponse - json.NewDecoder(resp.Body).Decode(&response) + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return SignMessageResponse{}, err + } return response, nil } diff --git a/client/internal/freon.go b/client/internal/freon.go index 475fb76..9e767ed 100644 --- a/client/internal/freon.go +++ b/client/internal/freon.go @@ -283,7 +283,7 @@ func JoinKeyGenCeremony(host, groupID, recipient string) { } // Okay, finally, we add the share data to the local config - err = config.AddShare(host, groupID, groupKey, secretShare, publicShares) + err = config.AddShare(host, groupID, uint16(myPartyID), groupKey, secretShare, publicShares) if err != nil { fmt.Fprintf(os.Stderr, "%s", err.Error()) os.Exit(1) diff --git a/client/internal/persistence.go b/client/internal/persistence.go index fc40a99..2ff4ece 100644 --- a/client/internal/persistence.go +++ b/client/internal/persistence.go @@ -74,10 +74,11 @@ func (cfg FreonConfig) Save() error { return encoder.Encode(cfg) } -func (cfg FreonConfig) AddShare(host, groupID, publicKey, share string, otherShares map[string]string) error { +func (cfg FreonConfig) AddShare(host, groupID string, partyID uint16, publicKey, share string, otherShares map[string]string) error { s := Shares{ Host: host, GroupID: groupID, + PartyID: partyID, PublicKey: publicKey, EncryptedShare: share, PublicShares: otherShares, diff --git a/client/internal/persistence_test.go b/client/internal/persistence_test.go index 4aae979..9e79cb2 100644 --- a/client/internal/persistence_test.go +++ b/client/internal/persistence_test.go @@ -28,7 +28,7 @@ func TestPersistence(t *testing.T) { assert.Equal(t, cfg, loadedCfg) // Test AddShare - err = loadedCfg.AddShare("localhost", "group1", "pk1", "share1", nil) + err = loadedCfg.AddShare("localhost", "group1", 1, "pk1", "share1", nil) assert.NoError(t, err) // Load the config again to check if the share was added diff --git a/client/internal/types.go b/client/internal/types.go index 2d2f7b2..39b7f69 100644 --- a/client/internal/types.go +++ b/client/internal/types.go @@ -4,6 +4,7 @@ package internal type Shares struct { Host string `json:"host"` GroupID string `json:"group-id"` + PartyID uint16 `json:"party-id"` PublicKey string `json:"public-key"` EncryptedShare string `json:"encrypted-share"` PublicShares map[string]string `json:"public-shares"` diff --git a/coordinator/server.go b/coordinator/server.go index 181de0e..31ddef4 100644 --- a/coordinator/server.go +++ b/coordinator/server.go @@ -188,22 +188,22 @@ func getKeygenMessages(w http.ResponseWriter, r *http.Request) { return } // Get a new maximum - var max = req.LastSeen + var latestID = req.LastSeen var messages []string for _, m := range inbox { - if m.DbId >= max { - max = m.DbId - } messages = append(messages, hex.EncodeToString(m.Message)) + if m.DbId > latestID { + latestID = m.DbId + } } // Let's queue up the messages response := KeyGenMessageResponse{ - LatestMessageID: max, + LatestMessageID: latestID, Messages: messages, } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(&response) } // Send a message to participate in a keygen ceremony @@ -219,39 +219,38 @@ func sendKeygen(w http.ResponseWriter, r *http.Request) { sendError(w, err) return } - inbox, err := internal.GetKeygenMessagesSince(db, req.GroupID, req.LastSeen) + + // First, add the new message to the database. + _, err = internal.AddKeyGenMessage(db, req.GroupID, req.MyPartyID, msg) if err != nil { sendError(w, err) return } - // Get a new maximum - var max = req.LastSeen - var messages []string - for _, m := range inbox { - if m.DbId >= max { - max = m.DbId - } - messages = append(messages, hex.EncodeToString(m.Message)) - } - record, err := internal.AddKeyGenMessage(db, req.GroupID, req.MyPartyID, msg) + // Now, get all messages since the client's last seen ID. + // This will include the message we just added, and any from other clients. + inbox, err := internal.GetKeygenMessagesSince(db, req.GroupID, req.LastSeen) if err != nil { sendError(w, err) return } - // If no other inserts occured, we can do this - if record.DbId-max == 1 { - max = record.DbId + // Build the response + var latestID = req.LastSeen + var messages []string + for _, m := range inbox { + messages = append(messages, hex.EncodeToString(m.Message)) + if m.DbId > latestID { + latestID = m.DbId + } } - // Let's queue up the messages response := KeyGenMessageResponse{ - LatestMessageID: max, + LatestMessageID: latestID, Messages: messages, } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(&response) } // Create a signing ceremony @@ -304,14 +303,18 @@ func pollSign(w http.ResponseWriter, r *http.Request) { sendError(w, err) return } - response, err := internal.PollSignCeremony(db, req.CeremonyID, *req.PartyID) + var partyID uint16 = 0 + if req.PartyID != nil { + partyID = *req.PartyID + } + response, err := internal.PollSignCeremony(db, req.CeremonyID, partyID) if err != nil { sendError(w, err) return } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(&response) } @@ -360,39 +363,37 @@ func sendSign(w http.ResponseWriter, r *http.Request) { sendError(w, err) return } - inbox, err := internal.GetSignMessagesSince(db, req.CeremonyID, req.LastSeen) + + // First, add the new message to the database. + _, err = internal.AddSignMessage(db, req.CeremonyID, req.MyPartyID, msg) if err != nil { sendError(w, err) return } - // Get a new maximum - var max = req.LastSeen - var messages []string - for _, m := range inbox { - if m.DbId >= max { - max = m.DbId - } - messages = append(messages, hex.EncodeToString(m.Message)) - } - record, err := internal.AddSignMessage(db, req.CeremonyID, req.MyPartyID, msg) + // Now, get all messages since the client's last seen ID. + inbox, err := internal.GetSignMessagesSince(db, req.CeremonyID, req.LastSeen) if err != nil { sendError(w, err) return } - // If no other inserts occured, we can do this - if record.DbId-max == 1 { - max = record.DbId + // Build the response + var latestID = req.LastSeen + var messages []string + for _, m := range inbox { + messages = append(messages, hex.EncodeToString(m.Message)) + if m.DbId > latestID { + latestID = m.DbId + } } - // Let's queue up the messages - response := KeyGenMessageResponse{ - LatestMessageID: max, + response := SignMessageResponse{ + LatestMessageID: latestID, Messages: messages, } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(&response) } // Store the final public key for the group From 4bbef455baa735f4b177216295f9369f1cc43072 Mon Sep 17 00:00:00 2001 From: Soatok Dreamseeker Date: Fri, 22 Aug 2025 22:07:05 -0400 Subject: [PATCH 07/10] fix public key and share encoding --- client/internal/freon.go | 274 +++++++++++++++++++++++++++------ coordinator/internal/keygen.go | 4 +- coordinator/server.go | 5 + 3 files changed, 236 insertions(+), 47 deletions(-) diff --git a/client/internal/freon.go b/client/internal/freon.go index 9e767ed..847d978 100644 --- a/client/internal/freon.go +++ b/client/internal/freon.go @@ -1,11 +1,13 @@ package internal import ( + "context" "crypto/sha512" "encoding/hex" "fmt" "hash" "os" + "sync" "time" "github.com/taurusgroup/frost-ed25519/pkg/eddsa" @@ -22,6 +24,7 @@ var timeout time.Duration = time.Hour // The ID of the last message seen. Sent with HTTP requests to fetch more messages. var lastMessageIdSeen int64 +var lastMessageMutex sync.Mutex // Used for goroutines that process FROST protocol messages // See ProcessKeygenMessages() and ProcessSignMessages() below. @@ -35,7 +38,11 @@ var ceremonyKeyGen = []byte("FREON KeyGen Ceremony v1") var ceremonySign = []byte("FREON Sign Ceremony v1") // Initialize a keygen ceremony with the coordinator -func InitKeyGenCeremony(host string, participants uint16, threshold uint16) { +func InitKeyGenCeremony(host string, participants, threshold uint16) { + if threshold > participants { + fmt.Printf("t > n: t = %d, n = %d\n", threshold, participants) + os.Exit(1) + } req := InitKeyGenRequest{ Participants: participants, Threshold: threshold, @@ -87,6 +94,7 @@ func ProcessKeygenMessages(msgsIn chan *messages.Message, s *state.State, host, fmt.Println("failed to serialize", err) continue } + lastMessageMutex.Lock() request := KeyGenMessageRequest{ GroupID: groupID, Message: hex.EncodeToString(msgBytes), @@ -96,6 +104,7 @@ func ProcessKeygenMessages(msgsIn chan *messages.Message, s *state.State, host, response, err := DuctKeygenProtocolMessage(host, request) if err != nil { fmt.Println("failed to parse response", err) + lastMessageMutex.Unlock() continue } @@ -112,6 +121,7 @@ func ProcessKeygenMessages(msgsIn chan *messages.Message, s *state.State, host, messagesIn <- &newMsg } lastMessageIdSeen = response.LatestMessageID + lastMessageMutex.Unlock() } case <-s.Done(): @@ -148,6 +158,7 @@ func ProcessSignMessages(msgsIn chan *messages.Message, s *state.State, host, ce fmt.Println("failed to serialize", err) continue } + lastMessageMutex.Lock() request := SignMessageRequest{ CeremonyID: ceremonyID, MyPartyID: myPartyID, @@ -157,6 +168,7 @@ func ProcessSignMessages(msgsIn chan *messages.Message, s *state.State, host, ce response, err := DuctSignProtocolMessage(host, request) if err != nil { fmt.Println("failed to parse response", err) + lastMessageMutex.Unlock() continue } @@ -173,6 +185,7 @@ func ProcessSignMessages(msgsIn chan *messages.Message, s *state.State, host, ce messagesIn <- &newMsg } lastMessageIdSeen = response.LatestMessageID + lastMessageMutex.Unlock() } case <-s.Done(): @@ -188,6 +201,76 @@ func ProcessSignMessages(msgsIn chan *messages.Message, s *state.State, host, ce } } +func PollKeyGenMessages(ctx context.Context, host, groupID string, myPartyID uint16) { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastMessageMutex.Lock() + seen := lastMessageIdSeen + lastMessageMutex.Unlock() + response, err := DuctKeygenGetMessages(host, groupID, myPartyID, seen) + if err != nil { + // Don't spam errors for polling + continue + } + if len(response.Messages) > 0 { + for _, m := range response.Messages { + raw, err := hex.DecodeString(m) + if err != nil { + continue + } + newMsg := messages.Message{} + newMsg.UnmarshalBinary(raw) + messagesIn <- &newMsg + } + } + lastMessageMutex.Lock() + lastMessageIdSeen = response.LatestMessageID + lastMessageMutex.Unlock() + } + } +} + +func PollSignMessages(ctx context.Context, host, ceremonyID string, myPartyID uint16) { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastMessageMutex.Lock() + seen := lastMessageIdSeen + lastMessageMutex.Unlock() + response, err := DuctSignGetMessages(host, ceremonyID, myPartyID, seen) + if err != nil { + // Don't spam errors + continue + } + if len(response.Messages) > 0 { + for _, m := range response.Messages { + raw, err := hex.DecodeString(m) + if err != nil { + continue + } + newMsg := messages.Message{} + newMsg.UnmarshalBinary(raw) + messagesIn <- &newMsg + } + } + lastMessageMutex.Lock() + lastMessageIdSeen = response.LatestMessageID + lastMessageMutex.Unlock() + } + } +} + // Join a keygen ceremony func JoinKeyGenCeremony(host, groupID, recipient string) { // First, poll the server to make sure it exists @@ -210,10 +293,13 @@ func JoinKeyGenCeremony(host, groupID, recipient string) { fmt.Fprintf(os.Stderr, "%s", err.Error()) os.Exit(1) } + if pollResponse.Threshold > pollResponse.PartySize { + fmt.Fprintf(os.Stderr, "Threshold is larger than Party Size") + os.Exit(1) + } // Load the properties from this threshold myPartyID := party.ID(joinResponse.MyPartyID) - // partySize := party.Size(pollResponse.PartySize) threshold := party.Size(pollResponse.Threshold) pollRequest.PartyID = &joinResponse.MyPartyID @@ -245,11 +331,53 @@ func JoinKeyGenCeremony(host, groupID, recipient string) { } // Use a goroutine for processing messages (which can append more messages) + messagesIn = make(chan *messages.Message, len(partyMembers)*2) lastMessageIdSeen = 0 ceremonyHash = sha512.New384() ceremonyHash.Write(ceremonyKeyGen) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go PollKeyGenMessages(ctx, host, groupID, joinResponse.MyPartyID) go ProcessKeygenMessages(messagesIn, state, host, groupID, joinResponse.MyPartyID) + // Kick off the ceremony + for _, msgOut := range state.ProcessAll() { + msgBytes, err := msgOut.MarshalBinary() + if err != nil { + fmt.Println("failed to serialize", err) + continue + } + lastMessageMutex.Lock() + request := KeyGenMessageRequest{ + GroupID: groupID, + Message: hex.EncodeToString(msgBytes), + MyPartyID: joinResponse.MyPartyID, + LastSeen: lastMessageIdSeen, + } + response, err := DuctKeygenProtocolMessage(host, request) + if err != nil { + fmt.Println("failed to parse response", err) + lastMessageMutex.Unlock() + continue + } + + // Did we get new messages to process? + for _, m := range response.Messages { + raw, err := hex.DecodeString(m) + if err != nil { + fmt.Println("failed to parse message", err) + continue + } + newMsg := messages.Message{} + newMsg.UnmarshalBinary(raw) + // Append to messagesIn + messagesIn <- &newMsg + } + lastMessageIdSeen = response.LatestMessageID + lastMessageMutex.Unlock() + } + err = state.WaitForError() if err != nil { fmt.Fprintf(os.Stderr, "%s", err.Error()) @@ -258,8 +386,13 @@ func JoinKeyGenCeremony(host, groupID, recipient string) { // If we've gotten here without an error, a group key has been established! public := output.Public + groupKeyStore, err := public.GroupKey.MarshalJSON() + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err.Error()) + os.Exit(1) + } groupKey := hex.EncodeToString(public.GroupKey.ToEd25519()) - plaintextShare, err := output.SecretKey.MarshalBinary() + plaintextShare, err := output.SecretKey.MarshalJSON() if err != nil { fmt.Fprintf(os.Stderr, "%s", err.Error()) os.Exit(1) @@ -276,14 +409,18 @@ func JoinKeyGenCeremony(host, groupID, recipient string) { } // Let's build the list of public shares publicShares := make(map[string]string) - for index, sh := range public.Shares { + for index, sh := range output.Public.Shares { i := Uint16ToHexBE(uint16(index)) - shh := hex.EncodeToString(sh.BytesEd25519()) - publicShares[i] = shh + shEncoded, err := sh.MarshalText() + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err.Error()) + os.Exit(1) + } + publicShares[i] = string(shEncoded) } // Okay, finally, we add the share data to the local config - err = config.AddShare(host, groupID, uint16(myPartyID), groupKey, secretShare, publicShares) + err = config.AddShare(host, groupID, uint16(myPartyID), string(groupKeyStore), secretShare, publicShares) if err != nil { fmt.Fprintf(os.Stderr, "%s", err.Error()) os.Exit(1) @@ -336,16 +473,40 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { fmt.Fprintf(os.Stderr, "%s", err.Error()) os.Exit(1) } - myPartyID := party.ID(pollResponse.MyPartyID) groupID := pollResponse.GroupID threshold := pollResponse.Threshold - // Next, we need to formally join the party and get your ID + // Let's pull in the data from the local config to find our party ID + config, err := LoadUserConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err.Error()) + os.Exit(1) + } + var encryptedShare string = "" + var publicSharesEncoded map[string]string + var publicKeyEncoded string + var myPartyIDfromConfig uint16 + for _, s := range config.Shares { + if s.GroupID == groupID { + encryptedShare = s.EncryptedShare + publicSharesEncoded = s.PublicShares + publicKeyEncoded = s.PublicKey + myPartyIDfromConfig = s.PartyID + break + } + } + if encryptedShare == "" { + fmt.Fprintf(os.Stderr, "could not find encrypted share for group %s", groupID) + os.Exit(1) + } + myPartyID := party.ID(myPartyIDfromConfig) + + // Next, we need to formally join the party hash := HashMessageForSanity(message, groupID) joinRequest := JoinSignRequest{ CeremonyID: ceremonyID, MessageHash: hash, - MyPartyID: pollResponse.MyPartyID, + MyPartyID: myPartyIDfromConfig, } // Enlist ourselves before we begin polling @@ -362,6 +523,7 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { opensshNamespace := res.Namespace // Now let's begin polling the server until enough parties join + pollRequest.PartyID = &myPartyIDfromConfig for { pollResponse, err = DuctPollSignCeremony(host, pollRequest) if err != nil { @@ -375,49 +537,28 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { time.Sleep(time.Second) } - // Let's pull in the data from thee local config: - config, err := LoadUserConfig() + publicKey := new(eddsa.PublicKey) + err = publicKey.UnmarshalJSON([]byte(publicKeyEncoded)) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) - } - var encryptedShare string = "" - var publicSharesHex map[string]string - var publicKeyHex string - for _, s := range config.Shares { - if s.GroupID == groupID { - encryptedShare = s.EncryptedShare - publicSharesHex = s.PublicShares - publicKeyHex = s.PublicKey - break - } - } - if encryptedShare == "" { - fmt.Fprintf(os.Stderr, "could not find encrypted share for group %s", groupID) - os.Exit(1) - } - rawPk, err := hex.DecodeString(publicKeyHex) - if err != nil { - fmt.Fprintf(os.Stderr, "could not decode encrypted share for group %s", groupID) + fmt.Fprintf(os.Stderr, "could not decode encrypted share for group %s\n%s", groupID, err.Error()) os.Exit(1) } // Let's deserialize the public shares - publicShares := make(map[party.ID]*ristretto.Element, len(publicSharesHex)) - for k, v := range publicSharesHex { + publicShares := make(map[party.ID]*ristretto.Element, len(publicSharesEncoded)) + for k, v := range publicSharesEncoded { p16, err := HexBEToUint16(k) if err != nil { fmt.Fprintf(os.Stderr, "%s", err.Error()) os.Exit(1) } - rawEl, err := hex.DecodeString(v) + pid := party.ID(p16) + el := new(ristretto.Element) + el.UnmarshalText([]byte(v)) if err != nil { fmt.Fprintf(os.Stderr, "%s", err.Error()) os.Exit(1) } - pid := party.ID(p16) - var el *ristretto.Element - el.SetCanonicalBytes(rawEl) publicShares[pid] = el } @@ -428,7 +569,7 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { os.Exit(1) } var secret eddsa.SecretShare - err = secret.UnmarshalBinary(secretBytes) + err = secret.UnmarshalJSON(secretBytes) if err != nil { fmt.Fprintf(os.Stderr, "%s", err.Error()) os.Exit(1) @@ -441,14 +582,11 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { } set := party.NewIDSlice(partyMembers) - var pkEl *ristretto.Element - pkEl.SetCanonicalBytes(rawPk) - pk := eddsa.NewPublicKeyFromPoint(pkEl) publicData := eddsa.Public{ PartyIDs: set, Threshold: party.Size(threshold), Shares: publicShares, - GroupKey: pk, + GroupKey: publicKey, } // Initilize the Sign ceremony state @@ -459,13 +597,58 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { } // Use a goroutine for processing messages (which can append more messages) + messagesIn = make(chan *messages.Message, len(partyMembers)*2) lastMessageIdSeen = 0 ceremonyHash = sha512.New384() ceremonyHash.Write(ceremonySign) - go ProcessSignMessages(messagesIn, state, host, groupID, uint16(myPartyID)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go PollSignMessages(ctx, host, ceremonyID, uint16(myPartyID)) + go ProcessSignMessages(messagesIn, state, host, ceremonyID, uint16(myPartyID)) + + // Kick off the ceremony + for _, msgOut := range state.ProcessAll() { + msgBytes, err := msgOut.MarshalBinary() + if err != nil { + fmt.Println("failed to serialize", err) + continue + } + lastMessageMutex.Lock() + seen := lastMessageIdSeen + lastMessageMutex.Unlock() + request := SignMessageRequest{ + CeremonyID: ceremonyID, + Message: hex.EncodeToString(msgBytes), + MyPartyID: uint16(myPartyID), + LastSeen: seen, + } + response, err := DuctSignProtocolMessage(host, request) + if err != nil { + fmt.Println("failed to parse response", err) + continue + } + + // Did we get new messages to process? + for _, m := range response.Messages { + raw, err := hex.DecodeString(m) + if err != nil { + fmt.Println("failed to parse message", err) + continue + } + newMsg := messages.Message{} + newMsg.UnmarshalBinary(raw) + // Append to messagesIn + messagesIn <- &newMsg + } + lastMessageMutex.Lock() + lastMessageIdSeen = response.LatestMessageID + lastMessageMutex.Unlock() + } err = state.WaitForError() if err != nil { + fmt.Fprintf(os.Stderr, "WaitForError\n") fmt.Fprintf(os.Stderr, "%s", err.Error()) os.Exit(1) } @@ -473,6 +656,7 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { // Final signature aggregation var groupSig string if openssh { + rawPk := publicKey.ToEd25519() groupSig = OpenSSHEncode(rawPk, signOutput.Signature.ToEd25519(), opensshNamespace) } else { groupSig = hex.EncodeToString(signOutput.Signature.ToEd25519()) diff --git a/coordinator/internal/keygen.go b/coordinator/internal/keygen.go index 171e53a..4de8f98 100644 --- a/coordinator/internal/keygen.go +++ b/coordinator/internal/keygen.go @@ -6,7 +6,7 @@ import ( ) // Create a new DKG group -func NewKeyGroup(db *sql.DB, n, t uint16) (string, error) { +func NewKeyGroup(db *sql.DB, partySize, threshold uint16) (string, error) { // Unique ID (192 bits entropy) uid, err := UniqueID() if err != nil { @@ -18,7 +18,7 @@ func NewKeyGroup(db *sql.DB, n, t uint16) (string, error) { if err != nil { return "", err } - _, err = stmt.Exec(uid, n, t) + _, err = stmt.Exec(uid, partySize, threshold) if err != nil { return "", err } diff --git a/coordinator/server.go b/coordinator/server.go index 31ddef4..0c2d7a0 100644 --- a/coordinator/server.go +++ b/coordinator/server.go @@ -4,6 +4,7 @@ import ( "database/sql" "encoding/hex" "encoding/json" + "errors" "fmt" "net/http" "os" @@ -95,6 +96,10 @@ func createKeygen(w http.ResponseWriter, r *http.Request) { sendError(w, err) return } + if req.Threshold > req.Participants { + sendError(w, errors.New("threshold cannot exceeed party size")) + return + } uid, err := internal.NewKeyGroup(db, req.Participants, req.Threshold) if err != nil { sendError(w, err) From eb9b876d3301515e9829afc96b86371f320e6253 Mon Sep 17 00:00:00 2001 From: Soatok Date: Sat, 30 Aug 2025 22:59:15 -0400 Subject: [PATCH 08/10] fix: switch FROST implementation to bytemare/frost --- client/go.mod | 12 +- client/go.sum | 29 +- client/internal/freon.go | 789 +++++++++++++--------------- client/internal/persistence.go | 3 +- client/internal/persistence_test.go | 2 +- client/internal/types.go | 1 - client/internal/util.go | 12 - client/internal/util_test.go | 7 - coordinator/go.mod | 7 - coordinator/go.sum | 19 - coordinator/server.go | 1 - go.mod | 2 - go.work.sum | 16 +- 13 files changed, 397 insertions(+), 503 deletions(-) diff --git a/client/go.mod b/client/go.mod index 7b84719..ea08e7e 100644 --- a/client/go.mod +++ b/client/go.mod @@ -4,17 +4,23 @@ go 1.25 require ( filippo.io/age v1.2.1 + github.com/bytemare/dkg v0.0.0-20241007182121-23ea4d549880 + github.com/bytemare/ecc v0.8.2 + github.com/bytemare/frost v0.0.0-20241019112700-8c6db5b04145 + github.com/bytemare/secret-sharing v0.7.0 github.com/stretchr/testify v1.10.0 - github.com/taurusgroup/frost-ed25519 v0.0.0-20210707140332-5abc84a4dba7 ) require ( filippo.io/edwards25519 v1.1.0 // indirect + filippo.io/nistec v0.0.3 // indirect + github.com/bytemare/hash v0.3.0 // indirect + github.com/bytemare/hash2curve v0.3.0 // indirect + github.com/bytemare/secp256k1 v0.1.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gtank/ristretto255 v0.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/sys v0.35.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/taurusgroup/frost-ed25519 => github.com/soatok/frost-ed25519 v0.0.0-20250805104728-ae78c7826e4b diff --git a/client/go.sum b/client/go.sum index 539f720..3be0cfe 100644 --- a/client/go.sum +++ b/client/go.sum @@ -4,20 +4,28 @@ filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +filippo.io/nistec v0.0.3 h1:h336Je2jRDZdBCLy2fLDUd9E2unG32JLwcJi0JQE9Cw= +filippo.io/nistec v0.0.3/go.mod h1:84fxC9mi+MhC2AERXI4LSa8cmSVOzrFikg6hZ4IfCyw= +github.com/bytemare/dkg v0.0.0-20241007182121-23ea4d549880 h1:KoEDglTZoJx0EaWdmYkvdrPNxAr/Hkc1WgWvH2b/XCw= +github.com/bytemare/dkg v0.0.0-20241007182121-23ea4d549880/go.mod h1:szhmKyIBs11r5IPo/jGqwxfmnpELmbj8okgdKxA+QVs= +github.com/bytemare/ecc v0.8.2 h1:MN+Ah48hApFpzJgIMa1xOrK7/R5uwCV06dtJyuHAi3Y= +github.com/bytemare/ecc v0.8.2/go.mod h1:dvkSikSCejw8YaTdJs6lZSN4qz9B4PC5PtGq+CRDmHk= +github.com/bytemare/frost v0.0.0-20241019112700-8c6db5b04145 h1:l9EW+NGLeOrDSl7UA6OHJFLVRnFWbMM6bGMLfOrAGKA= +github.com/bytemare/frost v0.0.0-20241019112700-8c6db5b04145/go.mod h1:WDSt6nC6QyLLrb181aQF2Niuqtxb4+MpCa9TylmmfLQ= +github.com/bytemare/hash v0.3.0 h1:RqFMt3mqpF7UxLdjBrsOZm/2cz0cQiAOnYc9gDLopWE= +github.com/bytemare/hash v0.3.0/go.mod h1:YKOBchL0l8hRLFinVCL8YUKokGNIMhrWEHPHo3EV7/M= +github.com/bytemare/hash2curve v0.3.0 h1:41Npcbc+u/E252A5aCMtxDcz7JPkkX1QzShneTFm4eg= +github.com/bytemare/hash2curve v0.3.0/go.mod h1:itj45U8uqvCtWC0eCswIHVHswXcEHkpFui7gfJdPSfQ= +github.com/bytemare/secp256k1 v0.1.6 h1:5pOA84UBBTPTUmCkjtH6jHrbvZSh2kyxG0mW/OjSih0= +github.com/bytemare/secp256k1 v0.1.6/go.mod h1:Zr7o3YCog5jKx5JwgYbj984gRIqVioTDZMSDo1y0zgE= +github.com/bytemare/secret-sharing v0.7.0 h1:ayJWEhwQzeChtavB4WrqufRJPnG5u2IePe1MEeJJEgs= +github.com/bytemare/secret-sharing v0.7.0/go.mod h1:Qzrf83Sk36D2NGJpk1/0H6YJx0SnsiOtrS6zaiISL2o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/soatok/frost-ed25519 v0.0.0-20250805104728-ae78c7826e4b h1:BmCLB7/3z4mYken+4TYyJ5n+/VVSjE+WFUYi1P4IHUo= -github.com/soatok/frost-ed25519 v0.0.0-20250805104728-ae78c7826e4b/go.mod h1:yTHqwn35f1qAkk2k6GbSWF84SpBkP8A1K2aE8g7ehTc= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= @@ -26,6 +34,5 @@ golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/client/internal/freon.go b/client/internal/freon.go index 847d978..9ece615 100644 --- a/client/internal/freon.go +++ b/client/internal/freon.go @@ -1,21 +1,17 @@ package internal import ( - "context" "crypto/sha512" "encoding/hex" "fmt" "hash" "os" - "sync" "time" - "github.com/taurusgroup/frost-ed25519/pkg/eddsa" - "github.com/taurusgroup/frost-ed25519/pkg/frost" - "github.com/taurusgroup/frost-ed25519/pkg/frost/party" - "github.com/taurusgroup/frost-ed25519/pkg/messages" - "github.com/taurusgroup/frost-ed25519/pkg/ristretto" - "github.com/taurusgroup/frost-ed25519/pkg/state" + "github.com/bytemare/dkg" + "github.com/bytemare/ecc" + "github.com/bytemare/frost" + "github.com/bytemare/secret-sharing/keys" ) // The default timeout for the FROST protocol. @@ -24,11 +20,6 @@ var timeout time.Duration = time.Hour // The ID of the last message seen. Sent with HTTP requests to fetch more messages. var lastMessageIdSeen int64 -var lastMessageMutex sync.Mutex - -// Used for goroutines that process FROST protocol messages -// See ProcessKeygenMessages() and ProcessSignMessages() below. -var messagesIn chan *messages.Message // Used for determining which party should report the final result to the ceremony var ceremonyHash hash.Hash @@ -38,11 +29,7 @@ var ceremonyKeyGen = []byte("FREON KeyGen Ceremony v1") var ceremonySign = []byte("FREON Sign Ceremony v1") // Initialize a keygen ceremony with the coordinator -func InitKeyGenCeremony(host string, participants, threshold uint16) { - if threshold > participants { - fmt.Printf("t > n: t = %d, n = %d\n", threshold, participants) - os.Exit(1) - } +func InitKeyGenCeremony(host string, participants uint16, threshold uint16) { req := InitKeyGenRequest{ Participants: participants, Threshold: threshold, @@ -73,374 +60,243 @@ func InitSignCeremony(host, groupID string, message []byte, openssh bool, namesp os.Exit(0) } -// Goroutine for processing the Keygen protocol messages -func ProcessKeygenMessages(msgsIn chan *messages.Message, s *state.State, host, groupID string, myPartyID uint16) { - for { - select { - case msg := <-msgsIn: - // The State performs some verification to check that the message is relevant for this protocol - if err := s.HandleMessage(msg); err != nil { - // An error here may not be too bad, it is not necessary to abort. - fmt.Println("failed to handle message", err) - continue - } - - // We ask the State for the next round of messages, and must handle them here. - // If an abort has occurred, then no messages are returned. - for _, msgOut := range s.ProcessAll() { - // Transport layer - msgBytes, err := msgOut.MarshalBinary() - if err != nil { - fmt.Println("failed to serialize", err) - continue - } - lastMessageMutex.Lock() - request := KeyGenMessageRequest{ - GroupID: groupID, - Message: hex.EncodeToString(msgBytes), - MyPartyID: myPartyID, - LastSeen: lastMessageIdSeen, - } - response, err := DuctKeygenProtocolMessage(host, request) - if err != nil { - fmt.Println("failed to parse response", err) - lastMessageMutex.Unlock() - continue - } - - // Did we get new messages to process? - for _, m := range response.Messages { - raw, err := hex.DecodeString(m) - if err != nil { - fmt.Println("failed to parse message", err) - continue - } - newMsg := messages.Message{} - newMsg.UnmarshalBinary(raw) - // Append to messagesIn - messagesIn <- &newMsg - } - lastMessageIdSeen = response.LatestMessageID - lastMessageMutex.Unlock() - } - - case <-s.Done(): - // s.Done() closes either when an abort has been called, or when the output has successfully been computed. - // If an error did occur, we can handle it here - err := s.WaitForError() - if err != nil { - fmt.Println("protocol aborted: ", err) - } - // In the main thread, it is safe to use the Output. - return - } - } -} - -// Goroutine for processing the Sign protocol messages -func ProcessSignMessages(msgsIn chan *messages.Message, s *state.State, host, ceremonyID string, myPartyID uint16) { - for { - select { - case msg := <-msgsIn: - // The State performs some verification to check that the message is relevant for this protocol - if err := s.HandleMessage(msg); err != nil { - // An error here may not be too bad, it is not necessary to abort. - fmt.Println("failed to handle message", err) - continue - } - - // We ask the State for the next round of messages, and must handle them here. - // If an abort has occurred, then no messages are returned. - for _, msgOut := range s.ProcessAll() { - // Transport layer - msgBytes, err := msgOut.MarshalBinary() - if err != nil { - fmt.Println("failed to serialize", err) - continue - } - lastMessageMutex.Lock() - request := SignMessageRequest{ - CeremonyID: ceremonyID, - MyPartyID: myPartyID, - Message: hex.EncodeToString(msgBytes), - LastSeen: lastMessageIdSeen, - } - response, err := DuctSignProtocolMessage(host, request) - if err != nil { - fmt.Println("failed to parse response", err) - lastMessageMutex.Unlock() - continue - } - - // Did we get new messages to process? - for _, m := range response.Messages { - raw, err := hex.DecodeString(m) - if err != nil { - fmt.Println("failed to parse message", err) - continue - } - newMsg := messages.Message{} - newMsg.UnmarshalBinary(raw) - // Append to messagesIn - messagesIn <- &newMsg - } - lastMessageIdSeen = response.LatestMessageID - lastMessageMutex.Unlock() - } - - case <-s.Done(): - // s.Done() closes either when an abort has been called, or when the output has successfully been computed. - // If an error did occur, we can handle it here - err := s.WaitForError() - if err != nil { - fmt.Println("protocol aborted: ", err) - } - // In the main thread, it is safe to use the Output. - return - } - } -} - -func PollKeyGenMessages(ctx context.Context, host, groupID string, myPartyID uint16) { - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - lastMessageMutex.Lock() - seen := lastMessageIdSeen - lastMessageMutex.Unlock() - response, err := DuctKeygenGetMessages(host, groupID, myPartyID, seen) - if err != nil { - // Don't spam errors for polling - continue - } - if len(response.Messages) > 0 { - for _, m := range response.Messages { - raw, err := hex.DecodeString(m) - if err != nil { - continue - } - newMsg := messages.Message{} - newMsg.UnmarshalBinary(raw) - messagesIn <- &newMsg - } - } - lastMessageMutex.Lock() - lastMessageIdSeen = response.LatestMessageID - lastMessageMutex.Unlock() - } - } -} - -func PollSignMessages(ctx context.Context, host, ceremonyID string, myPartyID uint16) { - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - lastMessageMutex.Lock() - seen := lastMessageIdSeen - lastMessageMutex.Unlock() - response, err := DuctSignGetMessages(host, ceremonyID, myPartyID, seen) - if err != nil { - // Don't spam errors - continue - } - if len(response.Messages) > 0 { - for _, m := range response.Messages { - raw, err := hex.DecodeString(m) - if err != nil { - continue - } - newMsg := messages.Message{} - newMsg.UnmarshalBinary(raw) - messagesIn <- &newMsg - } - } - lastMessageMutex.Lock() - lastMessageIdSeen = response.LatestMessageID - lastMessageMutex.Unlock() - } - } -} - -// Join a keygen ceremony -func JoinKeyGenCeremony(host, groupID, recipient string) { - // First, poll the server to make sure it exists +func joinCeremonyAndPoll(host, groupID string) (uint16, uint16, uint16, []uint16, error) { pollRequest := PollKeyGenRequest{ GroupID: groupID, PartyID: nil, } pollResponse, err := DuctPollKeyGenCeremony(host, pollRequest) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) + return 0, 0, 0, nil, err } - // Next, we need to formally join the party and get your ID joinRequest := JoinKeyGenRequest{ GroupID: groupID, } joinResponse, err := DuctJoinKeyGenCeremony(host, joinRequest) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) - } - if pollResponse.Threshold > pollResponse.PartySize { - fmt.Fprintf(os.Stderr, "Threshold is larger than Party Size") - os.Exit(1) + return 0, 0, 0, nil, err } + ceremonyHash = sha512.New384() + ceremonyHash.Write(ceremonyKeyGen) - // Load the properties from this threshold - myPartyID := party.ID(joinResponse.MyPartyID) - threshold := party.Size(pollResponse.Threshold) - pollRequest.PartyID = &joinResponse.MyPartyID + myPartyID := joinResponse.MyPartyID + threshold := pollResponse.Threshold + partySize := pollResponse.PartySize + pollRequest.PartyID = &myPartyID - // Now let's begin polling the server until enough parties join for { pollResponse, err = DuctPollKeyGenCeremony(host, pollRequest) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) + return 0, 0, 0, nil, err } found := uint16(len(pollResponse.OtherParties)) - if found+1 == pollResponse.PartySize { - // We can stop polling + if found+1 == partySize { break } time.Sleep(time.Second) } - // Great, let's process the party members now that we're full - partyMembers := []party.ID{myPartyID} - for _, p := range pollResponse.OtherParties { - partyMembers = append(partyMembers, party.ID(p)) - } - set := party.NewIDSlice(partyMembers) - state, output, err := frost.NewKeygenState(myPartyID, set, threshold, timeout) + partyMembers := []uint16{myPartyID} + partyMembers = append(partyMembers, pollResponse.OtherParties...) + return myPartyID, threshold, partySize, partyMembers, nil +} + +func performDKGRound1(host, groupID string, myPartyID, threshold, partySize uint16, partyMembers []uint16) (*dkg.Participant, []*dkg.Round1Data, error) { + participant, err := dkg.Edwards25519Sha512.NewParticipant(myPartyID, threshold, partySize) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) + return nil, nil, fmt.Errorf("failed to start dkg: %w", err) } - // Use a goroutine for processing messages (which can append more messages) - messagesIn = make(chan *messages.Message, len(partyMembers)*2) - lastMessageIdSeen = 0 - ceremonyHash = sha512.New384() - ceremonyHash.Write(ceremonyKeyGen) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - go PollKeyGenMessages(ctx, host, groupID, joinResponse.MyPartyID) - go ProcessKeygenMessages(messagesIn, state, host, groupID, joinResponse.MyPartyID) + r1Message := participant.Start() + r1Bytes := r1Message.Encode() + _, err = DuctKeygenProtocolMessage(host, KeyGenMessageRequest{ + GroupID: groupID, + Message: hex.EncodeToString(r1Bytes), + MyPartyID: myPartyID, + }) + if err != nil { + return nil, nil, fmt.Errorf("failed to send r1 message: %w", err) + } - // Kick off the ceremony - for _, msgOut := range state.ProcessAll() { - msgBytes, err := msgOut.MarshalBinary() + r1Messages := make(map[uint16]*dkg.Round1Data) + r1Messages[myPartyID] = r1Message + for len(r1Messages) < len(partyMembers) { + resp, err := DuctKeygenProtocolMessage(host, KeyGenMessageRequest{ + GroupID: groupID, + MyPartyID: myPartyID, + LastSeen: lastMessageIdSeen, + }) if err != nil { - fmt.Println("failed to serialize", err) - continue + return nil, nil, fmt.Errorf("failed to poll for r1 messages: %w", err) + } + for _, msgStr := range resp.Messages { + msgBytes, err := hex.DecodeString(msgStr) + if err != nil { + continue + } + ceremonyHash.Write(msgBytes) + msg := &dkg.Round1Data{} + if err := msg.Decode(msgBytes); err == nil { + if _, ok := r1Messages[msg.SenderIdentifier]; !ok { + r1Messages[msg.SenderIdentifier] = msg + } + } } - lastMessageMutex.Lock() - request := KeyGenMessageRequest{ + lastMessageIdSeen = resp.LatestMessageID + time.Sleep(time.Second) + } + var r1Data []*dkg.Round1Data + for _, m := range r1Messages { + r1Data = append(r1Data, m) + } + return participant, r1Data, nil +} + +func performDKGRound2(host, groupID string, myPartyID, threshold uint16, participant *dkg.Participant, r1Data []*dkg.Round1Data) ([]*dkg.Round2Data, error) { + r2Messages, err := participant.Continue(r1Data) + if err != nil { + return nil, fmt.Errorf("failed to continue dkg: %w", err) + } + for _, msg := range r2Messages { + msgBytes := msg.Encode() + ceremonyHash.Write(msgBytes) + _, err = DuctKeygenProtocolMessage(host, KeyGenMessageRequest{ GroupID: groupID, Message: hex.EncodeToString(msgBytes), - MyPartyID: joinResponse.MyPartyID, - LastSeen: lastMessageIdSeen, - } - response, err := DuctKeygenProtocolMessage(host, request) + MyPartyID: myPartyID, + }) if err != nil { - fmt.Println("failed to parse response", err) - lastMessageMutex.Unlock() - continue + return nil, fmt.Errorf("failed to send r2 message: %w", err) } + } - // Did we get new messages to process? - for _, m := range response.Messages { - raw, err := hex.DecodeString(m) + myR2Messages := make(map[uint16]*dkg.Round2Data) + for len(myR2Messages) < int(threshold)-1 { + resp, err := DuctKeygenProtocolMessage(host, KeyGenMessageRequest{ + GroupID: groupID, + MyPartyID: myPartyID, + LastSeen: lastMessageIdSeen, + }) + if err != nil { + return nil, fmt.Errorf("failed to poll for r2 messages: %w", err) + } + for _, msgStr := range resp.Messages { + msgBytes, err := hex.DecodeString(msgStr) if err != nil { - fmt.Println("failed to parse message", err) continue } - newMsg := messages.Message{} - newMsg.UnmarshalBinary(raw) - // Append to messagesIn - messagesIn <- &newMsg + ceremonyHash.Write(msgBytes) + msg := &dkg.Round2Data{} + if err := msg.Decode(msgBytes); err == nil { + if msg.RecipientIdentifier == myPartyID { + if _, ok := myR2Messages[msg.SenderIdentifier]; !ok { + myR2Messages[msg.SenderIdentifier] = msg + } + } + } } - lastMessageIdSeen = response.LatestMessageID - lastMessageMutex.Unlock() + lastMessageIdSeen = resp.LatestMessageID + time.Sleep(time.Second) } + var r2Data []*dkg.Round2Data + for _, m := range myR2Messages { + r2Data = append(r2Data, m) + } + return r2Data, nil +} - err = state.WaitForError() +func finalizeAndStoreKeys(host, groupID, recipient string, myPartyID uint16, partyMembers []uint16, participant *dkg.Participant, r1Data []*dkg.Round1Data, r2Data []*dkg.Round2Data) error { + keyShare, err := participant.Finalize(r1Data, r2Data) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) + return fmt.Errorf("failed to finalize dkg: %w", err) } - // If we've gotten here without an error, a group key has been established! - public := output.Public - groupKeyStore, err := public.GroupKey.MarshalJSON() - if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) + var allCommitments [][]*ecc.Element + for _, d := range r1Data { + allCommitments = append(allCommitments, d.Commitment) } - groupKey := hex.EncodeToString(public.GroupKey.ToEd25519()) - plaintextShare, err := output.SecretKey.MarshalJSON() - if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) + + publicShares := make(map[string]string) + for _, pID := range partyMembers { + pubKey, err := dkg.ComputeParticipantPublicKey(dkg.Edwards25519Sha512, pID, allCommitments) + if err != nil { + return fmt.Errorf("failed to compute public key for party %d: %w", pID, err) + } + pubKeyBytes := pubKey.Encode() + publicShares[Uint16ToHexBE(pID)] = hex.EncodeToString(pubKeyBytes) } - secretShare, err := EncryptShare(recipient, plaintextShare) + + groupKeyBytes := keyShare.VerificationKey.Encode() + groupKeyHex := hex.EncodeToString(groupKeyBytes) + + secretShareBytes := keyShare.Secret.Encode() + encryptedShare, err := EncryptShare(recipient, secretShareBytes) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) + return fmt.Errorf("failed to encrypt share: %w", err) } + config, err := LoadUserConfig() if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) - } - // Let's build the list of public shares - publicShares := make(map[string]string) - for index, sh := range output.Public.Shares { - i := Uint16ToHexBE(uint16(index)) - shEncoded, err := sh.MarshalText() - if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) - } - publicShares[i] = string(shEncoded) + return err } - // Okay, finally, we add the share data to the local config - err = config.AddShare(host, groupID, uint16(myPartyID), string(groupKeyStore), secretShare, publicShares) + err = config.AddShare(host, groupID, groupKeyHex, encryptedShare, publicShares) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) + return err } ch := ceremonyHash.Sum(nil) - if AmIElected(ch, uint16(myPartyID), PartyToUint16(partyMembers)) { + if AmIElected(ch, myPartyID, partyMembers) { report := KeygenFinalRequest{ GroupID: groupID, - MyPartyID: uint16(myPartyID), - PublicKey: groupKey, + MyPartyID: myPartyID, + PublicKey: groupKeyHex, } err := DuctKeygenFinalize(host, report) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - // This is only a reporting error, so do not error out. + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) } } - fmt.Printf("Group public key:\n%s\n", groupKey) - // OK + fmt.Printf("Group public key:\n%s\n", groupKeyHex) os.Exit(0) + return nil +} + +// Join a keygen ceremony +func JoinKeyGenCeremony(host, groupID, recipient string) { + // This function is getting long. Let's break it down into smaller pieces. + // 1. Join the ceremony and get participant info. + // 2. Perform DKG Round 1. + // 3. Perform DKG Round 2. + // 4. Finalize and store keys. + + // 1. Join the ceremony and get participant info. + myPartyID, threshold, partySize, partyMembers, err := joinCeremonyAndPoll(host, groupID) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to join ceremony: %s\n", err.Error()) + os.Exit(1) + } + + // 2. Perform DKG Round 1. + participant, r1Data, err := performDKGRound1(host, groupID, myPartyID, threshold, partySize, partyMembers) + if err != nil { + fmt.Fprintf(os.Stderr, "DKG round 1 failed: %s\n", err.Error()) + os.Exit(1) + } + + // 3. Perform DKG Round 2. + r2Data, err := performDKGRound2(host, groupID, myPartyID, threshold, participant, r1Data) + if err != nil { + fmt.Fprintf(os.Stderr, "DKG round 2 failed: %s\n", err.Error()) + os.Exit(1) + } + + // 4. Finalize and store keys. + err = finalizeAndStoreKeys(host, groupID, recipient, myPartyID, partyMembers, participant, r1Data, r2Data) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to finalize and store keys: %s\n", err.Error()) + os.Exit(1) + } } // List local key shares and groups @@ -470,49 +326,25 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { } pollResponse, err := DuctPollSignCeremony(host, pollRequest) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } + myPartyID := uint16(pollResponse.MyPartyID) groupID := pollResponse.GroupID threshold := pollResponse.Threshold - // Let's pull in the data from the local config to find our party ID - config, err := LoadUserConfig() - if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) - } - var encryptedShare string = "" - var publicSharesEncoded map[string]string - var publicKeyEncoded string - var myPartyIDfromConfig uint16 - for _, s := range config.Shares { - if s.GroupID == groupID { - encryptedShare = s.EncryptedShare - publicSharesEncoded = s.PublicShares - publicKeyEncoded = s.PublicKey - myPartyIDfromConfig = s.PartyID - break - } - } - if encryptedShare == "" { - fmt.Fprintf(os.Stderr, "could not find encrypted share for group %s", groupID) - os.Exit(1) - } - myPartyID := party.ID(myPartyIDfromConfig) - - // Next, we need to formally join the party + // Next, we need to formally join the party and get your ID hash := HashMessageForSanity(message, groupID) joinRequest := JoinSignRequest{ CeremonyID: ceremonyID, MessageHash: hash, - MyPartyID: myPartyIDfromConfig, + MyPartyID: pollResponse.MyPartyID, } // Enlist ourselves before we begin polling res, err := DuctJoinSignCeremony(host, joinRequest) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } if !res.Status { @@ -523,11 +355,10 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { opensshNamespace := res.Namespace // Now let's begin polling the server until enough parties join - pollRequest.PartyID = &myPartyIDfromConfig for { pollResponse, err = DuctPollSignCeremony(host, pollRequest) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } others := uint16(len(pollResponse.OtherParties)) @@ -537,142 +368,227 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { time.Sleep(time.Second) } - publicKey := new(eddsa.PublicKey) - err = publicKey.UnmarshalJSON([]byte(publicKeyEncoded)) + // Let's pull in the data from the local config: + config, err := LoadUserConfig() if err != nil { - fmt.Fprintf(os.Stderr, "could not decode encrypted share for group %s\n%s", groupID, err.Error()) + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } - - // Let's deserialize the public shares - publicShares := make(map[party.ID]*ristretto.Element, len(publicSharesEncoded)) - for k, v := range publicSharesEncoded { - p16, err := HexBEToUint16(k) - if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) - } - pid := party.ID(p16) - el := new(ristretto.Element) - el.UnmarshalText([]byte(v)) - if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) + var encryptedShare string + var publicSharesHex map[string]string + var publicKeyHex string + for _, s := range config.Shares { + if s.GroupID == groupID { + encryptedShare = s.EncryptedShare + publicSharesHex = s.PublicShares + publicKeyHex = s.PublicKey + break } - publicShares[pid] = el + } + if encryptedShare == "" { + fmt.Fprintf(os.Stderr, "could not find encrypted share for group %s\n", groupID) + os.Exit(1) } // Let's decrypt the local share with age secretBytes, err := DecryptShareFor(encryptedShare, identityFile) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } - var secret eddsa.SecretShare - err = secret.UnmarshalJSON(secretBytes) + secretKey := dkg.Edwards25519Sha512.Group().NewScalar() + if err := secretKey.Decode(secretBytes); err != nil { + fmt.Fprintf(os.Stderr, "failed to decode secret key: %s\n", err.Error()) + os.Exit(1) + } + + // Let's decode the public key and public shares + groupKeyBytes, err := hex.DecodeString(publicKeyHex) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) + fmt.Fprintf(os.Stderr, "failed to decode group key: %s\n", err.Error()) + os.Exit(1) + } + groupKey := dkg.Edwards25519Sha512.Group().NewElement() + if err := groupKey.Decode(groupKeyBytes); err != nil { + fmt.Fprintf(os.Stderr, "failed to decode group key: %s\n", err.Error()) os.Exit(1) } + // Let's make sure we have all parties' public shares setup locally + publicShares := make([]*keys.PublicKeyShare, 0, len(publicSharesHex)) + for k, v := range publicSharesHex { + p16, err := HexBEToUint16(k) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + rawEl, err := hex.DecodeString(v) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + el := dkg.Edwards25519Sha512.Group().NewElement() + if err := el.Decode(rawEl); err != nil { + fmt.Fprintf(os.Stderr, "failed to decode public share for party %d: %s\n", p16, err.Error()) + os.Exit(1) + } + publicShares = append(publicShares, &keys.PublicKeyShare{ID: p16, PublicKey: el}) + } + // Great, let's process the party members now that we're full - partyMembers := []party.ID{myPartyID} - for _, p := range pollResponse.OtherParties { - partyMembers = append(partyMembers, party.ID(p)) + partyMembers := []uint16{myPartyID} + partyMembers = append(partyMembers, pollResponse.OtherParties...) + + conf := &frost.Configuration{ + Ciphersuite: frost.Ed25519, + Threshold: threshold, + MaxSigners: uint16(len(partyMembers)), + VerificationKey: groupKey, + SignerPublicKeyShares: publicShares, + } + if err := conf.Init(); err != nil { + fmt.Fprintf(os.Stderr, "failed to initialize frost config: %s\n", err.Error()) + os.Exit(1) } - set := party.NewIDSlice(partyMembers) - publicData := eddsa.Public{ - PartyIDs: set, - Threshold: party.Size(threshold), - Shares: publicShares, - GroupKey: publicKey, + myPublicKey := dkg.Edwards25519Sha512.Group().Base().Multiply(secretKey) + myKeyShare := &keys.KeyShare{ + Secret: secretKey, + PublicKeyShare: keys.PublicKeyShare{ID: myPartyID, PublicKey: myPublicKey}, + VerificationKey: groupKey, } - // Initilize the Sign ceremony state - state, signOutput, err := frost.NewSignState(set, &secret, &publicData, message, timeout) + signer, err := conf.Signer(myKeyShare) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) + fmt.Fprintf(os.Stderr, "failed to create signer: %s\n", err.Error()) os.Exit(1) } - // Use a goroutine for processing messages (which can append more messages) - messagesIn = make(chan *messages.Message, len(partyMembers)*2) - lastMessageIdSeen = 0 + // Round 1: Commitment ceremonyHash = sha512.New384() ceremonyHash.Write(ceremonySign) + commitment := signer.Commit() + commitBytes := commitment.Encode() + _, err = DuctSignProtocolMessage(host, SignMessageRequest{ + CeremonyID: ceremonyID, + Message: hex.EncodeToString(commitBytes), + MyPartyID: myPartyID, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to send commitment: %s\n", err.Error()) + os.Exit(1) + } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - go PollSignMessages(ctx, host, ceremonyID, uint16(myPartyID)) - go ProcessSignMessages(messagesIn, state, host, ceremonyID, uint16(myPartyID)) - - // Kick off the ceremony - for _, msgOut := range state.ProcessAll() { - msgBytes, err := msgOut.MarshalBinary() + // Poll for commitments from other participants + commitments := make(map[uint16]*frost.Commitment) + commitments[myPartyID] = commitment + for len(commitments) < len(partyMembers) { + resp, err := DuctSignProtocolMessage(host, SignMessageRequest{ + CeremonyID: ceremonyID, + MyPartyID: myPartyID, + LastSeen: lastMessageIdSeen, + }) if err != nil { - fmt.Println("failed to serialize", err) - continue + fmt.Fprintf(os.Stderr, "failed to poll for commitments: %s\n", err.Error()) + os.Exit(1) } - lastMessageMutex.Lock() - seen := lastMessageIdSeen - lastMessageMutex.Unlock() - request := SignMessageRequest{ - CeremonyID: ceremonyID, - Message: hex.EncodeToString(msgBytes), - MyPartyID: uint16(myPartyID), - LastSeen: seen, + for _, msgStr := range resp.Messages { + msgBytes, err := hex.DecodeString(msgStr) + if err != nil { + continue // Ignore invalid messages + } + ceremonyHash.Write(msgBytes) + c := &frost.Commitment{} + if err := c.Decode(msgBytes); err == nil { + if _, ok := commitments[c.SignerID]; !ok { + commitments[c.SignerID] = c + } + } } - response, err := DuctSignProtocolMessage(host, request) + lastMessageIdSeen = resp.LatestMessageID + time.Sleep(time.Second) + } + + var commitmentList []*frost.Commitment + for _, c := range commitments { + commitmentList = append(commitmentList, c) + } + + // Round 2: Sign + sigShare, err := signer.Sign(message, commitmentList) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to sign: %s\n", err.Error()) + os.Exit(1) + } + shareBytes := sigShare.Encode() + _, err = DuctSignProtocolMessage(host, SignMessageRequest{ + CeremonyID: ceremonyID, + Message: hex.EncodeToString(shareBytes), + MyPartyID: myPartyID, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to send signature share: %s\n", err.Error()) + os.Exit(1) + } + + // Poll for signature shares from other participants + sigShares := make(map[uint16]*frost.SignatureShare) + sigShares[myPartyID] = sigShare + for len(sigShares) < len(partyMembers) { + resp, err := DuctSignProtocolMessage(host, SignMessageRequest{ + CeremonyID: ceremonyID, + MyPartyID: myPartyID, + LastSeen: lastMessageIdSeen, + }) if err != nil { - fmt.Println("failed to parse response", err) - continue + fmt.Fprintf(os.Stderr, "failed to poll for signature shares: %s\n", err.Error()) + os.Exit(1) } - - // Did we get new messages to process? - for _, m := range response.Messages { - raw, err := hex.DecodeString(m) + for _, msgStr := range resp.Messages { + msgBytes, err := hex.DecodeString(msgStr) if err != nil { - fmt.Println("failed to parse message", err) - continue + continue // Ignore invalid messages + } + ceremonyHash.Write(msgBytes) + s := &frost.SignatureShare{} + if err := s.Decode(msgBytes); err == nil { + if _, ok := sigShares[s.SignerIdentifier]; !ok { + sigShares[s.SignerIdentifier] = s + } } - newMsg := messages.Message{} - newMsg.UnmarshalBinary(raw) - // Append to messagesIn - messagesIn <- &newMsg } - lastMessageMutex.Lock() - lastMessageIdSeen = response.LatestMessageID - lastMessageMutex.Unlock() + lastMessageIdSeen = resp.LatestMessageID + time.Sleep(time.Second) + } + var signatureShares []*frost.SignatureShare + for _, s := range sigShares { + signatureShares = append(signatureShares, s) } - err = state.WaitForError() + // Aggregate signatures + finalSignature, err := conf.AggregateSignatures(message, signatureShares, commitmentList, true) if err != nil { - fmt.Fprintf(os.Stderr, "WaitForError\n") - fmt.Fprintf(os.Stderr, "%s", err.Error()) + fmt.Fprintf(os.Stderr, "failed to aggregate signatures: %s\n", err.Error()) os.Exit(1) } - // Final signature aggregation + finalSignatureBytes := append(finalSignature.R.Encode(), finalSignature.Z.Encode()...) var groupSig string if openssh { - rawPk := publicKey.ToEd25519() - groupSig = OpenSSHEncode(rawPk, signOutput.Signature.ToEd25519(), opensshNamespace) + groupSig = OpenSSHEncode(groupKeyBytes, finalSignatureBytes, opensshNamespace) } else { - groupSig = hex.EncodeToString(signOutput.Signature.ToEd25519()) + groupSig = hex.EncodeToString(finalSignatureBytes) } ch := ceremonyHash.Sum(nil) - - if AmIElected(ch, uint16(myPartyID), PartyToUint16(partyMembers)) { + if AmIElected(ch, myPartyID, partyMembers) { report := SignFinalRequest{ CeremonyID: ceremonyID, - MyPartyID: uint16(myPartyID), + MyPartyID: myPartyID, Signature: groupSig, } err := DuctSignFinalize(host, report) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - // We do not abort here, since the only error was with reporting upstream + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) } } fmt.Printf("Signature:\n%s\n", groupSig) @@ -734,6 +650,7 @@ func GetSignSignature(ceremonyID, host string) { os.Exit(0) } +// Tell the coordinator to pull the plug on a signing ceremony func TerminateSignCeremony(host, ceremonyID string) { req := TerminateRequest{ CeremonyID: ceremonyID, diff --git a/client/internal/persistence.go b/client/internal/persistence.go index 2ff4ece..fc40a99 100644 --- a/client/internal/persistence.go +++ b/client/internal/persistence.go @@ -74,11 +74,10 @@ func (cfg FreonConfig) Save() error { return encoder.Encode(cfg) } -func (cfg FreonConfig) AddShare(host, groupID string, partyID uint16, publicKey, share string, otherShares map[string]string) error { +func (cfg FreonConfig) AddShare(host, groupID, publicKey, share string, otherShares map[string]string) error { s := Shares{ Host: host, GroupID: groupID, - PartyID: partyID, PublicKey: publicKey, EncryptedShare: share, PublicShares: otherShares, diff --git a/client/internal/persistence_test.go b/client/internal/persistence_test.go index 9e79cb2..4aae979 100644 --- a/client/internal/persistence_test.go +++ b/client/internal/persistence_test.go @@ -28,7 +28,7 @@ func TestPersistence(t *testing.T) { assert.Equal(t, cfg, loadedCfg) // Test AddShare - err = loadedCfg.AddShare("localhost", "group1", 1, "pk1", "share1", nil) + err = loadedCfg.AddShare("localhost", "group1", "pk1", "share1", nil) assert.NoError(t, err) // Load the config again to check if the share was added diff --git a/client/internal/types.go b/client/internal/types.go index 39b7f69..2d2f7b2 100644 --- a/client/internal/types.go +++ b/client/internal/types.go @@ -4,7 +4,6 @@ package internal type Shares struct { Host string `json:"host"` GroupID string `json:"group-id"` - PartyID uint16 `json:"party-id"` PublicKey string `json:"public-key"` EncryptedShare string `json:"encrypted-share"` PublicShares map[string]string `json:"public-shares"` diff --git a/client/internal/util.go b/client/internal/util.go index ad87534..a4acadc 100644 --- a/client/internal/util.go +++ b/client/internal/util.go @@ -8,8 +8,6 @@ import ( "encoding/hex" "fmt" "slices" - - "github.com/taurusgroup/frost-ed25519/pkg/frost/party" ) // This is just a consistency check for the message, so we can abort early if something mismatches @@ -75,13 +73,3 @@ func AmIElected(ch []byte, me uint16, party []uint16) bool { index := SelectIndex(ch, ps) return party[index] == me } - -// Given a party.IDSlice, get a sorted []uint16 of party IDs. -func PartyToUint16(party party.IDSlice) []uint16 { - var party16 []uint16 - for _, p := range party { - party16 = append(party16, uint16(p)) - } - slices.Sort(party16) - return party16 -} diff --git a/client/internal/util_test.go b/client/internal/util_test.go index 19afe0e..f6ec4eb 100644 --- a/client/internal/util_test.go +++ b/client/internal/util_test.go @@ -7,7 +7,6 @@ import ( "github.com/soatok/freon/client/internal" "github.com/stretchr/testify/assert" - "github.com/taurusgroup/frost-ed25519/pkg/frost/party" ) func TestHashMessageForSanity(t *testing.T) { @@ -108,12 +107,6 @@ func TestSelectIndex(t *testing.T) { assert.Equal(t, uint64(4), index) } -func TestPartyToUint16(t *testing.T) { - party := party.IDSlice{5, 7, 6} - slice := internal.PartyToUint16(party) - assert.Equal(t, []uint16{5, 6, 7}, slice) -} - func TestHexBEToUint16(t *testing.T) { val, err := internal.HexBEToUint16("0100") assert.NoError(t, err) diff --git a/coordinator/go.mod b/coordinator/go.mod index cd31c3c..1796228 100644 --- a/coordinator/go.mod +++ b/coordinator/go.mod @@ -2,25 +2,18 @@ module github.com/soatok/freon/coordinator go 1.25 -require github.com/taurusgroup/frost-ed25519 v0.0.0-20210707140332-5abc84a4dba7 - require github.com/alexedwards/scs/v2 v2.9.0 require ( - filippo.io/age v1.2.1 github.com/ncruces/go-sqlite3 v0.28.0 github.com/stretchr/testify v1.10.0 ) require ( - filippo.io/edwards25519 v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect - golang.org/x/crypto v0.41.0 // indirect golang.org/x/sys v0.35.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/taurusgroup/frost-ed25519 => github.com/soatok/frost-ed25519 v0.0.0-20250805104728-ae78c7826e4b diff --git a/coordinator/go.sum b/coordinator/go.sum index 1b24e3c..2c58eb9 100644 --- a/coordinator/go.sum +++ b/coordinator/go.sum @@ -1,12 +1,5 @@ -c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= -c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= -filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= -filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90= github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ncruces/go-sqlite3 v0.28.0 h1:AQVTUPgfamONl09LS+4rGFbHmLKM8/QrJJJi1UukjEQ= @@ -15,27 +8,15 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/soatok/frost-ed25519 v0.0.0-20250805104728-ae78c7826e4b h1:BmCLB7/3z4mYken+4TYyJ5n+/VVSjE+WFUYi1P4IHUo= -github.com/soatok/frost-ed25519 v0.0.0-20250805104728-ae78c7826e4b/go.mod h1:yTHqwn35f1qAkk2k6GbSWF84SpBkP8A1K2aE8g7ehTc= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/coordinator/server.go b/coordinator/server.go index 0c2d7a0..dafa7b5 100644 --- a/coordinator/server.go +++ b/coordinator/server.go @@ -14,7 +14,6 @@ import ( _ "github.com/ncruces/go-sqlite3/driver" _ "github.com/ncruces/go-sqlite3/embed" "github.com/soatok/freon/coordinator/internal" - _ "github.com/taurusgroup/frost-ed25519/pkg/frost" ) var sessionManager *scs.SessionManager diff --git a/go.mod b/go.mod index de143b6..25f0b04 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/soatok/freon go 1.25 - -replace github.com/taurusgroup/frost-ed25519 => github.com/soatok/frost-ed25519 v0.0.0-20250805104728-ae78c7826e4b diff --git a/go.work.sum b/go.work.sum index 2c02a4d..98b041d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,23 +1,37 @@ filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE= +github.com/bytemare/secret-sharing v0.8.0 h1:bLq3+L1cwpEBxoLS9BCdXzsvbDZ8tyvwRwNYkXR+h3w= +github.com/bytemare/secret-sharing v0.8.0/go.mod h1:kATLAk+KLRfzUZeA4RJo8TwOHOn0sVmJuE+FOQjjKhU= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/ncruces/aa v0.3.0 h1:6NPcK3jsyPWRZBZWCyF7c4IzEQX4eJtkKJBA+IRKTkQ= github.com/ncruces/aa v0.3.0/go.mod h1:ctOw1LVqfuqzqg2S9LlR045bLAiXtaTiPMCL3zzl7Ik= +github.com/ncruces/sort v0.1.5 h1:fiFWXXAqKI8QckPf/6hu/bGFwcEPrirIOFaJqWujs4k= github.com/ncruces/sort v0.1.5/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk= +github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE= github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/soatok/freon/coordinator v0.0.0-20250822044951-bf74d56db437/go.mod h1:g4YrSLYKtfoiWHNuUn/MUobeff/xLq/Y4xNJXtVVfiI= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA= lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw= From 991aabd771106f0ec59abdeae1d57592de73c270 Mon Sep 17 00:00:00 2001 From: Soatok Date: Sat, 30 Aug 2025 23:29:17 -0400 Subject: [PATCH 09/10] fix: bugs found by integration test --- client/internal/freon.go | 102 ++++++++++++++++++---------- client/internal/persistence.go | 3 +- client/internal/persistence_test.go | 2 +- client/internal/types.go | 1 + client/internal/util.go | 1 + 5 files changed, 70 insertions(+), 39 deletions(-) diff --git a/client/internal/freon.go b/client/internal/freon.go index 9ece615..337cca3 100644 --- a/client/internal/freon.go +++ b/client/internal/freon.go @@ -153,7 +153,7 @@ func performDKGRound1(host, groupID string, myPartyID, threshold, partySize uint return participant, r1Data, nil } -func performDKGRound2(host, groupID string, myPartyID, threshold uint16, participant *dkg.Participant, r1Data []*dkg.Round1Data) ([]*dkg.Round2Data, error) { +func performDKGRound2(host, groupID string, myPartyID, partySize uint16, participant *dkg.Participant, r1Data []*dkg.Round1Data) ([]*dkg.Round2Data, error) { r2Messages, err := participant.Continue(r1Data) if err != nil { return nil, fmt.Errorf("failed to continue dkg: %w", err) @@ -172,7 +172,7 @@ func performDKGRound2(host, groupID string, myPartyID, threshold uint16, partici } myR2Messages := make(map[uint16]*dkg.Round2Data) - for len(myR2Messages) < int(threshold)-1 { + for len(myR2Messages) < int(partySize)-1 { resp, err := DuctKeygenProtocolMessage(host, KeyGenMessageRequest{ GroupID: groupID, MyPartyID: myPartyID, @@ -241,7 +241,7 @@ func finalizeAndStoreKeys(host, groupID, recipient string, myPartyID uint16, par return err } - err = config.AddShare(host, groupID, groupKeyHex, encryptedShare, publicShares) + err = config.AddShare(host, groupID, groupKeyHex, encryptedShare, publicShares, myPartyID) if err != nil { return err } @@ -285,7 +285,7 @@ func JoinKeyGenCeremony(host, groupID, recipient string) { } // 3. Perform DKG Round 2. - r2Data, err := performDKGRound2(host, groupID, myPartyID, threshold, participant, r1Data) + r2Data, err := performDKGRound2(host, groupID, myPartyID, partySize, participant, r1Data) if err != nil { fmt.Fprintf(os.Stderr, "DKG round 2 failed: %s\n", err.Error()) os.Exit(1) @@ -319,26 +319,55 @@ func ListKeyGen() { // Join a signing ceremony func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { - // First, poll the server to get metadata + // Let's pull in the data from the local config: + config, err := LoadUserConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + + // Before we can do anything, we need to get the GroupID from the ceremony. + // The only way to do that is to poll. pollRequest := PollSignRequest{ CeremonyID: ceremonyID, - PartyID: nil, + PartyID: nil, // We don't know our party ID yet. } pollResponse, err := DuctPollSignCeremony(host, pollRequest) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } - myPartyID := uint16(pollResponse.MyPartyID) groupID := pollResponse.GroupID threshold := pollResponse.Threshold - // Next, we need to formally join the party and get your ID + var encryptedShare string + var publicSharesHex map[string]string + var publicKeyHex string + var myPartyID uint16 + for _, s := range config.Shares { + if s.GroupID == groupID { + encryptedShare = s.EncryptedShare + publicSharesHex = s.PublicShares + publicKeyHex = s.PublicKey + myPartyID = s.MyPartyID + break + } + } + if encryptedShare == "" { + fmt.Fprintf(os.Stderr, "could not find encrypted share for group %s\n", groupID) + os.Exit(1) + } + if myPartyID == 0 { + fmt.Fprintf(os.Stderr, "could not find party ID for group %s\n", groupID) + os.Exit(1) + } + + // Next, we need to formally join the party hash := HashMessageForSanity(message, groupID) joinRequest := JoinSignRequest{ CeremonyID: ceremonyID, MessageHash: hash, - MyPartyID: pollResponse.MyPartyID, + MyPartyID: myPartyID, } // Enlist ourselves before we begin polling @@ -356,6 +385,8 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { // Now let's begin polling the server until enough parties join for { + // We need to use our actual party ID for polling now + pollRequest.PartyID = &myPartyID pollResponse, err = DuctPollSignCeremony(host, pollRequest) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err.Error()) @@ -368,28 +399,6 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { time.Sleep(time.Second) } - // Let's pull in the data from the local config: - config, err := LoadUserConfig() - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err.Error()) - os.Exit(1) - } - var encryptedShare string - var publicSharesHex map[string]string - var publicKeyHex string - for _, s := range config.Shares { - if s.GroupID == groupID { - encryptedShare = s.EncryptedShare - publicSharesHex = s.PublicShares - publicKeyHex = s.PublicKey - break - } - } - if encryptedShare == "" { - fmt.Fprintf(os.Stderr, "could not find encrypted share for group %s\n", groupID) - os.Exit(1) - } - // Let's decrypt the local share with age secretBytes, err := DecryptShareFor(encryptedShare, identityFile) if err != nil { @@ -414,6 +423,16 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { os.Exit(1) } + // Great, let's process the party members now that we're full + partyMembers := []uint16{myPartyID} + partyMembers = append(partyMembers, pollResponse.OtherParties...) + + // Create a map of party members for quick lookup + partyMemberSet := make(map[uint16]struct{}) + for _, p := range partyMembers { + partyMemberSet[p] = struct{}{} + } + // Let's make sure we have all parties' public shares setup locally publicShares := make([]*keys.PublicKeyShare, 0, len(publicSharesHex)) for k, v := range publicSharesHex { @@ -422,6 +441,9 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } + if _, ok := partyMemberSet[p16]; !ok { + continue + } rawEl, err := hex.DecodeString(v) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err.Error()) @@ -432,13 +454,14 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { fmt.Fprintf(os.Stderr, "failed to decode public share for party %d: %s\n", p16, err.Error()) os.Exit(1) } - publicShares = append(publicShares, &keys.PublicKeyShare{ID: p16, PublicKey: el}) + ps := &keys.PublicKeyShare{ + ID: p16, + PublicKey: el, + Group: dkg.Edwards25519Sha512.Group(), + } + publicShares = append(publicShares, ps) } - // Great, let's process the party members now that we're full - partyMembers := []uint16{myPartyID} - partyMembers = append(partyMembers, pollResponse.OtherParties...) - conf := &frost.Configuration{ Ciphersuite: frost.Ed25519, Threshold: threshold, @@ -452,9 +475,14 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { } myPublicKey := dkg.Edwards25519Sha512.Group().Base().Multiply(secretKey) + myPkShare := &keys.PublicKeyShare{ + ID: myPartyID, + PublicKey: myPublicKey, + Group: dkg.Edwards25519Sha512.Group(), + } myKeyShare := &keys.KeyShare{ Secret: secretKey, - PublicKeyShare: keys.PublicKeyShare{ID: myPartyID, PublicKey: myPublicKey}, + PublicKeyShare: *myPkShare, VerificationKey: groupKey, } diff --git a/client/internal/persistence.go b/client/internal/persistence.go index fc40a99..381ca14 100644 --- a/client/internal/persistence.go +++ b/client/internal/persistence.go @@ -74,13 +74,14 @@ func (cfg FreonConfig) Save() error { return encoder.Encode(cfg) } -func (cfg FreonConfig) AddShare(host, groupID, publicKey, share string, otherShares map[string]string) error { +func (cfg FreonConfig) AddShare(host, groupID, publicKey, share string, otherShares map[string]string, myPartyID uint16) error { s := Shares{ Host: host, GroupID: groupID, PublicKey: publicKey, EncryptedShare: share, PublicShares: otherShares, + MyPartyID: myPartyID, } cfg.Shares = append(cfg.Shares, s) return cfg.Save() diff --git a/client/internal/persistence_test.go b/client/internal/persistence_test.go index 4aae979..dc14e30 100644 --- a/client/internal/persistence_test.go +++ b/client/internal/persistence_test.go @@ -28,7 +28,7 @@ func TestPersistence(t *testing.T) { assert.Equal(t, cfg, loadedCfg) // Test AddShare - err = loadedCfg.AddShare("localhost", "group1", "pk1", "share1", nil) + err = loadedCfg.AddShare("localhost", "group1", "pk1", "share1", nil, 1) assert.NoError(t, err) // Load the config again to check if the share was added diff --git a/client/internal/types.go b/client/internal/types.go index 2d2f7b2..41d2f41 100644 --- a/client/internal/types.go +++ b/client/internal/types.go @@ -5,6 +5,7 @@ type Shares struct { Host string `json:"host"` GroupID string `json:"group-id"` PublicKey string `json:"public-key"` + MyPartyID uint16 `json:"my-party-id"` EncryptedShare string `json:"encrypted-share"` PublicShares map[string]string `json:"public-shares"` } diff --git a/client/internal/util.go b/client/internal/util.go index a4acadc..203e841 100644 --- a/client/internal/util.go +++ b/client/internal/util.go @@ -69,6 +69,7 @@ func AmIElected(ch []byte, me uint16, party []uint16) bool { // If you are not a member, you are not chosen return false } + slices.Sort(party) ps := uint64(len(party)) index := SelectIndex(ch, ps) return party[index] == me From 7aff708594f187539ddedbcec3f7955011a88459 Mon Sep 17 00:00:00 2001 From: Soatok Date: Sat, 30 Aug 2025 23:31:48 -0400 Subject: [PATCH 10/10] fix(test): add integration testing script This test instantiates several independent clients and a coordinator, performs the keygen and sign ceremonies, and then verifies that the signature produced is a valid Ed25519 signature for the group public key. --- .github/workflows/ci.yml | 5 +- coordinator/tests/integration_test.go | 312 ++++++++++++++++++++++++++ 2 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 coordinator/tests/integration_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d5bf54..1a35108 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,4 +34,7 @@ jobs: run: cd client/internal && go test -v -short && cd ../.. - name: Test Coordinator Internal - run: cd coordinator/internal && go test -v -short && cd .. + run: cd coordinator/internal && go test -v -short && cd ../.. + + - name: Integration Test + run: cd coordinator/tests && go test -v -short && cd ../.. diff --git a/coordinator/tests/integration_test.go b/coordinator/tests/integration_test.go new file mode 100644 index 0000000..c94e234 --- /dev/null +++ b/coordinator/tests/integration_test.go @@ -0,0 +1,312 @@ +package main_test + +import ( + "bytes" + "context" + "crypto/ed25519" + "database/sql" + "encoding/hex" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "sync" + "testing" + "time" + + "filippo.io/age" + _ "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" + "github.com/soatok/freon/coordinator/internal" + "github.com/stretchr/testify/require" +) + +var ( + clientBinPath string + coordinatorBinPath string +) + +func TestMain(m *testing.M) { + tempDir, err := os.MkdirTemp("", "freon-bins-") + if err != nil { + fmt.Printf("could not create temp dir: %v", err) + os.Exit(1) + } + defer os.RemoveAll(tempDir) + + // Build client + clientBinPath = filepath.Join(tempDir, "client") + cmdClient := exec.Command("go", "build", "-o", getExePath(clientBinPath), "../../client") + if err := cmdClient.Run(); err != nil { + fmt.Printf("could not build client: %v", err) + os.Exit(1) + } + clientBinPath = getExePath(clientBinPath) + + // Build coordinator + coordinatorBinPath = filepath.Join(tempDir, "coordinator") + cmdCoord := exec.Command("go", "build", "-o", getExePath(coordinatorBinPath), "..") + if err := cmdCoord.Run(); err != nil { + fmt.Printf("could not build coordinator: %v", err) + os.Exit(1) + } + coordinatorBinPath = getExePath(coordinatorBinPath) + + exitCode := m.Run() + + os.Exit(exitCode) +} + +// Kludge to make Windows testing succeed +func getExePath(path string) string { + if runtime.GOOS == "windows" { + return path + ".exe" + } + return path +} + +// coordinator holds the state of a coordinator instance +type coordinator struct { + db *sql.DB + proc *os.Process + hostname string +} + +// client holds the state of a client instance +type client struct { + homeDir string + ageKey *age.X25519Identity + agePubKey string + identityFile string +} + +// startCoordinator starts a new coordinator instance on a random port +func startCoordinator(t *testing.T) *coordinator { + t.Helper() + + // Create a temporary directory for the coordinator's database + dir, err := os.MkdirTemp("", "freon-coordinator-test") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(dir) }) + + dbFile := filepath.Join(dir, "database.sqlite") + db, err := sql.Open("sqlite3", dbFile) + require.NoError(t, err) + + // Ensure foreign keys + _, err = db.Exec("PRAGMA foreign_keys = ON") + require.NoError(t, err) + internal.DbEnsureTablesExist(db) + + // Find an available port + port, err := findAvailablePort() + require.NoError(t, err) + hostname := fmt.Sprintf("localhost:%d", port) + + // Create a temporary config file for the coordinator + configFile, err := os.CreateTemp("", "freon-coordinator-config-*.json") + require.NoError(t, err) + defer os.Remove(configFile.Name()) + + f, err := os.OpenFile(configFile.Name(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + require.NoError(t, err) + dbFileForJson := strings.ReplaceAll(dbFile, `\`, `\\`) + _, err = f.WriteString(fmt.Sprintf(`{"hostname": "%s", "database": "%s"}`, hostname, dbFileForJson)) + require.NoError(t, err) + f.Close() + + // Start the coordinator + cmd := exec.Command(coordinatorBinPath) + cmd.Env = append(os.Environ(), "FREON_COORDINATOR_CONFIG="+configFile.Name()) + var coordOutput bytes.Buffer + cmd.Stdout = &coordOutput + cmd.Stderr = &coordOutput + err = cmd.Start() + require.NoError(t, err) + + // Wait for the coordinator to be ready + waitForCoordinator(t, hostname, &coordOutput) + + return &coordinator{ + db: db, + proc: cmd.Process, + hostname: hostname, + } +} + +// stop stops the coordinator instance +func (c *coordinator) stop(t *testing.T) { + t.Helper() + err := c.proc.Kill() + require.NoError(t, err) + c.db.Close() +} + +// newClient creates a new client instance with its own home directory +func newClient(t *testing.T) *client { + t.Helper() + + homeDir, err := os.MkdirTemp("", "freon-client-test") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(homeDir) }) + + ageKey, err := age.GenerateX25519Identity() + require.NoError(t, err) + + identityFile := filepath.Join(homeDir, "keys.age") + err = os.WriteFile(identityFile, []byte(ageKey.String()), 0600) + require.NoError(t, err) + + return &client{ + homeDir: homeDir, + ageKey: ageKey, + agePubKey: ageKey.Recipient().String(), + identityFile: identityFile, + } +} + +// run runs a freon client command +func (c *client) run(t *testing.T, args ...string) (string, error) { + t.Helper() + + cmd := exec.Command(clientBinPath, args...) + cmd.Env = append(os.Environ(), "FREON_HOME="+c.homeDir) + + output, err := cmd.CombinedOutput() + return string(output), err +} + +// waitForCoordinator waits for the coordinator to be ready to accept connections +func waitForCoordinator(t *testing.T, host string, coordOutput *bytes.Buffer) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + for { + select { + case <-ctx.Done(): + t.Logf("Coordinator logs:\n%s", coordOutput.String()) + t.Fatal("coordinator did not start in time") + default: + conn, err := net.DialTimeout("tcp", host, 1*time.Second) + if err == nil { + conn.Close() + return + } + time.Sleep(100 * time.Millisecond) + } + } +} + +// findAvailablePort finds an available TCP port on the local machine +func findAvailablePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + +func TestIntegration(t *testing.T) { + // Start coordinator + coord := startCoordinator(t) + defer coord.stop(t) + + // Create clients + numClients := 4 + threshold := 3 + clients := make([]*client, numClients) + for i := 0; i < numClients; i++ { + clients[i] = newClient(t) + } + + // DKG ceremony + var groupID string + t.Run("DKG", func(t *testing.T) { + // Client 0 creates the DKG + output, err := clients[0].run(t, "keygen", "create", "-h", coord.hostname, "-n", fmt.Sprintf("%d", numClients), "-t", fmt.Sprintf("%d", threshold)) + require.NoError(t, err, output) + re := regexp.MustCompile(`Group ID:\s*(\S+)`) + matches := re.FindStringSubmatch(output) + require.Len(t, matches, 2) + groupID = matches[1] + + // Other clients join the DKG + var wg sync.WaitGroup + for i := 0; i < numClients; i++ { + wg.Add(1) + time.Sleep(100 * time.Millisecond) + go func(i int) { + defer wg.Done() + out, err := clients[i].run(t, "keygen", "join", "-h", coord.hostname, "-g", groupID, "-r", clients[i].agePubKey) + require.NoError(t, err, out) + }(i) + } + wg.Wait() + }) + + // Signing ceremony + t.Run("SignAndVerify", func(t *testing.T) { + message := "test message" + messageFile := filepath.Join(clients[0].homeDir, "message.txt") + err := os.WriteFile(messageFile, []byte(message), 0644) + require.NoError(t, err) + + // Client 0 creates the signing ceremony + output, err := clients[0].run(t, "sign", "create", "-h", coord.hostname, "-g", groupID, messageFile) + require.NoError(t, err, output) + re := regexp.MustCompile(`created!\s*(\S+)`) + matches := re.FindStringSubmatch(output) + require.Len(t, matches, 2) + ceremonyID := matches[1] + + // First `threshold` clients join the signing ceremony + var wg sync.WaitGroup + for i := 0; i < threshold; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + output, err := clients[i].run(t, "sign", "join", "-h", coord.hostname, "-c", ceremonyID, "-i", clients[i].identityFile, messageFile) + require.NoError(t, err, output) + }(i) + } + wg.Wait() + + // Get the signature + output, err = clients[0].run(t, "sign", "get", "-h", coord.hostname, "-c", ceremonyID) + require.NoError(t, err, output) + + // Extract signature from output + re = regexp.MustCompile(`Signature:\s*(\S+)`) + matches = re.FindStringSubmatch(output) + require.Len(t, matches, 2) + sigHex := matches[1] + signature, err := hex.DecodeString(sigHex) + require.NoError(t, err) + + // Get the group public key + output, err = clients[0].run(t, "keygen", "list") + require.NoError(t, err, output) + re = regexp.MustCompile(groupID + `\s+([a-f0-9]+)`) + matches = re.FindStringSubmatch(output) + require.Len(t, matches, 2, "could not find public key for group in client 0 output") + pubKeyHex := matches[1] + pubKey, err := hex.DecodeString(pubKeyHex) + require.NoError(t, err) + + // Verify the Ed25519 signature + verified := ed25519.Verify(pubKey, []byte(message), signature) + require.True(t, verified, "Ed25519 signature verification failed") + }) +}