From a0e8fed891175ad0634210a85b906665f1446039 Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 01:17:26 -0400 Subject: [PATCH 01/10] add unit tests --- client/go.sum | 5 +- client/internal/crypto_test.go | 114 +++++++++++++ client/internal/freon.go | 4 +- client/internal/util.go | 4 +- client/internal/util_test.go | 18 +++ coordinator/go.mod | 12 +- coordinator/go.sum | 3 +- coordinator/internal/config.go | 10 +- coordinator/internal/config_test.go | 45 ++++++ coordinator/internal/database.go | 15 +- coordinator/internal/database_test.go | 224 ++++++++++++++++++++++++++ coordinator/internal/keygen_test.go | 85 ++++++++++ coordinator/internal/sign_test.go | 114 +++++++++++++ coordinator/internal/util_test.go | 21 +++ go.work | 6 + go.work.sum | 5 + 16 files changed, 661 insertions(+), 24 deletions(-) create mode 100644 client/internal/crypto_test.go create mode 100644 coordinator/internal/config_test.go create mode 100644 coordinator/internal/database_test.go create mode 100644 coordinator/internal/keygen_test.go create mode 100644 coordinator/internal/sign_test.go create mode 100644 coordinator/internal/util_test.go create mode 100644 go.work create mode 100644 go.work.sum diff --git a/client/go.sum b/client/go.sum index a679e38..539f720 100644 --- a/client/go.sum +++ b/client/go.sum @@ -20,14 +20,11 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO 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.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 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.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= diff --git a/client/internal/crypto_test.go b/client/internal/crypto_test.go new file mode 100644 index 0000000..e5422c6 --- /dev/null +++ b/client/internal/crypto_test.go @@ -0,0 +1,114 @@ +package internal_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "filippo.io/age" + "github.com/soatok/freon/client/internal" +) + +func TestEncryptDecryptShare(t *testing.T) { + identity, err := age.GenerateX25519Identity() + if err != nil { + t.Fatal(err) + } + + recipient := identity.Recipient().String() + share := []byte("test share") + + encrypted, err := internal.EncryptShare(recipient, share) + if err != nil { + t.Fatal(err) + } + + decrypted, err := internal.DecryptShare(encrypted, identity) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(share, decrypted) { + t.Fatal("decrypted share does not match original share") + } +} + +func TestParseAgeIdentityFile(t *testing.T) { + identity, err := age.GenerateX25519Identity() + if err != nil { + t.Fatal(err) + } + + tempDir := t.TempDir() + identityFile := filepath.Join(tempDir, "key.txt") + + f, err := os.Create(identityFile) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + _, err = f.WriteString(identity.String()) + if err != nil { + t.Fatal(err) + } + f.Close() + + identities, err := internal.ParseAgeIdentityFile(identityFile) + if err != nil { + t.Fatal(err) + } + + if len(identities) != 1 { + t.Fatalf("expected 1 identity, got %d", len(identities)) + } + + parsedIdentity, ok := identities[0].(*age.X25519Identity) + if !ok { + t.Fatal("identity is not of type X25519Identity") + } + + if parsedIdentity.String() != identity.String() { + t.Fatal("parsed identity does not match original identity") + } +} + +func TestDecryptShareFor(t *testing.T) { + identity, err := age.GenerateX25519Identity() + if err != nil { + t.Fatal(err) + } + + recipient := identity.Recipient().String() + share := []byte("test share") + + encrypted, err := internal.EncryptShare(recipient, share) + if err != nil { + t.Fatal(err) + } + + tempDir := t.TempDir() + identityFile := filepath.Join(tempDir, "key.txt") + + f, err := os.Create(identityFile) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + _, err = f.WriteString(identity.String()) + if err != nil { + t.Fatal(err) + } + f.Close() + + decrypted, err := internal.DecryptShareFor(encrypted, identityFile) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(share, decrypted) { + t.Fatal("decrypted share does not match original share") + } +} diff --git a/client/internal/freon.go b/client/internal/freon.go index 19d4bd8..40546be 100644 --- a/client/internal/freon.go +++ b/client/internal/freon.go @@ -277,7 +277,7 @@ 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 { - i := uint16ToHexBE(uint16(index)) + i := Uint16ToHexBE(uint16(index)) shh := hex.EncodeToString(sh.BytesEd25519()) publicShares[i] = shh } @@ -405,7 +405,7 @@ func JoinSignCeremony(ceremonyID, host, identityFile string, message []byte) { // Let's deserialize the public shares publicShares := make(map[party.ID]*ristretto.Element, len(publicSharesHex)) for k, v := range publicSharesHex { - p16, err := hexBEToUint16(k) + p16, err := HexBEToUint16(k) if err != nil { fmt.Fprintf(os.Stderr, "%s", err.Error()) os.Exit(1) diff --git a/client/internal/util.go b/client/internal/util.go index d990091..ad87534 100644 --- a/client/internal/util.go +++ b/client/internal/util.go @@ -20,12 +20,12 @@ func HashMessageForSanity(data []byte, groupID string) string { return hex.EncodeToString(mac.Sum(nil)) } -func uint16ToHexBE(n uint16) string { +func Uint16ToHexBE(n uint16) string { bytes := []byte{byte(n >> 8), byte(n)} return hex.EncodeToString(bytes) } -func hexBEToUint16(s string) (uint16, error) { +func HexBEToUint16(s string) (uint16, error) { bytes, err := hex.DecodeString(s) if err != nil { return 0, err diff --git a/client/internal/util_test.go b/client/internal/util_test.go index 5b1501b..19afe0e 100644 --- a/client/internal/util_test.go +++ b/client/internal/util_test.go @@ -113,3 +113,21 @@ func TestPartyToUint16(t *testing.T) { 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) + assert.Equal(t, uint16(256), val) + + val, err = internal.HexBEToUint16("ffff") + assert.NoError(t, err) + assert.Equal(t, uint16(65535), val) +} + +func TestUint16ToHexBE(t *testing.T) { + hex := internal.Uint16ToHexBE(256) + assert.Equal(t, "0100", hex) + + hex = internal.Uint16ToHexBE(65535) + assert.Equal(t, "ffff", hex) +} diff --git a/coordinator/go.mod b/coordinator/go.mod index 572640c..81663fb 100644 --- a/coordinator/go.mod +++ b/coordinator/go.mod @@ -6,8 +6,16 @@ require github.com/taurusgroup/frost-ed25519 v0.0.0-20210707140332-5abc84a4dba7 require github.com/alexedwards/scs/v2 v2.9.0 -require github.com/mattn/go-sqlite3 v1.14.32 +require ( + github.com/mattn/go-sqlite3 v1.14.32 + github.com/stretchr/testify v1.10.0 +) -require filippo.io/edwards25519 v1.1.0 // indirect +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.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 95adcae..7504463 100644 --- a/coordinator/go.sum +++ b/coordinator/go.sum @@ -5,8 +5,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.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY= -github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -22,6 +20,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO 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= +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= diff --git a/coordinator/internal/config.go b/coordinator/internal/config.go index 5e3a525..5feb15f 100644 --- a/coordinator/internal/config.go +++ b/coordinator/internal/config.go @@ -22,13 +22,13 @@ func getConfigFile() (string, error) { // Default user config func NewServerConfig() (CoordinatorConfig, error) { - config := CoordinatorConfig{} + config := CoordinatorConfig{ + Hostname: "localhost:8462", + Database: "./database.sqlite", + } err := config.Save() if err != nil { - return CoordinatorConfig{ - Hostname: "localhost:8462", - Database: "./database.sqlite", - }, err + return CoordinatorConfig{}, err } return config, nil } diff --git a/coordinator/internal/config_test.go b/coordinator/internal/config_test.go new file mode 100644 index 0000000..731fba0 --- /dev/null +++ b/coordinator/internal/config_test.go @@ -0,0 +1,45 @@ +package internal_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/soatok/freon/coordinator/internal" + "github.com/stretchr/testify/assert" +) + +func TestServerConfig(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("HOME", tempDir) + os.Mkdir(tempDir, 0700) + + // Test NewServerConfig + config, err := internal.NewServerConfig() + assert.NoError(t, err) + assert.Equal(t, config.Hostname, "localhost:8462") + assert.Equal(t, config.Database, "./database.sqlite") + + if err := config.Save(); err != nil { + panic(err) + } + + // Check if the file was created + configPath := filepath.Join(tempDir, ".freon-coordinator.json") + _, err = os.Stat(configPath) + assert.NoError(t, err) + + // Test LoadServerConfig + loadedConfig, err := internal.LoadServerConfig() + assert.NoError(t, err) + assert.Equal(t, config, loadedConfig) + + // Test Save + loadedConfig.Hostname = "example.com:1234" + err = loadedConfig.Save() + assert.NoError(t, err) + + savedConfig, err := internal.LoadServerConfig() + assert.NoError(t, err) + assert.Equal(t, loadedConfig, savedConfig) +} diff --git a/coordinator/internal/database.go b/coordinator/internal/database.go index bd420e6..b90f31b 100644 --- a/coordinator/internal/database.go +++ b/coordinator/internal/database.go @@ -106,16 +106,17 @@ func GetGroupByID(db *sql.DB, groupID int64) (FreonGroup, error) { } defer stmt.Close() + var id int64 var uid string var threshold uint16 var participants uint16 var publicKey *string - err = stmt.QueryRow(groupID).Scan(&uid, &threshold, &participants, &publicKey) + err = stmt.QueryRow(groupID).Scan(&id, &uid, &threshold, &participants, &publicKey) if err != nil { return FreonGroup{}, err } return FreonGroup{ - DbId: groupID, + DbId: id, Uid: uid, Participants: participants, Threshold: threshold, @@ -134,7 +135,7 @@ func GetGroupParticipants(db *sql.DB, groupUid string) ([]FreonParticipant, erro p.state FROM keygroups g JOIN participants p ON p.groupid = g.id - WHERE group.uid = ? + WHERE g.uid = ? `) if err != nil { return nil, err @@ -185,7 +186,7 @@ func GetParticipantID(db *sql.DB, groupUid string, myPartyID uint16) (int64, err defer stmt.Close() var id int64 - err = stmt.QueryRow(groupUid).Scan(&id) + err = stmt.QueryRow(groupUid, myPartyID).Scan(&id) if err != nil { return 0, err } @@ -246,7 +247,7 @@ func GetRecentCeremonies(db *sql.DB, groupID string, limit, offset int64) ([]Fre for rows.Next() { var ceremonyID string var hash string - var signature string + var signature *string var openssh bool var opensshnamespace string var active bool @@ -257,7 +258,7 @@ func GetRecentCeremonies(db *sql.DB, groupID string, limit, offset int64) ([]Fre Uid: ceremonyID, Active: active, Hash: hash, - Signature: &signature, + Signature: signature, OpenSSH: openssh, OpenSSHNamespace: opensshnamespace, } @@ -368,7 +369,7 @@ func GetSignMessagesSince(db *sql.DB, ceremonyUid string, lastSeen int64) ([]Fre msg.sender, msg.message FROM ceremonies c - JOIN signmsg msg ON msg.groupid = g.id + JOIN signmsg msg ON msg.ceremonyid = c.id JOIN participants p ON msg.sender = p.id WHERE c.uid = ? AND msg.id > ? `) diff --git a/coordinator/internal/database_test.go b/coordinator/internal/database_test.go new file mode 100644 index 0000000..216d1d6 --- /dev/null +++ b/coordinator/internal/database_test.go @@ -0,0 +1,224 @@ +package internal_test + +import ( + "database/sql" + "testing" + + _ "github.com/mattn/go-sqlite3" + "github.com/soatok/freon/coordinator/internal" + "github.com/stretchr/testify/assert" +) + +func setupTestDB(t *testing.T) *sql.DB { + db, err := sql.Open("sqlite3", "file::memory:?cache=shared") + assert.NoError(t, err) + t.Cleanup(func() { + db.Close() + }) + return db +} + +func TestDbEnsureTablesExist(t *testing.T) { + db := setupTestDB(t) + err := internal.DbEnsureTablesExist(db) + assert.NoError(t, err) + + // Check if tables were created + tables := []string{ + "keygroups", + "participants", + "ceremonies", + "players", + "keygenmsg", + "signmsg", + } + for _, table := range tables { + var name string + err := db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name) + assert.NoError(t, err) + assert.Equal(t, table, name) + } +} + +func TestParticipantFunctions(t *testing.T) { + db := setupTestDB(t) + err := internal.DbEnsureTablesExist(db) + assert.NoError(t, err) + + // Insert a group + group := internal.FreonGroup{ + Uid: "test_group", + Participants: 3, + Threshold: 2, + } + gid, err := internal.InsertGroup(db, group) + assert.NoError(t, err) + + // Insert a participant + p := internal.FreonParticipant{ + GroupID: gid, + Uid: "test_participant", + PartyID: 1, + State: []byte("state1"), + } + pid, err := internal.InsertParticipant(db, p) + assert.NoError(t, err) + assert.NotZero(t, pid) + p.DbId = pid + + // GetGroupParticipants + participants, err := internal.GetGroupParticipants(db, "test_group") + assert.NoError(t, err) + assert.Len(t, participants, 1) + assert.Equal(t, p.Uid, participants[0].Uid) + + // GetParticipantID + participantID, err := internal.GetParticipantID(db, "test_group", 1) + assert.NoError(t, err) + assert.Equal(t, pid, participantID) + + // UpdateParticipantState + p.State = []byte("state2") + err = internal.UpdateParticipantState(db, p) + assert.NoError(t, err) + + participants, err = internal.GetGroupParticipants(db, "test_group") + assert.NoError(t, err) + assert.Len(t, participants, 1) + assert.Equal(t, p.State, participants[0].State) +} + +func TestCeremonyAndMessageFunctions(t *testing.T) { + db := setupTestDB(t) + err := internal.DbEnsureTablesExist(db) + assert.NoError(t, err) + + // Insert a group and participant + group := internal.FreonGroup{Uid: "g", Participants: 2, Threshold: 2} + gid, err := internal.InsertGroup(db, group) + assert.NoError(t, err) + p := internal.FreonParticipant{GroupID: gid, Uid: "p", PartyID: 1} + pid, err := internal.InsertParticipant(db, p) + assert.NoError(t, err) + + // Insert a ceremony + c := internal.FreonCeremonies{ + GroupID: gid, + Uid: "c", + Active: true, + Hash: "hash", + } + cid, err := internal.InsertCeremony(db, c) + assert.NoError(t, err) + c.DbId = cid + + // GetCeremonyData + cData, err := internal.GetCeremonyData(db, "c") + assert.NoError(t, err) + assert.Equal(t, c.Uid, cData.Uid) + + // Insert a player + player := internal.FreonPlayers{ + CeremonyID: cid, + ParticipantID: pid, + } + _, err = internal.InsertPlayer(db, player) + assert.NoError(t, err) + + // GetCeremonyPlayers + players, err := internal.GetCeremonyPlayers(db, "c") + assert.NoError(t, err) + assert.Len(t, players, 1) + assert.Equal(t, pid, players[0].ParticipantID) + + // GetRecentCeremonies + recent, err := internal.GetRecentCeremonies(db, "g", 10, 0) + assert.NoError(t, err) + assert.Len(t, recent, 1) + assert.Equal(t, "c", recent[0].Uid) + + // FinalizeSignature + err = internal.FinalizeSignature(db, c, "sig") + assert.NoError(t, err) + cData, err = internal.GetCeremonyData(db, "c") + assert.NoError(t, err) + assert.False(t, cData.Active) + assert.Equal(t, "sig", *cData.Signature) + + // InsertKeygenMessage + km := internal.FreonKeygenMessage{ + GroupID: gid, + Sender: pid, + Message: []byte("keygen message"), + } + kmid, err := internal.InsertKeygenMessage(db, km) + assert.NoError(t, err) + assert.NotZero(t, kmid) + + // GetKeygenMessagesSince + kms, err := internal.GetKeygenMessagesSince(db, "g", 0) + assert.NoError(t, err) + assert.Len(t, kms, 1) + assert.Equal(t, km.Message, kms[0].Message) + + // InsertSignMessage + sm := internal.FreonSignMessage{ + CeremonyID: cid, + Sender: pid, + Message: []byte("sign message"), + } + smid, err := internal.InsertSignMessage(db, sm) + assert.NoError(t, err) + assert.NotZero(t, smid) + + // GetSignMessagesSince + sms, err := internal.GetSignMessagesSince(db, "c", 0) + assert.NoError(t, err) + assert.Len(t, sms, 1) + assert.Equal(t, sm.Message, sms[0].Message) +} + +func TestGroupFunctions(t *testing.T) { + db := setupTestDB(t) + err := internal.DbEnsureTablesExist(db) + assert.NoError(t, err) + + // Insert a group + group := internal.FreonGroup{ + Uid: "test_group", + Participants: 3, + Threshold: 2, + } + id, err := internal.InsertGroup(db, group) + assert.NoError(t, err) + assert.NotZero(t, id) + group.DbId = id + + // GetGroupRowId + rowId, err := internal.GetGroupRowId(db, "test_group") + assert.NoError(t, err) + assert.Equal(t, id, int64(rowId)) + + // GetGroupData + groupData, err := internal.GetGroupData(db, "test_group") + assert.NoError(t, err) + assert.Equal(t, group.Uid, groupData.Uid) + assert.Equal(t, group.Participants, groupData.Participants) + assert.Equal(t, group.Threshold, groupData.Threshold) + + // GetGroupByID + groupByID, err := internal.GetGroupByID(db, id) + assert.NoError(t, err) + assert.Equal(t, group.Uid, groupByID.Uid) + + // FinalizeGroup + publicKey := "test_public_key" + group.PublicKey = &publicKey + err = internal.FinalizeGroup(db, group) + assert.NoError(t, err) + + finalizedGroup, err := internal.GetGroupData(db, "test_group") + assert.NoError(t, err) + assert.NotNil(t, finalizedGroup.PublicKey) + assert.Equal(t, publicKey, *finalizedGroup.PublicKey) +} diff --git a/coordinator/internal/keygen_test.go b/coordinator/internal/keygen_test.go new file mode 100644 index 0000000..e83623d --- /dev/null +++ b/coordinator/internal/keygen_test.go @@ -0,0 +1,85 @@ +package internal_test + +import ( + "database/sql" + "testing" + + _ "github.com/mattn/go-sqlite3" + "github.com/soatok/freon/coordinator/internal" + "github.com/stretchr/testify/assert" +) + +func setupTestDBForKeygen(t *testing.T) *sql.DB { + db, err := sql.Open("sqlite3", "file::memory:?cache=shared") + assert.NoError(t, err) + err = internal.DbEnsureTablesExist(db) + assert.NoError(t, err) + t.Cleanup(func() { + db.Close() + }) + return db +} + +func TestNewKeyGroup(t *testing.T) { + db := setupTestDBForKeygen(t) + uid, err := internal.NewKeyGroup(db, 3, 2) + assert.NoError(t, err) + assert.NotEmpty(t, uid) + + group, err := internal.GetGroupData(db, uid) + assert.NoError(t, err) + assert.Equal(t, uint16(3), group.Participants) + assert.Equal(t, uint16(2), group.Threshold) +} + +func TestAddParticipant(t *testing.T) { + db := setupTestDBForKeygen(t) + uid, err := internal.NewKeyGroup(db, 2, 2) + assert.NoError(t, err) + + p1, err := internal.AddParticipant(db, uid) + assert.NoError(t, err) + assert.Equal(t, uint16(1), p1.PartyID) + + p2, err := internal.AddParticipant(db, uid) + assert.NoError(t, err) + assert.Equal(t, uint16(2), p2.PartyID) + + // Group is full + _, err = internal.AddParticipant(db, uid) + assert.Error(t, err) +} + +func TestAddKeyGenMessage(t *testing.T) { + db := setupTestDBForKeygen(t) + g_uid, err := internal.NewKeyGroup(db, 2, 2) + assert.NoError(t, err) + p, err := internal.AddParticipant(db, g_uid) + assert.NoError(t, err) + + msg, err := internal.AddKeyGenMessage(db, g_uid, p.PartyID, []byte("test message")) + assert.NoError(t, err) + assert.NotZero(t, msg.DbId) + + msgs, err := internal.GetKeygenMessagesSince(db, g_uid, 0) + assert.NoError(t, err) + assert.Len(t, msgs, 1) + assert.Equal(t, []byte("test message"), msgs[0].Message) +} + +func TestSetGroupPublicKey(t *testing.T) { + db := setupTestDBForKeygen(t) + g_uid, err := internal.NewKeyGroup(db, 2, 2) + assert.NoError(t, err) + + err = internal.SetGroupPublicKey(db, g_uid, "test_pk") + assert.NoError(t, err) + + group, err := internal.GetGroupData(db, g_uid) + assert.NoError(t, err) + assert.Equal(t, "test_pk", *group.PublicKey) + + // Cannot set it again + err = internal.SetGroupPublicKey(db, g_uid, "test_pk_2") + assert.Error(t, err) +} diff --git a/coordinator/internal/sign_test.go b/coordinator/internal/sign_test.go new file mode 100644 index 0000000..cde8660 --- /dev/null +++ b/coordinator/internal/sign_test.go @@ -0,0 +1,114 @@ +package internal_test + +import ( + "database/sql" + "testing" + + _ "github.com/mattn/go-sqlite3" + "github.com/soatok/freon/coordinator/internal" + "github.com/stretchr/testify/assert" +) + +func setupTestDBForSign(t *testing.T) *sql.DB { + db, err := sql.Open("sqlite3", "file::memory:?cache=shared") + assert.NoError(t, err) + err = internal.DbEnsureTablesExist(db) + assert.NoError(t, err) + t.Cleanup(func() { + db.Close() + }) + return db +} + +func TestNewSignGroup(t *testing.T) { + db := setupTestDBForSign(t) + g_uid, err := internal.NewKeyGroup(db, 2, 2) + assert.NoError(t, err) + + c_uid, err := internal.NewSignGroup(db, g_uid, "hash", false, "") + assert.NoError(t, err) + assert.NotEmpty(t, c_uid) + + c, err := internal.GetCeremonyData(db, c_uid) + assert.NoError(t, err) + assert.Equal(t, "hash", c.Hash) +} + +func TestJoinSignCeremony(t *testing.T) { + db := setupTestDBForSign(t) + g_uid, err := internal.NewKeyGroup(db, 2, 2) + assert.NoError(t, err) + p, err := internal.AddParticipant(db, g_uid) + assert.NoError(t, err) + c_uid, err := internal.NewSignGroup(db, g_uid, "hash", false, "") + assert.NoError(t, err) + + pid, err := internal.JoinSignCeremony(db, c_uid, "hash", p.PartyID) + assert.NoError(t, err) + assert.Equal(t, p.DbId, pid) + + // Wrong hash + _, err = internal.JoinSignCeremony(db, c_uid, "wrong_hash", p.PartyID) + assert.Error(t, err) +} + +func TestPollSignCeremony(t *testing.T) { + db := setupTestDBForSign(t) + g_uid, err := internal.NewKeyGroup(db, 2, 2) + assert.NoError(t, err) + p1, err := internal.AddParticipant(db, g_uid) + assert.NoError(t, err) + p2, err := internal.AddParticipant(db, g_uid) + assert.NoError(t, err) + c_uid, err := internal.NewSignGroup(db, g_uid, "hash", false, "") + assert.NoError(t, err) + _, err = internal.JoinSignCeremony(db, c_uid, "hash", p1.PartyID) + assert.NoError(t, err) + _, err = internal.JoinSignCeremony(db, c_uid, "hash", p2.PartyID) + assert.NoError(t, err) + + poll, err := internal.PollSignCeremony(db, c_uid, p1.PartyID) + assert.NoError(t, err) + assert.Equal(t, g_uid, poll.GroupID) + // This is not correct, JoinSignCeremony does not add players + // assert.Len(t, poll.OtherParties, 1) + // assert.Equal(t, p2.PartyID, poll.OtherParties[0]) +} + +func TestAddSignMessage(t *testing.T) { + db := setupTestDBForSign(t) + g_uid, err := internal.NewKeyGroup(db, 2, 2) + assert.NoError(t, err) + p, err := internal.AddParticipant(db, g_uid) + assert.NoError(t, err) + c_uid, err := internal.NewSignGroup(db, g_uid, "hash", false, "") + assert.NoError(t, err) + + msg, err := internal.AddSignMessage(db, c_uid, p.PartyID, []byte("test message")) + assert.NoError(t, err) + assert.NotZero(t, msg.DbId) + + msgs, err := internal.GetSignMessagesSince(db, c_uid, 0) + assert.NoError(t, err) + assert.Len(t, msgs, 1) + assert.Equal(t, []byte("test message"), msgs[0].Message) +} + +func TestSetSignature(t *testing.T) { + db := setupTestDBForSign(t) + g_uid, err := internal.NewKeyGroup(db, 2, 2) + assert.NoError(t, err) + c_uid, err := internal.NewSignGroup(db, g_uid, "hash", false, "") + assert.NoError(t, err) + + err = internal.SetSignature(db, c_uid, "sig") + assert.NoError(t, err) + + c, err := internal.GetCeremonyData(db, c_uid) + assert.NoError(t, err) + assert.Equal(t, "sig", *c.Signature) + + // Cannot set it again + err = internal.SetSignature(db, c_uid, "sig2") + assert.Error(t, err) +} diff --git a/coordinator/internal/util_test.go b/coordinator/internal/util_test.go new file mode 100644 index 0000000..f8664f5 --- /dev/null +++ b/coordinator/internal/util_test.go @@ -0,0 +1,21 @@ +package internal_test + +import ( + "testing" + + "github.com/soatok/freon/coordinator/internal" + "github.com/stretchr/testify/assert" +) + +func TestUniqueID(t *testing.T) { + id1, err := internal.UniqueID() + assert.NoError(t, err) + assert.Len(t, id1, 48) + + id2, err := internal.UniqueID() + assert.NoError(t, err) + assert.Len(t, id2, 48) + + // These should never be equal + assert.NotEqual(t, id1, id2) +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..f48171d --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.25 + +use ( + ./client + ./coordinator +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..6205997 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,5 @@ +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +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= From dd23e922fd23b40369b78f98b396403c352a1f06 Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 01:36:17 -0400 Subject: [PATCH 02/10] replace sqlite driver to not requite CGO --- README.md | 2 +- coordinator/go.mod | 5 ++++- coordinator/go.sum | 10 ++++++++++ coordinator/internal/database_test.go | 3 ++- coordinator/internal/keygen_test.go | 3 ++- coordinator/internal/sign_test.go | 3 ++- coordinator/server.go | 3 ++- go.work.sum | 8 +++++++- 8 files changed, 30 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 104db82..01b9d70 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ go install ```terminal git clone https://github.com/soatok/freon.git cd freon -CGO_ENABLED=1 go build -o coordinator ./coordinator +go build -o coordinator ./coordinator ./coordinator ``` diff --git a/coordinator/go.mod b/coordinator/go.mod index 81663fb..d31e9bf 100644 --- a/coordinator/go.mod +++ b/coordinator/go.mod @@ -7,14 +7,17 @@ require github.com/taurusgroup/frost-ed25519 v0.0.0-20210707140332-5abc84a4dba7 require github.com/alexedwards/scs/v2 v2.9.0 require ( - github.com/mattn/go-sqlite3 v1.14.32 + 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/sys v0.35.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/coordinator/go.sum b/coordinator/go.sum index 7504463..74c28ac 100644 --- a/coordinator/go.sum +++ b/coordinator/go.sum @@ -7,6 +7,10 @@ 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= @@ -20,6 +24,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO 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/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= diff --git a/coordinator/internal/database_test.go b/coordinator/internal/database_test.go index 216d1d6..64f6a9a 100644 --- a/coordinator/internal/database_test.go +++ b/coordinator/internal/database_test.go @@ -4,7 +4,8 @@ import ( "database/sql" "testing" - _ "github.com/mattn/go-sqlite3" + _ "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" "github.com/soatok/freon/coordinator/internal" "github.com/stretchr/testify/assert" ) diff --git a/coordinator/internal/keygen_test.go b/coordinator/internal/keygen_test.go index e83623d..ccb4e9f 100644 --- a/coordinator/internal/keygen_test.go +++ b/coordinator/internal/keygen_test.go @@ -4,7 +4,8 @@ import ( "database/sql" "testing" - _ "github.com/mattn/go-sqlite3" + _ "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" "github.com/soatok/freon/coordinator/internal" "github.com/stretchr/testify/assert" ) diff --git a/coordinator/internal/sign_test.go b/coordinator/internal/sign_test.go index cde8660..8f66097 100644 --- a/coordinator/internal/sign_test.go +++ b/coordinator/internal/sign_test.go @@ -4,7 +4,8 @@ import ( "database/sql" "testing" - _ "github.com/mattn/go-sqlite3" + _ "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" "github.com/soatok/freon/coordinator/internal" "github.com/stretchr/testify/assert" ) diff --git a/coordinator/server.go b/coordinator/server.go index 58e0c93..28ecc76 100644 --- a/coordinator/server.go +++ b/coordinator/server.go @@ -10,7 +10,8 @@ import ( "time" "github.com/alexedwards/scs/v2" - _ "github.com/mattn/go-sqlite3" // SQLite driver + _ "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" ) diff --git a/go.work.sum b/go.work.sum index 6205997..b99a4c3 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,5 +1,11 @@ +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +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= 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/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= +lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw= From 3daa4278c6e983651616cf762a554df8c5c1b4f8 Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 01:53:21 -0400 Subject: [PATCH 03/10] fix unit tests --- client/main.go | 1 - coordinator/internal/config_test.go | 2 ++ coordinator/internal/database.go | 17 ++++++++++++----- coordinator/internal/types.go | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/client/main.go b/client/main.go index 9530873..a2151a5 100644 --- a/client/main.go +++ b/client/main.go @@ -360,7 +360,6 @@ func FreonSignList(args []string) { fs.Usage() os.Exit(1) } - internal.ListSign(*host, *groupID, *limit, *offset) } diff --git a/coordinator/internal/config_test.go b/coordinator/internal/config_test.go index 731fba0..cc449be 100644 --- a/coordinator/internal/config_test.go +++ b/coordinator/internal/config_test.go @@ -12,6 +12,8 @@ import ( func TestServerConfig(t *testing.T) { tempDir := t.TempDir() t.Setenv("HOME", tempDir) + // Windows uses this environment variable instead + t.Setenv("USERPROFILE", tempDir) os.Mkdir(tempDir, 0700) // Test NewServerConfig diff --git a/coordinator/internal/database.go b/coordinator/internal/database.go index b90f31b..8ac294e 100644 --- a/coordinator/internal/database.go +++ b/coordinator/internal/database.go @@ -195,7 +195,8 @@ func GetParticipantID(db *sql.DB, groupUid string, myPartyID uint16) (int64, err func GetCeremonyData(db *sql.DB, ceremonyID string) (FreonCeremonies, error) { stmt, err := db.Prepare(`SELECT - id, groupid, active, hash, signature, openssh, opensshnamespace FROM ceremonies + id, groupid, active, hash, signature, openssh, opensshnamespace + FROM ceremonies WHERE uid = ?`) if err != nil { return FreonCeremonies{}, err @@ -208,8 +209,8 @@ func GetCeremonyData(db *sql.DB, ceremonyID string) (FreonCeremonies, error) { var hash string var signature *string var openssh bool - var opensshnamespace string - err = stmt.QueryRow(ceremonyID).Scan(&id, &groupid, &active, &signature, &openssh, &opensshnamespace) + var opensshnamespace *string + err = stmt.QueryRow(ceremonyID).Scan(&id, &groupid, &active, &hash, &signature, &openssh, &opensshnamespace) if err != nil { return FreonCeremonies{}, err } @@ -249,18 +250,24 @@ func GetRecentCeremonies(db *sql.DB, groupID string, limit, offset int64) ([]Fre var hash string var signature *string var openssh bool - var opensshnamespace string + var opensshnamespace *string var active bool if err := rows.Scan(&ceremonyID, &hash, &signature, &openssh, &opensshnamespace, &active); err != nil { return nil, err } + var ns string + if opensshnamespace == nil { + ns = "" + } else { + ns = *opensshnamespace + } row := FreonCeremonySummary{ Uid: ceremonyID, Active: active, Hash: hash, Signature: signature, OpenSSH: openssh, - OpenSSHNamespace: opensshnamespace, + OpenSSHNamespace: ns, } results = append(results, row) } diff --git a/coordinator/internal/types.go b/coordinator/internal/types.go index e646353..5c6ae1f 100644 --- a/coordinator/internal/types.go +++ b/coordinator/internal/types.go @@ -31,7 +31,7 @@ type FreonCeremonies struct { Hash string Signature *string OpenSSH bool - OpenSSHNamespace string + OpenSSHNamespace *string } // For public lists of signing ceremonies From 76c318cbf516537ef70c63785fc2e2f0dbb700dd Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 02:02:08 -0400 Subject: [PATCH 04/10] add participant to signing ceremony --- coordinator/internal/sign.go | 10 ++++++++++ coordinator/internal/sign_test.go | 7 ++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/coordinator/internal/sign.go b/coordinator/internal/sign.go index e76b099..718fa23 100644 --- a/coordinator/internal/sign.go +++ b/coordinator/internal/sign.go @@ -55,6 +55,16 @@ func JoinSignCeremony(db *sql.DB, ceremonyID, hash string, myPartyID uint16) (in if subtle.ConstantTimeCompare([]byte(ceremonyData.Hash), []byte(hash)) != 1 { return 0, errors.New("hash mismatch") } + + // Now insert the player + insert, err := db.Prepare(`INSERT INTO players (ceremonyid, participantid) VALUES (?, ?)`) + if err != nil { + return 0, err + } + _, err = insert.Exec(ceremonyData.DbId, participantId) + if err != nil { + return 0, err + } return participantId, nil } diff --git a/coordinator/internal/sign_test.go b/coordinator/internal/sign_test.go index 8f66097..103a35c 100644 --- a/coordinator/internal/sign_test.go +++ b/coordinator/internal/sign_test.go @@ -71,9 +71,10 @@ func TestPollSignCeremony(t *testing.T) { poll, err := internal.PollSignCeremony(db, c_uid, p1.PartyID) assert.NoError(t, err) assert.Equal(t, g_uid, poll.GroupID) - // This is not correct, JoinSignCeremony does not add players - // assert.Len(t, poll.OtherParties, 1) - // assert.Equal(t, p2.PartyID, poll.OtherParties[0]) + + // Ensure party 1 sees party 2 + assert.Len(t, poll.OtherParties, 1) + assert.Equal(t, p2.PartyID, poll.OtherParties[0]) } func TestAddSignMessage(t *testing.T) { From 83c1b11902ba6608d71c9adc66e0ee0676216ca1 Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 02:10:11 -0400 Subject: [PATCH 05/10] remove vestigial "state" from participants --- coordinator/internal/database.go | 50 +++++---------------------- coordinator/internal/database_test.go | 11 ------ coordinator/internal/sign.go | 1 + coordinator/internal/types.go | 1 - 4 files changed, 10 insertions(+), 53 deletions(-) diff --git a/coordinator/internal/database.go b/coordinator/internal/database.go index 8ac294e..03308ed 100644 --- a/coordinator/internal/database.go +++ b/coordinator/internal/database.go @@ -19,8 +19,7 @@ func DbEnsureTablesExist(db *sql.DB) error { id INTEGER PRIMARY KEY AUTOINCREMENT, groupid INTEGER REFERENCES keygroups(id), uid TEXT NOT NULL, - partyid INTEGER, - state TEXT + partyid INTEGER ); CREATE TABLE IF NOT EXISTS ceremonies ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -35,8 +34,7 @@ func DbEnsureTablesExist(db *sql.DB) error { CREATE TABLE IF NOT EXISTS players ( id INTEGER PRIMARY KEY AUTOINCREMENT, ceremonyid INTEGER REFERENCES ceremonies(id), - participantid INTEGER REFERENCES participants(id), - state TEXT + participantid INTEGER REFERENCES participants(id) ); CREATE TABLE IF NOT EXISTS keygenmsg ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -131,8 +129,7 @@ func GetGroupParticipants(db *sql.DB, groupUid string) ([]FreonParticipant, erro p.id, g.id AS groupid, p.uid, - p.partyid, - p.state + p.partyid FROM keygroups g JOIN participants p ON p.groupid = g.id WHERE g.uid = ? @@ -153,12 +150,7 @@ func GetGroupParticipants(db *sql.DB, groupUid string) ([]FreonParticipant, erro var groupId int64 var uid string var partyid uint16 - var stateHex string - if err := rows.Scan(&dbId, &groupId, &uid, &partyid, &stateHex); err != nil { - return nil, err - } - state, err := hex.DecodeString(stateHex) - if err != nil { + if err := rows.Scan(&dbId, &groupId, &uid, &partyid); err != nil { return nil, err } p := FreonParticipant{ @@ -166,7 +158,6 @@ func GetGroupParticipants(db *sql.DB, groupUid string) ([]FreonParticipant, erro GroupID: groupId, Uid: uid, PartyID: partyid, - State: state, } participants = append(participants, p) } @@ -280,7 +271,6 @@ func GetCeremonyPlayers(db *sql.DB, ceremonyID string) ([]FreonPlayers, error) { x.id, x.ceremonyid, x.participantid, - x.state, p.partyid FROM players x JOIN participants p ON x.participantid = p.id @@ -301,20 +291,14 @@ func GetCeremonyPlayers(db *sql.DB, ceremonyID string) ([]FreonPlayers, error) { var dbId int64 var ceremonyId int64 var participantId int64 - var stateHex string var partyId uint16 - if err := rows.Scan(&dbId, &ceremonyId, &participantId, &stateHex, &partyId); err != nil { - return nil, err - } - state, err := hex.DecodeString(stateHex) - if err != nil { + if err := rows.Scan(&dbId, &ceremonyId, &participantId, &partyId); err != nil { return nil, err } p := FreonPlayers{ DbId: dbId, CeremonyID: ceremonyId, ParticipantID: participantId, - State: state, PartyID: partyId, } players = append(players, p) @@ -447,12 +431,11 @@ func InsertCeremony(db *sql.DB, c FreonCeremonies) (int64, error) { } func InsertParticipant(db *sql.DB, p FreonParticipant) (int64, error) { - stmt, err := db.Prepare(`INSERT INTO participants (groupid, uid, partyid, state) VALUES (?, ?, ?, ?)`) + stmt, err := db.Prepare(`INSERT INTO participants (groupid, uid, partyid) VALUES (?, ?, ?)`) if err != nil { return 0, err } - stateHex := hex.EncodeToString(p.State) - res, err := stmt.Exec(p.GroupID, p.Uid, p.PartyID, stateHex) + res, err := stmt.Exec(p.GroupID, p.Uid, p.PartyID) if err != nil { return 0, err } @@ -464,12 +447,11 @@ func InsertParticipant(db *sql.DB, p FreonParticipant) (int64, error) { } func InsertPlayer(db *sql.DB, p FreonPlayers) (int64, error) { - stmt, err := db.Prepare(`INSERT INTO players (ceremonyid, participantid, state) VALUES (?, ?, ?)`) + stmt, err := db.Prepare(`INSERT INTO players (ceremonyid, participantid) VALUES (?, ?)`) if err != nil { return 0, err } - stateHex := hex.EncodeToString(p.State) - res, err := stmt.Exec(p.CeremonyID, p.ParticipantID, stateHex) + res, err := stmt.Exec(p.CeremonyID, p.ParticipantID) if err != nil { return 0, err } @@ -514,20 +496,6 @@ func InsertSignMessage(db *sql.DB, m FreonSignMessage) (int64, error) { return id, nil } -// Allows the "state" of the participants in a key group to be updated -func UpdateParticipantState(db *sql.DB, p FreonParticipant) error { - stmt, err := db.Prepare(`UPDATE participants SET state = ? WHERE id = ?`) - if err != nil { - return err - } - stateHex := hex.EncodeToString(p.State) - _, err = stmt.Exec(stateHex, p.DbId) - if err != nil { - return err - } - return nil -} - func FinalizeGroup(db *sql.DB, g FreonGroup) error { if g.PublicKey == nil { return errors.New("public key is not stored in FreonGroup struct") diff --git a/coordinator/internal/database_test.go b/coordinator/internal/database_test.go index 64f6a9a..b931863 100644 --- a/coordinator/internal/database_test.go +++ b/coordinator/internal/database_test.go @@ -60,7 +60,6 @@ func TestParticipantFunctions(t *testing.T) { GroupID: gid, Uid: "test_participant", PartyID: 1, - State: []byte("state1"), } pid, err := internal.InsertParticipant(db, p) assert.NoError(t, err) @@ -77,16 +76,6 @@ func TestParticipantFunctions(t *testing.T) { participantID, err := internal.GetParticipantID(db, "test_group", 1) assert.NoError(t, err) assert.Equal(t, pid, participantID) - - // UpdateParticipantState - p.State = []byte("state2") - err = internal.UpdateParticipantState(db, p) - assert.NoError(t, err) - - participants, err = internal.GetGroupParticipants(db, "test_group") - assert.NoError(t, err) - assert.Len(t, participants, 1) - assert.Equal(t, p.State, participants[0].State) } func TestCeremonyAndMessageFunctions(t *testing.T) { diff --git a/coordinator/internal/sign.go b/coordinator/internal/sign.go index 718fa23..13c0205 100644 --- a/coordinator/internal/sign.go +++ b/coordinator/internal/sign.go @@ -71,6 +71,7 @@ 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 } diff --git a/coordinator/internal/types.go b/coordinator/internal/types.go index 5c6ae1f..a6486d0 100644 --- a/coordinator/internal/types.go +++ b/coordinator/internal/types.go @@ -49,7 +49,6 @@ type FreonPlayers struct { CeremonyID int64 ParticipantID int64 PartyID uint16 - State []byte } type FreonSignMessage struct { From 42b03ed9ec47ef2a2c877ac892a1cdb044466f79 Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 02:14:22 -0400 Subject: [PATCH 06/10] add CI/CD integration, README badge --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ README.md | 2 ++ 2 files changed, 27 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6e056cf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: Go Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.24.5' + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v -short ./... diff --git a/README.md b/README.md index 01b9d70..c6b4e33 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ > FOSS Resists Executive Overreaching Nations +[![Build Status](https://github.com/soatok/freon/actions/workflows/ci.yml/badge.svg)](https://github.com/soatok/freon/actions/workflows/ci.yml) + FREON implements FROST ([RFC 9591](https://www.rfc-editor.org/rfc/rfc9591.html)) to allow geographically distributed teams produce digital signatures. Each share of the signing key is encrypted locally using [age](https://github.com/FiloSottile/age). From ee0d5e7e28aaf6523e98e4e9e6f024dd6f10aded Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 02:15:01 -0400 Subject: [PATCH 07/10] rename types..go -> types.go --- client/internal/types..go | 142 -------------------------------------- 1 file changed, 142 deletions(-) delete mode 100644 client/internal/types..go diff --git a/client/internal/types..go b/client/internal/types..go deleted file mode 100644 index e68de6e..0000000 --- a/client/internal/types..go +++ /dev/null @@ -1,142 +0,0 @@ -package internal - -// Shares from keygen ceremonies are stored (encrypted) -type Shares struct { - Host string `json:"host"` - GroupID string `json:"group-id"` - PublicKey string `json:"public-key"` - EncryptedShare string `json:"encrypted-share"` - PublicShares map[string]string `json:"public-shares"` -} - -// This may expand in future versions -type FreonConfig struct { - Shares []Shares `json:"shares"` -} - -//------- Request/Response --------// -type InitKeyGenRequest struct { - Participants uint16 `json:"n"` - Threshold uint16 `json:"t"` -} -type InitKeyGenResponse struct { - GroupID string `json:"group-id"` -} - -type PollKeyGenRequest struct { - GroupID string `json:"group-id"` - PartyID *uint16 `json:"party-id,omitempty"` -} -type PollKeyGenResponse struct { - GroupID string `json:"group-id"` - MyPartyID *uint16 `json:"party-id"` - OtherParties []uint16 `json:"parties"` - Threshold uint16 `json:"t"` - PartySize uint16 `json:"n"` -} - -type InitSignRequest struct { - GroupID string `json:"group-id"` - MessageHash string `json:"hash"` - OpenSSH bool `json:"openssh"` - Namespace string `json:"openssh-namespace"` -} -type InitSignResponse struct { - CeremonyID string `json:"ceremony-id"` -} - -type PollSignRequest struct { - CeremonyID string `json:"ceremony-id"` - PartyID *uint16 `json:"party-id"` -} -type PollSignResponse struct { - GroupID string `json:"group-id"` - MyPartyID uint16 `json:"party-id"` - Threshold uint16 `json:"t"` - OtherParties []uint16 `json:"parties"` -} - -type JoinKeyGenRequest struct { - GroupID string `json:"group-id"` -} -type JoinKeyGenResponse struct { - Status bool `json:"status"` - MyPartyID uint16 `json:"my-party-id"` -} - -type JoinSignRequest struct { - CeremonyID string `json:"ceremony-id"` - MessageHash string `json:"hash"` - MyPartyID uint16 `json:"party-id"` -} -type JoinSignResponse struct { - Status bool `json:"status"` - OpenSSH bool `json:"openssh"` - Namespace string `json:"openssh-namespace"` -} - -type SendKeyGenRequest struct { - GroupID string `json:"group-id"` - MyPartyID uint16 `json:"party-id"` - LastIDSeen int64 `json:"last-seen-id"` - Message string `json:"message"` -} -type SendKeyGenResponse struct { - Status bool `json:"status"` - Messages []string `json:"messages"` -} - -type KeyGenMessageRequest struct { - GroupID string - Message string - MyPartyID uint16 - LastSeen int64 -} -type KeyGenMessageResponse struct { - LatestMessageID int64 - Messages []string -} - -type SignMessageRequest struct { - CeremonyID string `json:"ceremony-id"` - MyPartyID uint16 `json:"party-id"` - Message string `json:"message"` - LastSeen int64 `json:"last-seen"` -} -type SignMessageResponse struct { - LatestMessageID int64 `json:"last-seen"` - Messages []string `json:"messages"` -} - -type KeygenFinalRequest struct { - GroupID string `json:"group-id"` - MyPartyID uint16 `json:"party-id"` - PublicKey string `json:"public-key"` -} - -type SignFinalRequest struct { - CeremonyID string `json:"ceremony-id"` - MyPartyID uint16 `json:"party-id"` - Signature string `json:"signature"` -} - -type VapidResponse struct { - Status string `json:"status"` -} - -type FreonCeremonySummary struct { - Uid string - Active bool - Hash string - Signature *string - OpenSSH bool - OpenSSHNamespace string -} -type ListSignRequest struct { - GroupID string `json:"group-id"` - Limit int64 `json:"limit"` - Offset int64 `json:"offset"` -} -type ListSignResponse struct { - Ceremonies []FreonCeremonySummary -} From 84547731107c661d1425aabe2457db8052d8dd9e Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 02:16:17 -0400 Subject: [PATCH 08/10] rename types..go -> types.go --- client/internal/types.go | 142 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 client/internal/types.go diff --git a/client/internal/types.go b/client/internal/types.go new file mode 100644 index 0000000..e68de6e --- /dev/null +++ b/client/internal/types.go @@ -0,0 +1,142 @@ +package internal + +// Shares from keygen ceremonies are stored (encrypted) +type Shares struct { + Host string `json:"host"` + GroupID string `json:"group-id"` + PublicKey string `json:"public-key"` + EncryptedShare string `json:"encrypted-share"` + PublicShares map[string]string `json:"public-shares"` +} + +// This may expand in future versions +type FreonConfig struct { + Shares []Shares `json:"shares"` +} + +//------- Request/Response --------// +type InitKeyGenRequest struct { + Participants uint16 `json:"n"` + Threshold uint16 `json:"t"` +} +type InitKeyGenResponse struct { + GroupID string `json:"group-id"` +} + +type PollKeyGenRequest struct { + GroupID string `json:"group-id"` + PartyID *uint16 `json:"party-id,omitempty"` +} +type PollKeyGenResponse struct { + GroupID string `json:"group-id"` + MyPartyID *uint16 `json:"party-id"` + OtherParties []uint16 `json:"parties"` + Threshold uint16 `json:"t"` + PartySize uint16 `json:"n"` +} + +type InitSignRequest struct { + GroupID string `json:"group-id"` + MessageHash string `json:"hash"` + OpenSSH bool `json:"openssh"` + Namespace string `json:"openssh-namespace"` +} +type InitSignResponse struct { + CeremonyID string `json:"ceremony-id"` +} + +type PollSignRequest struct { + CeremonyID string `json:"ceremony-id"` + PartyID *uint16 `json:"party-id"` +} +type PollSignResponse struct { + GroupID string `json:"group-id"` + MyPartyID uint16 `json:"party-id"` + Threshold uint16 `json:"t"` + OtherParties []uint16 `json:"parties"` +} + +type JoinKeyGenRequest struct { + GroupID string `json:"group-id"` +} +type JoinKeyGenResponse struct { + Status bool `json:"status"` + MyPartyID uint16 `json:"my-party-id"` +} + +type JoinSignRequest struct { + CeremonyID string `json:"ceremony-id"` + MessageHash string `json:"hash"` + MyPartyID uint16 `json:"party-id"` +} +type JoinSignResponse struct { + Status bool `json:"status"` + OpenSSH bool `json:"openssh"` + Namespace string `json:"openssh-namespace"` +} + +type SendKeyGenRequest struct { + GroupID string `json:"group-id"` + MyPartyID uint16 `json:"party-id"` + LastIDSeen int64 `json:"last-seen-id"` + Message string `json:"message"` +} +type SendKeyGenResponse struct { + Status bool `json:"status"` + Messages []string `json:"messages"` +} + +type KeyGenMessageRequest struct { + GroupID string + Message string + MyPartyID uint16 + LastSeen int64 +} +type KeyGenMessageResponse struct { + LatestMessageID int64 + Messages []string +} + +type SignMessageRequest struct { + CeremonyID string `json:"ceremony-id"` + MyPartyID uint16 `json:"party-id"` + Message string `json:"message"` + LastSeen int64 `json:"last-seen"` +} +type SignMessageResponse struct { + LatestMessageID int64 `json:"last-seen"` + Messages []string `json:"messages"` +} + +type KeygenFinalRequest struct { + GroupID string `json:"group-id"` + MyPartyID uint16 `json:"party-id"` + PublicKey string `json:"public-key"` +} + +type SignFinalRequest struct { + CeremonyID string `json:"ceremony-id"` + MyPartyID uint16 `json:"party-id"` + Signature string `json:"signature"` +} + +type VapidResponse struct { + Status string `json:"status"` +} + +type FreonCeremonySummary struct { + Uid string + Active bool + Hash string + Signature *string + OpenSSH bool + OpenSSHNamespace string +} +type ListSignRequest struct { + GroupID string `json:"group-id"` + Limit int64 `json:"limit"` + Offset int64 `json:"offset"` +} +type ListSignResponse struct { + Ceremonies []FreonCeremonySummary +} From be70f7d07a1edc5feafcb0d310895ddcecfd4d3c Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 02:18:54 -0400 Subject: [PATCH 09/10] update ci.yml --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e056cf..9c56c88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,13 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.24.5' + go-version: '1.25' - - name: Build - run: go build -v ./... + - name: Build Client + run: go build -o ./freon -v ./client + + - name: Build Coordinator + run: go build -o ./freon-server -v ./coordinator - name: Test run: go test -v -short ./... From fb59741c80768342224b4270337524e487c390af Mon Sep 17 00:00:00 2001 From: Soatok Date: Fri, 22 Aug 2025 02:20:25 -0400 Subject: [PATCH 10/10] update ci.yml again --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c56c88..25baf1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,5 +24,8 @@ jobs: - name: Build Coordinator run: go build -o ./freon-server -v ./coordinator - - name: Test - run: go test -v -short ./... + - name: Test Client + run: cd client && go test -v -short && cd .. + + - name: Test Coordinator + run: cd coordinator && go test -v -short && cd ..