Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 30 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,37 @@
# FREON
# FREEON

> FOSS Resists Executive Overreaching Nations
> FOSS Resists Extralegal 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)
[![Build Status](https://github.com/soatok/freeon/actions/workflows/ci.yml/badge.svg)](https://github.com/soatok/freeon/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.
FREEON 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).

> [!WARNING]
> This software is a minimum viable product (MVP) and is only in the **alpha** stage of development. It has not been audited. Do not use this in production yet!

## Installation
> This project is not affiliated with [the Chemours Company](https://en.wikipedia.org/wiki/Freon).

### Freon Clients
## Installation

> [!WARNING]
> This command currently does not work. See [below](#temporary-workaround-for-replace-directive-error).
### Freeon Clients

```terminal
go install github.com/soatok/freon/client@latest
go install github.com/soatok/freeon/client@latest
```

#### For Developers

You can also clone the repository to install the client locally:

```terminal
git clone https://github.com/soatok/freon.git
cd freon/client
git clone https://github.com/soatok/freeon.git
cd freeon/client
go install
```

### Freon Coordinators
### Freeon Coordinators

> [!WARNING]
> The coordinator is expected to run on a private network, such as [Tailscale](https://tailscale.com),
Expand All @@ -42,8 +41,8 @@ go install
> Internet, but this was cut from the alpha release due to time constraints.

```terminal
git clone https://github.com/soatok/freon.git
cd freon
git clone https://github.com/soatok/freeon.git
cd freeon
go build -o coordinator ./coordinator
./coordinator
```
Expand All @@ -54,7 +53,7 @@ The order of operations is as followed:

1. Perform the **Distributed Key Generation** ceremony once, for each Ed25519 keypair.
1. One client tell the coordinator to initiate a new DKG. The number of parties and threshold are required at this step.
2. The coordinator sets up a session that other clients can connect to, and gives the DKG Group ID to the client. This is to be shared with other users. The Group ID is not sensitive; it only serves to allow multiple keys be managed by one Freon coordinator.
2. The coordinator sets up a session that other clients can connect to, and gives the DKG Group ID to the client. This is to be shared with other users. The Group ID is not sensitive; it only serves to allow multiple keys be managed by one Freeon coordinator.
3. Once every participating user enrolls in the DKG ceremony, the final public key is calculated and shared with each participant.
4. Each client encrypts their Shamir Share, along with their Party ID and the group identifier, locally.
2. For each message to be signed:
Expand All @@ -71,20 +70,20 @@ To initiate a new DKG group, one of the clients with access to the coordinator w
the number of participants and threshold needed to perform a signature operation, respectively):

```terminal
freon keygen create -h hostname:port -n 7 -t 3
freeon keygen create -h hostname:port -n 7 -t 3
```

Upon success, a Group ID will be returned. This is to be shared with the other participants, who will pass it as an extra argument:

```terminal
freon keygen join -h hostname:port -g [group-id-goes-here]
freeon keygen join -h hostname:port -g [group-id-goes-here]
```

This will maintain a connection with the coordinator until all `n` participants have connected. Afterwards, a copy of the public key will be returned to each client.

#### Optional Arguments

If you provide a public key (`-r [RECIPIENT]`) as an optional argument, the Freon client will use [age](https://age-encryption.org) to encrypt the share locally. This public key can be an age public key or an OpenSSH public key.
If you provide a public key (`-r [RECIPIENT]`) as an optional argument, the Freeon client will use [age](https://age-encryption.org) to encrypt the share locally. This public key can be an age public key or an OpenSSH public key.

### Signature Generation

Expand All @@ -98,8 +97,8 @@ To initiate a key ceremony, the following information is needed:
The message can be passed as a file name or via `STDIN`, like so:

```terminal
freon sign create -g [group-id-goes-here] file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freon sign create -g [group-id-goes-here]
freeon sign create -g [group-id-goes-here] file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freeon sign create -g [group-id-goes-here]
```

This will initialize a signature in progress and return a Ceremony ID.
Expand All @@ -114,16 +113,16 @@ By default, the final signature will be returned as a 128-character hex-encoded
You can pass an optional `--openssh` flag to return an OpenSSH-compatible signature.

```terminal
freon sign create --openssh -g [group-id-goes-here] file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freon sign create --openssh -g [group-id-goes-here]
freeon sign create --openssh -g [group-id-goes-here] file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freeon sign create --openssh -g [group-id-goes-here]
```

##### Terminating Incomplete Ceremonies

You can run this command to flush any incomplete ceremonies.

```terminal
freon terminate [ceremony-id-goes-here]
freeon terminate [ceremony-id-goes-here]
```

> [!WARNING]
Expand All @@ -134,23 +133,23 @@ freon terminate [ceremony-id-goes-here]
Each client will need to run this command to participate in the ceremony.

```terminal
freon sign join -c [ceremony-id] file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freon sign join -c [ceremony-id]
freeon sign join -c [ceremony-id] file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freeon sign join -c [ceremony-id]

# Identical:
freon sign join --ceremony [ceremony-id] file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freon sign join --ceremony [ceremony-id]
freeon sign join --ceremony [ceremony-id] file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freeon sign join --ceremony [ceremony-id]
```

##### Optional Arguments

You can furthermore pass the `-i` or `--identity` flag to specify the file path for your age secret keys.

```terminal
freon sign join -c [ceremony-id] -i /path/to/age.keys file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freon sign join -i /path/to/age.keys -c [ceremony-id]
freeon sign join -c [ceremony-id] -i /path/to/age.keys file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freeon sign join -i /path/to/age.keys -c [ceremony-id]

# Identical
freon sign join --ceremony [ceremony-id] --identity /path/to/age.keys file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freon sign join --identity /path/to/age.keys --ceremony [ceremony-id]
freeon sign join --ceremony [ceremony-id] --identity /path/to/age.keys file-with-message.txt
echo -n "MESSAGE TO BE SIGNED" | freeon sign join --identity /path/to/age.keys --ceremony [ceremony-id]
```
3 changes: 2 additions & 1 deletion client/go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/soatok/freon/client
module github.com/soatok/freeon/client

go 1.25

Expand All @@ -8,6 +8,7 @@ require (
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/soatok/freeon/client v0.0.0-20250831122941-3dcf071c280b
github.com/stretchr/testify v1.10.0
)

Expand Down
2 changes: 2 additions & 0 deletions client/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uM
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/freeon/client v0.0.0-20250831122941-3dcf071c280b h1:/Vsazx182lA4Gsnzg40FefJSXWMIivDi8aSGdbSNBIU=
github.com/soatok/freeon/client v0.0.0-20250831122941-3dcf071c280b/go.mod h1:rVzD6TFMqqlsQcAXoo+RlUO6fVcJtHyuY1QKdXjnbzc=
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=
Expand Down
2 changes: 1 addition & 1 deletion client/internal/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"testing"

"filippo.io/age"
"github.com/soatok/freon/client/internal"
"github.com/soatok/freeon/client/internal"
)

func TestEncryptDecryptShare(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions client/internal/duct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"net/http/httptest"
"testing"

"github.com/soatok/freon/client/internal"
"github.com/soatok/freeon/client/internal"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -46,7 +46,7 @@ func TestDuct(t *testing.T) {
case "/sign/list":
var req internal.ListSignRequest
json.NewDecoder(r.Body).Decode(&req)
resp := internal.ListSignResponse{Ceremonies: []internal.FreonCeremonySummary{{Uid: "test-ceremony"}}}
resp := internal.ListSignResponse{Ceremonies: []internal.FreeonCeremonySummary{{Uid: "test-ceremony"}}}
json.NewEncoder(w).Encode(resp)
case "/keygen/send":
var req internal.KeyGenMessageRequest
Expand Down
4 changes: 2 additions & 2 deletions client/internal/openssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"crypto/sha512"
"testing"

"github.com/soatok/freon/client/internal"
"github.com/soatok/freeon/client/internal"
"github.com/stretchr/testify/assert"
)

Expand All @@ -30,7 +30,7 @@ func TestOpenSSHEncode(t *testing.T) {
var sig2 []byte
h2 := sha256.Sum256([]byte("Signature Format"))
sig2 = append(sig2, h2[:]...)
h2 = sha256.Sum256([]byte("Freon - OpenSSH"))
h2 = sha256.Sum256([]byte("Freeon - OpenSSH"))
sig2 = append(sig2, h2[:]...)

encoded2 := internal.OpenSSHEncode(pk2, sig2, namespace)
Expand Down
24 changes: 12 additions & 12 deletions client/internal/persistence.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,46 +18,46 @@ func getConfigFile() (string, error) {
if err != nil {
return "", err
}
return filepath.Join(homeDir, ".freon.json"), nil
return filepath.Join(homeDir, ".freeon.json"), nil
}

// Default user config
func NewUserConfig() (FreonConfig, error) {
config := FreonConfig{
func NewUserConfig() (FreeonConfig, error) {
config := FreeonConfig{
Shares: []Shares{},
}
err := config.Save()
if err != nil {
return FreonConfig{}, err
return FreeonConfig{}, err
}
return config, nil
}

// Load the user config from a saved file
func LoadUserConfig() (FreonConfig, error) {
func LoadUserConfig() (FreeonConfig, error) {
configPath, err := getConfigFile()
if err != nil {
return FreonConfig{}, err
return FreeonConfig{}, err
}

file, err := os.Open(configPath)
if err != nil {
if os.IsNotExist(err) {
return NewUserConfig()
}
return FreonConfig{}, err
return FreeonConfig{}, err
}
defer file.Close()

var conf FreonConfig
var conf FreeonConfig
if err := json.NewDecoder(file).Decode(&conf); err != nil {
return FreonConfig{}, err
return FreeonConfig{}, err
}
return conf, err
}

// This API felt more natural for me to implement than `func SaveUserConfig(cfg FreonConfig) error`
func (cfg FreonConfig) Save() error {
// This API felt more natural for me to implement than `func SaveUserConfig(cfg FreeonConfig) error`
func (cfg FreeonConfig) Save() error {
configPath, err := getConfigFile()
if err != nil {
return err
Expand All @@ -74,7 +74,7 @@ func (cfg FreonConfig) Save() error {
return encoder.Encode(cfg)
}

func (cfg FreonConfig) AddShare(host, groupID, publicKey, share string, otherShares map[string]string, myPartyID uint16) error {
func (cfg FreeonConfig) AddShare(host, groupID, publicKey, share string, otherShares map[string]string, myPartyID uint16) error {
s := Shares{
Host: host,
GroupID: groupID,
Expand Down
4 changes: 2 additions & 2 deletions client/internal/persistence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import (
"os"
"testing"

"github.com/soatok/freon/client/internal"
"github.com/soatok/freeon/client/internal"
"github.com/stretchr/testify/assert"
)

func TestPersistence(t *testing.T) {
// Create a temporary directory for the config file
tmpdir, err := os.MkdirTemp("", "freon-test-")
tmpdir, err := os.MkdirTemp("", "freeon-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpdir)

Expand Down
6 changes: 3 additions & 3 deletions client/internal/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type Shares struct {
}

// This may expand in future versions
type FreonConfig struct {
type FreeonConfig struct {
Shares []Shares `json:"shares"`
}

Expand Down Expand Up @@ -141,7 +141,7 @@ type VapidResponse struct {
Status string `json:"status"`
}

type FreonCeremonySummary struct {
type FreeonCeremonySummary struct {
Uid string
Active bool
Hash string
Expand All @@ -155,5 +155,5 @@ type ListSignRequest struct {
Offset int64 `json:"offset"`
}
type ListSignResponse struct {
Ceremonies []FreonCeremonySummary
Ceremonies []FreeonCeremonySummary
}
6 changes: 3 additions & 3 deletions client/internal/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"fmt"
"testing"

"github.com/soatok/freon/client/internal"
"github.com/soatok/freeon/client/internal"
"github.com/stretchr/testify/assert"
)

Expand All @@ -29,7 +29,7 @@ func TestHashMessageForSanity(t *testing.T) {
}

func TestAmIElected(t *testing.T) {
hash := sha512.Sum384([]byte("freon testing"))
hash := sha512.Sum384([]byte("freeon testing"))
tests := []struct {
Elected bool
MyID uint16
Expand Down Expand Up @@ -102,7 +102,7 @@ func TestUniqueID(t *testing.T) {
}

func TestSelectIndex(t *testing.T) {
hash := sha512.Sum384([]byte("freon testing"))
hash := sha512.Sum384([]byte("freeon testing"))
index := internal.SelectIndex(hash[:], 8)
assert.Equal(t, uint64(4), index)
}
Expand Down
Loading
Loading