diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5934a3a..1a35108 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 .. @@ -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/.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/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/duct.go b/client/internal/duct.go index ee2c8d9..1fc517d 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 @@ -48,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": @@ -60,10 +66,14 @@ 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": + u.Path = "/sign/get" case "TerminateSignCeremony": - u.Path = "/sign/terminate" + u.Path = "/terminate" default: return "", fmt.Errorf("unknown feature: %s", feature) } @@ -88,6 +98,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 { @@ -113,6 +131,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 { @@ -138,6 +164,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 { @@ -163,6 +197,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 { @@ -188,6 +229,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 { @@ -212,6 +260,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 { @@ -239,6 +294,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 { @@ -247,6 +309,43 @@ func DuctSignList(host string, req ListSignRequest) (ListSignResponse, error) { return response, nil } +// Get keygen protocol messages +func DuctKeygenGetMessages(host string, groupID string, myPartyID uint16, 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, + MyPartyID: myPartyID, + 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 + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return KeyGenMessageResponse{}, err + } + return response, nil +} + // Send keygen protocol messages func DuctKeygenProtocolMessage(host string, req KeyGenMessageRequest) (KeyGenMessageResponse, error) { err := InitializeHttpClient() @@ -264,8 +363,55 @@ 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) + 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, myPartyID uint16, 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, + MyPartyID: myPartyID, + 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 + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return SignMessageResponse{}, err + } return response, nil } @@ -286,8 +432,18 @@ 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) + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return SignMessageResponse{}, err + } return response, nil } @@ -304,8 +460,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 { @@ -321,6 +489,88 @@ 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) { + 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() + + 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 { + return GetSignResponse{}, err + } + 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() + + 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 { + 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 40546be..337cca3 100644 --- a/client/internal/freon.go +++ b/client/internal/freon.go @@ -8,12 +8,10 @@ import ( "os" "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. @@ -23,10 +21,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 -// 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 @@ -66,244 +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 - } - 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) - 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 - } - - 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 - } - 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) - 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 - } - - 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 - } - } -} - -// 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) + return 0, 0, 0, nil, err } + ceremonyHash = sha512.New384() + ceremonyHash.Write(ceremonyKeyGen) - // 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 + 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)) + 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 { + return nil, nil, fmt.Errorf("failed to start dkg: %w", err) } - set := party.NewIDSlice(partyMembers) - state, output, err := frost.NewKeygenState(myPartyID, set, threshold, timeout) + + r1Message := participant.Start() + r1Bytes := r1Message.Encode() + _, err = DuctKeygenProtocolMessage(host, KeyGenMessageRequest{ + GroupID: groupID, + Message: hex.EncodeToString(r1Bytes), + MyPartyID: myPartyID, + }) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) + return nil, nil, fmt.Errorf("failed to send r1 message: %w", err) } - // Use a goroutine for processing messages (which can append more messages) - lastMessageIdSeen = 0 - ceremonyHash = sha512.New384() - ceremonyHash.Write(ceremonyKeyGen) - go ProcessKeygenMessages(messagesIn, state, host, groupID, joinResponse.MyPartyID) + 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 { + 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 + } + } + } + lastMessageIdSeen = resp.LatestMessageID + time.Sleep(time.Second) + } + var r1Data []*dkg.Round1Data + for _, m := range r1Messages { + r1Data = append(r1Data, m) + } + return participant, r1Data, nil +} - err = state.WaitForError() +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 { - fmt.Fprintf(os.Stderr, "%s", err.Error()) - os.Exit(1) + 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: myPartyID, + }) + if err != nil { + return nil, fmt.Errorf("failed to send r2 message: %w", err) + } } - // If we've gotten here without an error, a group key has been established! - public := output.Public - groupKey := hex.EncodeToString(public.GroupKey.ToEd25519()) - plaintextShare, err := output.SecretKey.MarshalBinary() + myR2Messages := make(map[uint16]*dkg.Round2Data) + for len(myR2Messages) < int(partySize)-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 { + continue + } + 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 = resp.LatestMessageID + time.Sleep(time.Second) + } + var r2Data []*dkg.Round2Data + for _, m := range myR2Messages { + r2Data = append(r2Data, m) + } + return r2Data, nil +} + +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) + } + + var allCommitments [][]*ecc.Element + for _, d := range r1Data { + allCommitments = append(allCommitments, d.Commitment) } - secretShare, err := EncryptShare(recipient, plaintextShare) + + 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) + } + + 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 public.Shares { - i := Uint16ToHexBE(uint16(index)) - shh := hex.EncodeToString(sh.BytesEd25519()) - publicShares[i] = shh + return err } - // Okay, finally, we add the share data to the local config - err = config.AddShare(host, groupID, groupKey, secretShare, publicShares) + err = config.AddShare(host, groupID, groupKeyHex, encryptedShare, publicShares, myPartyID) 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, partySize, 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 @@ -326,32 +319,61 @@ 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", err.Error()) + fmt.Fprintf(os.Stderr, "%s\n", 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 + 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 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 { @@ -363,9 +385,11 @@ 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", err.Error()) + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } others := uint16(len(pollResponse.OtherParties)) @@ -375,120 +399,224 @@ 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() + // 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 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) + 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) } - rawPk, err := hex.DecodeString(publicKeyHex) + + // Let's decode the public key and public shares + groupKeyBytes, err := hex.DecodeString(publicKeyHex) if err != nil { - fmt.Fprintf(os.Stderr, "could not decode encrypted share for group %s", groupID) + 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 deserialize the public shares - publicShares := make(map[party.ID]*ristretto.Element, len(publicSharesHex)) + // 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 { p16, err := HexBEToUint16(k) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) + 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", err.Error()) + 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) } - pid := party.ID(p16) - var el *ristretto.Element - el.SetCanonicalBytes(rawEl) - publicShares[pid] = el + ps := &keys.PublicKeyShare{ + ID: p16, + PublicKey: el, + Group: dkg.Edwards25519Sha512.Group(), + } + publicShares = append(publicShares, ps) } - // Let's decrypt the local share with age - secretBytes, err := DecryptShareFor(encryptedShare, identityFile) + 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) + } + + myPublicKey := dkg.Edwards25519Sha512.Group().Base().Multiply(secretKey) + myPkShare := &keys.PublicKeyShare{ + ID: myPartyID, + PublicKey: myPublicKey, + Group: dkg.Edwards25519Sha512.Group(), + } + myKeyShare := &keys.KeyShare{ + Secret: secretKey, + PublicKeyShare: *myPkShare, + VerificationKey: groupKey, + } + + 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) } - var secret eddsa.SecretShare - err = secret.UnmarshalBinary(secretBytes) + + // 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, "%s", err.Error()) + fmt.Fprintf(os.Stderr, "failed to send commitment: %s\n", err.Error()) os.Exit(1) } - // 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)) + // 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.Fprintf(os.Stderr, "failed to poll for commitments: %s\n", err.Error()) + os.Exit(1) + } + 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 + } + } + } + lastMessageIdSeen = resp.LatestMessageID + time.Sleep(time.Second) } - 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, + var commitmentList []*frost.Commitment + for _, c := range commitments { + commitmentList = append(commitmentList, c) } - // Initilize the Sign ceremony state - state, signOutput, err := frost.NewSignState(set, &secret, &publicData, message, timeout) + // Round 2: Sign + sigShare, err := signer.Sign(message, commitmentList) if err != nil { - fmt.Fprintf(os.Stderr, "%s", err.Error()) + 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) } - // Use a goroutine for processing messages (which can append more messages) - lastMessageIdSeen = 0 - ceremonyHash = sha512.New384() - ceremonyHash.Write(ceremonySign) - go ProcessSignMessages(messagesIn, state, host, groupID, uint16(myPartyID)) + // 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.Fprintf(os.Stderr, "failed to poll for signature shares: %s\n", err.Error()) + os.Exit(1) + } + for _, msgStr := range resp.Messages { + msgBytes, err := hex.DecodeString(msgStr) + if err != nil { + 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 + } + } + } + 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, "%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 { - 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) @@ -536,6 +664,30 @@ func ListSign(host, groupID string, limit, offset int64) { os.Exit(0) } -func TerminateSignCeremony(ceremonyID string) { - // TODO - soatok +// 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) +} + +// Tell the coordinator to pull the plug on a signing ceremony +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/persistence.go b/client/internal/persistence.go index b6d1c97..381ca14 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 } @@ -67,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 e68de6e..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"` } @@ -120,6 +121,22 @@ type SignFinalRequest struct { Signature string `json:"signature"` } +type GetSignRequest struct { + CeremonyID string `json:"ceremony"` +} + +type GetSignResponse struct { + Signature string `json:"signature"` +} + +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/internal/util.go b/client/internal/util.go index ad87534..203e841 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 @@ -71,17 +69,8 @@ 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 } - -// 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/client/main.go b/client/main.go index a2151a5..0dbe77e 100644 --- a/client/main.go +++ b/client/main.go @@ -19,11 +19,7 @@ func main() { os.Exit(1) } - 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. @@ -66,6 +62,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) @@ -287,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 ...` @@ -363,20 +361,70 @@ func FreonSignList(args []string) { internal.ListSign(*host, *groupID, *limit, *offset) } -// CMD: `freon sign terminate ...` +// 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 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/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..1796228 100644 --- a/coordinator/go.mod +++ b/coordinator/go.mod @@ -2,8 +2,6 @@ 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 ( @@ -12,7 +10,6 @@ require ( ) 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 @@ -20,5 +17,3 @@ require ( 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 74c28ac..2c58eb9 100644 --- a/coordinator/go.sum +++ b/coordinator/go.sum @@ -1,27 +1,13 @@ -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/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= 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= @@ -32,6 +18,5 @@ 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/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/database.go b/coordinator/internal/database.go index 03308ed..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 @@ -522,3 +529,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/keygen.go b/coordinator/internal/keygen.go index 0e81565..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 } @@ -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/internal/sign.go b/coordinator/internal/sign.go index 13c0205..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 @@ -71,7 +74,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 } @@ -105,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 { @@ -135,9 +140,23 @@ 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") } 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..5c90193 100644 --- a/coordinator/requesttypes.go +++ b/coordinator/requesttypes.go @@ -106,6 +106,18 @@ type SignFinalRequest struct { Signature string `json:"signature"` } +type GetSignRequest struct { + CeremonyID string `json:"ceremony"` +} + +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 28ecc76..dafa7b5 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" @@ -13,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 @@ -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,10 +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 @@ -94,6 +95,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) @@ -173,6 +178,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 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: latestID, + 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 @@ -186,39 +223,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 @@ -271,30 +307,29 @@ 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) } -// Send a message to a signing ceremony -func sendSign(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 } - msg, err := hex.DecodeString(req.Message) - if err != nil { - sendError(w, err) - return - } inbox, err := internal.GetSignMessagesSince(db, req.CeremonyID, req.LastSeen) if err != nil { sendError(w, err) @@ -310,24 +345,59 @@ func sendSign(w http.ResponseWriter, r *http.Request) { messages = append(messages, hex.EncodeToString(m.Message)) } - record, err := internal.AddSignMessage(db, req.CeremonyID, req.MyPartyID, msg) + // 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 + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + sendError(w, err) + return + } + msg, err := hex.DecodeString(req.Message) if err != nil { sendError(w, err) return } - // If no other inserts occured, we can do this - if record.DbId-max == 1 { - max = record.DbId + // First, add the new message to the database. + _, err = internal.AddSignMessage(db, req.CeremonyID, req.MyPartyID, msg) + if err != nil { + sendError(w, err) + return } - // Let's queue up the messages - response := KeyGenMessageResponse{ - LatestMessageID: max, + // 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 + } + + // 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 + } + } + + 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 @@ -413,6 +483,43 @@ 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 + 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) } 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") + }) +} 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 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..98b041d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,11 +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/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=