├── .gitignore ├── cmd ├── client │ ├── Makefile │ ├── proxy.go │ ├── url.go │ ├── manifest.go │ ├── main.go │ ├── codes.go │ ├── manifest.toml │ ├── url_test.go │ ├── op_verify.go │ ├── op_push.go │ ├── op_pull.go │ ├── README.md │ ├── op_create.go │ └── flags.go └── server │ ├── Makefile │ ├── init.go │ ├── config.toml │ ├── action.go │ ├── config.go │ ├── log.go │ ├── main.go │ └── handlers.go ├── .travis.yml ├── constants.go ├── crypto.go ├── types_test.go ├── proof.go ├── types.go ├── pgp.go ├── canary.go ├── pgp_test.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pgp 2 | *.proof 3 | *.sig 4 | *.pem 5 | *.txt 6 | cmd/client/fugl-client 7 | cmd/server/fugl-server 8 | -------------------------------------------------------------------------------- /cmd/client/Makefile: -------------------------------------------------------------------------------- 1 | BIN=fugl-client 2 | 3 | $(BIN): *.go 4 | go get 5 | go build -o $(BIN) 6 | 7 | clean: 8 | rm $(BIN) 9 | -------------------------------------------------------------------------------- /cmd/server/Makefile: -------------------------------------------------------------------------------- 1 | BIN=fugl-server 2 | 3 | $(BIN): *.go 4 | go get 5 | go build -o $(BIN) 6 | 7 | clean: 8 | rm $(BIN) 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - tip 5 | - 1.8 6 | - 1.7 7 | - 1.6 8 | - 1.5 9 | - 1.4 10 | 11 | install: 12 | - go get -t ./... 13 | 14 | script: 15 | - go test -v ./... 16 | - make -C cmd/client 17 | - make -C cmd/server 18 | -------------------------------------------------------------------------------- /cmd/server/init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | ) 8 | 9 | var FlagConfigPath = flag.String("config", "config.toml", "path to config file") 10 | 11 | func init() { 12 | flag.Parse() 13 | log.SetFlags(0) 14 | log.SetOutput(logWriter{os.Stdout}) 15 | } 16 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package fugl 2 | 3 | const ( 4 | SERVER_SUBMIT_FIELD_NAME = "proof" 5 | SERVER_SUBMIT_PATH = "/submit" 6 | SERVER_STATUS_PATH = "/status" 7 | SERVER_LATEST_PATH = "/latest" 8 | SERVER_GETKEY_PATH = "/getkey" 9 | CANARY_SEPERATOR = "# Metadata" 10 | ) 11 | -------------------------------------------------------------------------------- /cmd/server/config.toml: -------------------------------------------------------------------------------- 1 | [canary] 2 | store = "./proofs" 3 | key_file = "./public.pgp" 4 | on_failure = "" 5 | 6 | [logging] 7 | file = "./log.txt" 8 | level = "info" 9 | 10 | [server] 11 | port = 8080 12 | timeout_read = 10 13 | timeout_write = 10 14 | enable_submit = true 15 | enable_latest = true 16 | enable_getkey = true 17 | -------------------------------------------------------------------------------- /cmd/client/proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "golang.org/x/net/proxy" 5 | "net/http" 6 | ) 7 | 8 | func CreateHttpClient(proxyAddr string) (*http.Client, error) { 9 | if proxyAddr == "" { 10 | return &http.Client{}, nil 11 | } 12 | dialer, err := proxy.SOCKS5("tcp", proxyAddr, nil, proxy.Direct) 13 | httpTransport := &http.Transport{ 14 | Dial: dialer.Dial, 15 | } 16 | return &http.Client{Transport: httpTransport}, err 17 | } 18 | -------------------------------------------------------------------------------- /cmd/client/url.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "path" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | DefaultProtocol = "https" 12 | ) 13 | 14 | func createURL(address string, p string) (string, error) { 15 | // If we're missing the uri scheme, prepend it 16 | if !strings.HasPrefix(address, "http") { 17 | address = fmt.Sprintf("%s://%s", DefaultProtocol, address) 18 | } 19 | 20 | url, err := url.Parse(address) 21 | if err != nil { 22 | return "", err 23 | } 24 | 25 | url.Path = path.Join(url.Path, p) 26 | 27 | return url.String(), nil 28 | } 29 | -------------------------------------------------------------------------------- /cmd/client/manifest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/BurntSushi/toml" 5 | ) 6 | 7 | type Manifest struct { 8 | Author string `toml:"author"` // supposed author of canary 9 | Delta int64 `toml:"delta"` // time in seconds 10 | Promises []string `toml:"promises"` // list of promises (for machines) 11 | Description string `toml:"description"` // content of human readable portion 12 | Final bool `toml:"final"` // canary is final 13 | } 14 | 15 | func ParseManifest(path string) (Manifest, error) { 16 | var manifest Manifest 17 | _, err := toml.DecodeFile(path, &manifest) 18 | return manifest, err 19 | } 20 | -------------------------------------------------------------------------------- /cmd/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | const ( 9 | SCRYPT_N = 2 << 20 10 | SCRYPT_R = 8 11 | SCRYPT_P = 1 12 | SALT_SIZE = 16 13 | ) 14 | 15 | func main() { 16 | // parse flags 17 | flags := parseFlags() 18 | 19 | // handle different operations 20 | switch flags.Operation { 21 | case "create": 22 | operationCreate(flags) 23 | case "verify": 24 | operationVerify(flags) 25 | case "push": 26 | operationPush(flags) 27 | case "pull": 28 | operationPull(flags) 29 | case "": 30 | printHelp() 31 | default: 32 | fmt.Fprintf(os.Stderr, "Invalid operation: %s\n", flags.Operation) 33 | os.Exit(EXIT_INVALID_OPERATION) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package fugl 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | ) 8 | 9 | var RAND_ALPHABET = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") 10 | 11 | func HashString(input string) string { 12 | hash := sha256.Sum256([]byte(input)) 13 | return hex.EncodeToString(hash[:]) 14 | } 15 | 16 | func GetRandBytes(n int) []byte { 17 | b := make([]byte, n) 18 | i, err := rand.Read(b) 19 | if i != n || err != nil { 20 | panic(err) 21 | } 22 | return b 23 | } 24 | 25 | func GetRandStr(n int) string { 26 | str := make([]rune, n) 27 | for i, v := range GetRandBytes(n) { 28 | str[i] = RAND_ALPHABET[int(v)%len(RAND_ALPHABET)] 29 | } 30 | return string(str) 31 | } 32 | -------------------------------------------------------------------------------- /cmd/client/codes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | EXIT_SUCCESS = 0 5 | EXIT_INVALID_ADDRESS = -1 6 | EXIT_MISSING_PUBLIC_KEY = -2 7 | EXIT_MISSING_SECRET_KEY = -3 8 | EXIT_INVALID_PUBLIC_KEY = -4 9 | EXIT_INVALID_SECRET_KEY = -5 10 | EXIT_NO_SUCH_FILE = -6 11 | EXIT_FILE_READ_ERROR = -7 12 | EXIT_INVALID_LOCAL_CANARY = -8 13 | EXIT_INVALID_REMOTE_CANARY = -9 14 | EXIT_INVALID_OPERATION = -10 15 | EXIT_INVALID_ARGUMENTS = -11 16 | EXIT_BAD_PROXY = -12 17 | EXIT_CONNECTION_FAILURE = -13 18 | EXIT_INVALID_SIGNATURE = -14 19 | EXIT_INVALID_CANARY = -15 20 | EXIT_HTTP_UNEXPECTED_STATUS = -16 21 | EXIT_FILE_WRITE_ERROR = -17 22 | ) 23 | -------------------------------------------------------------------------------- /cmd/client/manifest.toml: -------------------------------------------------------------------------------- 1 | final = false 2 | delta = 864000 # 10 days in seconds 3 | author = "Test author" 4 | 5 | promises = [ 6 | "Not forced to handover logs to Europol", 7 | "Have not faked the moon landing", 8 | "We have not been raided by the FBI", 9 | "Promises are optional" 10 | ] 11 | 12 | description = """ 13 | # Test canary 14 | 15 | You can: 16 | 17 | * Explain the purpose of the canary 18 | * Maintain a human readable canary 19 | * Win peoples heart 20 | * Or just, Lorem ipsum dolor sit amet, consectetuer adipiscing elit. 21 | 22 | The description should be valid markdown, to please your fellow humans. 23 | The machines should have no problem understanding it regardless 24 | (just avoid using the "Metadata" header) 25 | """ 26 | -------------------------------------------------------------------------------- /cmd/client/url_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestURL__Defaults(t *testing.T) { 8 | cases := []struct { 9 | address, path, expected string 10 | }{ 11 | {"localhost:8080", "submit", "https://localhost:8080/submit"}, 12 | {"localhost:8080", "/submit", "https://localhost:8080/submit"}, 13 | {"http://localhost:8080", "submit", "http://localhost:8080/submit"}, 14 | {"localhost:8080/proxy", "submit", "https://localhost:8080/proxy/submit"}, 15 | } 16 | for _,tt := range cases { 17 | u, err := createURL(tt.address, tt.path) 18 | if err != nil { 19 | t.Fatalf("'%s' / '%s' - err - %v", tt.address, tt.path, err) 20 | } 21 | if u != tt.expected { 22 | t.Fatalf("'%s' doesn't match expected default address", u) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /types_test.go: -------------------------------------------------------------------------------- 1 | package fugl 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestCanarySerializeCycle(t *testing.T) { 11 | canary := Canary{ 12 | Version: 1, 13 | Author: "John Doe", 14 | Creation: CanaryTime(time.Now()), 15 | Expiry: CanaryTime(time.Now()), 16 | Promises: []string{"example"}, 17 | Nonce: "nonce", 18 | Final: false, 19 | } 20 | 21 | bs, err := json.Marshal(canary) 22 | if err != nil { 23 | t.Fatalf("error encoding Canary to json, err=%v", err) 24 | } 25 | 26 | var out Canary 27 | err = json.Unmarshal(bs, &out) 28 | if err != nil { 29 | t.Fatalf("error decoding Canary from json, err=%v", err) 30 | } 31 | 32 | if !canary.Equal(out) { 33 | fmt.Printf("canary=%v\n", canary) 34 | fmt.Printf("out =%v\n", out) 35 | 36 | t.Fatal("serialization cycle mismatch") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cmd/server/action.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | /* Runs an optional command unpon failure to submit new canary 10 | * 11 | * This can be used in a dead man's switch type scenario 12 | */ 13 | 14 | func actionRunner(command string, state *ServerState) { 15 | // check if feature enabled 16 | if command == "" { 17 | return 18 | } 19 | parts := strings.Fields(command) 20 | logInfo("Failure action:", parts) 21 | 22 | // wait for deadline 23 | for ticker := time.NewTicker(time.Second * 5); ; <-ticker.C { 24 | // check if canary on server 25 | state.canaryLock.RLock() 26 | if state.latestCanary == nil { 27 | state.canaryLock.RUnlock() 28 | continue 29 | } 30 | 31 | // check time 32 | now := time.Now() 33 | expiry := state.latestCanary.Expiry.Time() 34 | state.canaryLock.RUnlock() 35 | if now.After(expiry) { 36 | logInfo("Running failure action") 37 | out, err := exec.Command(parts[0], parts[1:]...).Output() 38 | if err != nil { 39 | logWarning("Failed to execute failure action:", err) 40 | } 41 | logInfo("Failure action output:\n" + string(out)) 42 | select {} // wait forever 43 | } 44 | 45 | // sleep 46 | logDebug("Action Runner going to sleep:", expiry.Sub(now).Seconds()) 47 | time.Sleep(expiry.Sub(now)) 48 | logDebug("Woke from sleep") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cmd/server/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/BurntSushi/toml" 5 | "time" 6 | ) 7 | 8 | type ConfigLogging struct { 9 | File string `toml:"file"` 10 | Level string `toml:"level"` 11 | } 12 | 13 | type ConfigServer struct { 14 | Port uint16 15 | Address string 16 | TimeoutRead time.Duration 17 | TimeoutWrite time.Duration 18 | CertFile string `toml:"cert_file"` // tls: certificate 19 | KeyFile string `toml:"key_file"` // tls: private key 20 | EnableViewSubmit bool `toml:"enable_submit"` // enable submit view 21 | EnableViewStatus bool `toml:"enable_status"` // enable status view 22 | EnableViewLatest bool `toml:"enable_latest"` // enable latest view 23 | EnableViewGetKey bool `toml:"enable_getkey"` // enable get key view 24 | } 25 | 26 | type ConfigCanary struct { 27 | OnFailure string `toml:"on_failure"` // command on failure 28 | KeyFile string `toml:"key_file"` // load key from this file 29 | Store string `toml:"store"` // directory for storing canaries 30 | } 31 | 32 | type Config struct { 33 | Logging ConfigLogging `toml:"logging"` // log settings 34 | Server ConfigServer `toml:"server"` // http server settings 35 | Canary ConfigCanary `toml:"canary"` // canary settings 36 | } 37 | 38 | func loadConfig() (Config, error) { 39 | var config Config 40 | _, err := toml.DecodeFile(*FlagConfigPath, &config) 41 | return config, err 42 | } 43 | -------------------------------------------------------------------------------- /cmd/client/op_verify.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rot256/fugl" 6 | "golang.org/x/crypto/openpgp" 7 | "io/ioutil" 8 | "time" 9 | ) 10 | 11 | /* Validates and adds a canary to the store 12 | * and updates the state of the store 13 | */ 14 | 15 | func requiredFlagsVerify(flags Flags) { 16 | var opt FlagOpt 17 | opt.Required(FlagNamePublicKey, flags.PublicKey != "") 18 | opt.Required(FlagNameProof, flags.Proof != "") 19 | opt.Check() 20 | } 21 | 22 | func operationVerify(flags Flags) { 23 | requiredFlagsVerify(flags) 24 | 25 | // load new proof (input) 26 | proof, err := ioutil.ReadFile(flags.Proof) 27 | if err != nil { 28 | exitError(EXIT_FILE_READ_ERROR, "Failed to input proof: %s", err.Error()) 29 | } 30 | 31 | // load public key 32 | pk, err := func() (*openpgp.Entity, error) { 33 | skData, err := ioutil.ReadFile(flags.PublicKey) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return fugl.PGPLoadPublicKey(skData) 38 | }() 39 | if err != nil { 40 | exitError(EXIT_FILE_READ_ERROR, "Failed to read public key: %s", err.Error()) 41 | } 42 | 43 | // validate new proof 44 | canary, description, err := fugl.OpenProof(pk, string(proof)) 45 | if err != nil { 46 | exitError(EXIT_INVALID_SIGNATURE, "Failed to validate signature on proof: %s", err.Error()) 47 | } 48 | 49 | // verify fields 50 | err = fugl.CheckCanaryFormat(canary, time.Now()) 51 | if err != nil { 52 | exitError(EXIT_INVALID_CANARY, "Failed to validate canary fields: %s", err.Error()) 53 | } 54 | fmt.Println("Author:", canary.Author) 55 | fmt.Println("Expires:", canary.Expiry.String()) 56 | fmt.Println("Description:\n" + description) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/client/op_push.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rot256/fugl" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | func requiredFlagsPush(flags Flags) { 12 | var opt FlagOpt 13 | opt.Required(FlagNameAddress, flags.Address != "") 14 | opt.Required(FlagNameProof, flags.Proof != "") 15 | opt.Optional(FlagNameProxy, flags.Proxy != "") 16 | opt.Check() 17 | } 18 | 19 | func operationPush(flags Flags) { 20 | requiredFlagsPush(flags) 21 | 22 | // create final url 23 | addr, err := createURL(flags.Address, fugl.SERVER_SUBMIT_PATH) 24 | if err != nil { 25 | exitError(EXIT_INVALID_ADDRESS, "Failed to parse address %s", err.Error()) 26 | } 27 | 28 | // create (proxied) client 29 | client, err := CreateHttpClient(flags.Proxy) 30 | if err != nil { 31 | exitError(EXIT_BAD_PROXY, "Failed connect to proxy: %s", err.Error()) 32 | } 33 | 34 | // read input file 35 | content, err := ioutil.ReadFile(flags.Proof) 36 | if err != nil { 37 | exitError(EXIT_FILE_READ_ERROR, "Failed to read input file: %s", err.Error()) 38 | } 39 | 40 | // post 41 | form := url.Values{} 42 | form.Add(fugl.SERVER_SUBMIT_FIELD_NAME, string(content)) 43 | resp, err := client.PostForm(addr, form) 44 | if err != nil { 45 | exitError(EXIT_CONNECTION_FAILURE, "Failed to connect to remote server: %s", err.Error()) 46 | } 47 | 48 | // check for server error message 49 | if resp.StatusCode != http.StatusNoContent { 50 | msg, err := ioutil.ReadAll(resp.Body) 51 | if err != nil { 52 | exitError(EXIT_CONNECTION_FAILURE, "Submission failed: %s", resp.Status) 53 | } 54 | exitError(EXIT_CONNECTION_FAILURE, "Submission failed %s with: '%s'", resp.Status, string(msg)) 55 | } 56 | fmt.Println("Successfully pushed new proof to server") 57 | } 58 | -------------------------------------------------------------------------------- /proof.go: -------------------------------------------------------------------------------- 1 | package fugl 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "golang.org/x/crypto/openpgp" 8 | "strings" 9 | ) 10 | 11 | func OpenProof(entity *openpgp.Entity, proof string) (*Canary, string, error) { 12 | // parse and verify signature 13 | block, err := PGPVerify(entity, []byte(proof)) 14 | if err != nil { 15 | return nil, "", err 16 | } 17 | 18 | // scan for seperator 19 | start := 0 20 | lines := strings.Split(string(block.Bytes), "\n") 21 | for ; start < len(lines); start++ { 22 | if strings.TrimRight(lines[start], "\n\r") == CANARY_SEPERATOR { 23 | break 24 | } 25 | } 26 | if start == len(lines) { 27 | return nil, "", errors.New("Unable to find canary seperator") 28 | } 29 | 30 | // eat seperator and empty lines 31 | des := strings.Join(lines[:start-1], "\n") 32 | for start = start + 1; start < len(lines); start++ { 33 | if strings.TrimRight(lines[start], "\n\r") != "" { 34 | break 35 | } 36 | } 37 | 38 | // load JSON structure 39 | var canary Canary 40 | ser := strings.Join(lines[start:], "\n") 41 | err = json.Unmarshal([]byte(ser), &canary) 42 | if err != nil { 43 | return nil, "", errors.New("Unable to parse json structure") 44 | } 45 | return &canary, des, nil 46 | } 47 | 48 | func SealProof(entity *openpgp.Entity, canary Canary, description string) (string, error) { 49 | // serialize canary 50 | ser, err := json.MarshalIndent(canary, "", " ") 51 | if err != nil { 52 | return "", err 53 | } 54 | 55 | // add serperator and sign 56 | var inner string 57 | if description == "" { 58 | inner = fmt.Sprintf("%s\n%s", CANARY_SEPERATOR, string(ser)) 59 | } else { 60 | inner = fmt.Sprintf("%s\n%s\n\n%s", description, CANARY_SEPERATOR, string(ser)) 61 | } 62 | return PGPSign(entity, []byte(inner)) 63 | } 64 | -------------------------------------------------------------------------------- /cmd/client/op_pull.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rot256/fugl" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | func requiredFlagsPull(flags Flags) { 12 | var opt FlagOpt 13 | opt.Required(FlagNameAddress, flags.Address != "") 14 | opt.Required(FlagNameProof, flags.Proof != "") 15 | opt.Optional(FlagNameProxy, flags.Proxy != "") 16 | opt.Check() 17 | } 18 | 19 | func operationPull(flags Flags) { 20 | requiredFlagsPull(flags) 21 | 22 | // create final url 23 | addr, err := createURL(flags.Address, fugl.SERVER_LATEST_PATH) 24 | if err != nil { 25 | exitError(EXIT_INVALID_ADDRESS, "Failed to parse address %s", err.Error()) 26 | } 27 | 28 | // create (proxied) client 29 | client, err := CreateHttpClient(flags.Proxy) 30 | if err != nil { 31 | fmt.Fprintf(os.Stderr, "Failed connect to proxy: %s", err.Error()) 32 | os.Exit(EXIT_BAD_PROXY) 33 | } 34 | 35 | // do http request 36 | resp, err := client.Get(addr) 37 | if err != nil { 38 | fmt.Fprintf(os.Stderr, "Failed connect to address: %s", err.Error()) 39 | os.Exit(EXIT_CONNECTION_FAILURE) 40 | } 41 | defer resp.Body.Close() 42 | 43 | // check if body is present 44 | if resp.StatusCode == http.StatusNoContent { 45 | fmt.Println("No canary available") 46 | return 47 | } 48 | if resp.StatusCode != http.StatusOK { 49 | exitError(EXIT_HTTP_UNEXPECTED_STATUS, "Server returned unexpected status code: %s", resp.Status) 50 | } 51 | 52 | // read response 53 | body, err := ioutil.ReadAll(resp.Body) 54 | if err != nil { 55 | exitError(EXIT_CONNECTION_FAILURE, "Failed to read response body: %s", err.Error()) 56 | } 57 | 58 | // write to file 59 | err = ioutil.WriteFile(flags.Proof, body, 0644) 60 | if err != nil { 61 | exitError(EXIT_FILE_WRITE_ERROR, "Failed to write proof to file: %s", err.Error()) 62 | } 63 | fmt.Println("Saved to:", flags.Proof) 64 | } 65 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package fugl 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "time" 8 | ) 9 | 10 | type CanaryTime time.Time 11 | 12 | type Canary struct { 13 | Version int64 `json:"version"` // Canary struct version 14 | Author string `json:"author"` // Publishing entity of the canary 15 | Creation CanaryTime `json:"creation"` // Time of creation 16 | Expiry CanaryTime `json:"expiry"` // Expiry time of canary 17 | Promises []string `json:"promises"` // Set of promises (may be empty) 18 | Nonce string `json:"nonce"` // Random nonce 19 | Final bool `json:"final"` // Is this canary final? 20 | } 21 | 22 | func (c Canary) Equal(other Canary) bool { 23 | return (c.Version == other.Version) && 24 | (c.Author == other.Author) && 25 | c.Creation.Time().Equal(other.Creation.Time()) && 26 | c.Expiry.Time().Equal(other.Expiry.Time()) && 27 | reflect.DeepEqual(c.Promises, other.Promises) && 28 | (c.Nonce == other.Nonce) && 29 | (c.Final == other.Final) 30 | } 31 | 32 | /* specifies the time format used in the canaries 33 | */ 34 | 35 | func (t CanaryTime) MarshalJSON() ([]byte, error) { 36 | stamp := fmt.Sprintf("\"%s\"", t.String()) 37 | return []byte(stamp), nil 38 | } 39 | 40 | func (t *CanaryTime) UnmarshalJSON(val []byte) error { 41 | str := string(val) 42 | if len(str) < 2 { 43 | return errors.New("Time field too short") 44 | } 45 | if str[0] != '"' || str[len(str)-1] != '"' { 46 | return errors.New("Time must be json string type") 47 | } 48 | date, err := time.Parse(CanaryTimeFormat, str[1:len(str)-1]) 49 | if err != nil { 50 | return err 51 | } 52 | *t = CanaryTime(date) 53 | return nil 54 | } 55 | 56 | func (t CanaryTime) Time() time.Time { 57 | return time.Time(t).Round(1 * time.Second) 58 | } 59 | 60 | func (t CanaryTime) String() string { 61 | return t.Time().Format(CanaryTimeFormat) 62 | } 63 | -------------------------------------------------------------------------------- /pgp.go: -------------------------------------------------------------------------------- 1 | package fugl 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "golang.org/x/crypto/openpgp" 7 | "golang.org/x/crypto/openpgp/armor" 8 | "golang.org/x/crypto/openpgp/clearsign" 9 | "golang.org/x/crypto/openpgp/packet" 10 | ) 11 | 12 | func PGPLoadPrivateKey(key []byte) (*openpgp.Entity, error) { 13 | block, err := armor.Decode(bytes.NewReader([]byte(key))) 14 | if err != nil { 15 | return nil, err 16 | } else if block.Type != openpgp.PrivateKeyType { 17 | return nil, errors.New("Not a OpenPGP public key") 18 | } 19 | return openpgp.ReadEntity(packet.NewReader(block.Body)) 20 | } 21 | 22 | func PGPLoadPublicKey(key []byte) (*openpgp.Entity, error) { 23 | block, err := armor.Decode(bytes.NewReader([]byte(key))) 24 | if err != nil { 25 | return nil, err 26 | } else if block.Type != openpgp.PublicKeyType { 27 | return nil, errors.New("Not a OpenPGP private key") 28 | } 29 | return openpgp.ReadEntity(packet.NewReader(block.Body)) 30 | } 31 | 32 | func PGPSign(entity *openpgp.Entity, message []byte) (string, error) { 33 | if entity.PrivateKey == nil { 34 | return "", errors.New("invalid private key") 35 | } 36 | 37 | // create signature writer 38 | var outSig bytes.Buffer 39 | writer, err := clearsign.Encode(&outSig, entity.PrivateKey, nil) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | // sign entire message and flush 45 | _, err = writer.Write(message) 46 | if err != nil { 47 | return "", err 48 | } 49 | err = writer.Close() 50 | if err != nil { 51 | return "", err 52 | } 53 | return outSig.String(), err 54 | } 55 | 56 | func PGPVerify(entity *openpgp.Entity, signature []byte) (*clearsign.Block, error) { 57 | if entity == nil { 58 | return nil, errors.New("invalid public key") 59 | } 60 | 61 | // parse clear signature 62 | block, rest := clearsign.Decode(signature) 63 | if len(rest) > 0 { 64 | return nil, errors.New("Proof contains junk") 65 | } 66 | if block == nil { 67 | return nil, errors.New("Unable to read pgp block") 68 | } 69 | 70 | // verify signature 71 | keyring := make(openpgp.EntityList, 1) 72 | keyring[0] = entity 73 | content := bytes.NewReader(block.Bytes) 74 | _, err := openpgp.CheckDetachedSignature(keyring, content, block.ArmoredSignature.Body) 75 | if err != nil { 76 | return nil, errors.New("Invalid signature") 77 | } 78 | return block, nil 79 | } 80 | -------------------------------------------------------------------------------- /cmd/client/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | You specify an operation using the --operation flag. 4 | See details below for the different operations supported. 5 | 6 | ## Before you start 7 | 8 | Before you start you should have a PGP key-pair, 9 | the public key should be configured on the server and 10 | the private key available on client (if you wish to create new proofs). 11 | Furthermore you should create a directory for storing the proof chain (default is ./store) 12 | 13 | ## Creating 14 | 15 | You can create new proofs using the create operation. 16 | Before doing so you must create a canary "manifest", which is a template for creating new canaries. 17 | An example of such a manifest can be found in the current directory (see manifest.toml). 18 | 19 | After editing the manifest, a new proof can be created like this: 20 | 21 | ``` 22 | ~> ./client --operation=create --private-key=./private.pgp 23 | Private key encrypted, please enter passphrase: 24 | Wrote new proof to: temp.proof 25 | ``` 26 | 27 | ## Verifying 28 | 29 | For verifying the validity of proofs, you must use the "verify" operation. 30 | It verifies the PGP signature as well as fields of the canary in the metadata section. 31 | 32 | An example follows (using the newly created proof): 33 | 34 | ``` 35 | ~> ./client --operation=verify --public-key=./public.pgp 36 | Author: Test author 37 | Expires: 2017-02-21T11:27:54+01:00 38 | Description: 39 | # Test canary 40 | 41 | You can: 42 | 43 | * Explain the purpose of the canary 44 | * Maintain a human readable canary 45 | ... 46 | ``` 47 | 48 | ## Pushing 49 | 50 | Proofs are added to the server by pushing. 51 | Pushing is really a fancy word for issuing a POST request, after which the server will validate the proof 52 | (in the same way as the "verify" operation on the client). 53 | 54 | A silly example using a server on localhost follows (using the default value for the "proof" parameter): 55 | 56 | ``` 57 | ~> ./client --operation=push --address=http://127.0.0.1:8080/submit 58 | Successfully pushed new proof to server 59 | ``` 60 | 61 | ## Pulling 62 | 63 | The client "pulls" the newest proof from the server into a temporary file using a GET request. 64 | You then use the verify operation to verify its validity. 65 | 66 | Here an example of using the "pull" operation: 67 | 68 | ``` 69 | ~> ./client --operation=pull --address=http://127.0.0.1:8080/latest 70 | Saved to: temp.proof 71 | ``` 72 | -------------------------------------------------------------------------------- /cmd/client/op_create.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/rot256/fugl" 7 | "golang.org/x/crypto/openpgp" 8 | "golang.org/x/crypto/ssh/terminal" 9 | "io/ioutil" 10 | "os" 11 | "time" 12 | ) 13 | 14 | func requiredFlagsCreate(flags Flags) { 15 | var opt FlagOpt 16 | opt.Required(FlagNamePrivateKey, flags.PrivateKey != "") 17 | opt.Required(FlagNameManifest, flags.Manifest != "") 18 | opt.Required(FlagNameProof, flags.Proof != "") 19 | opt.Check() 20 | } 21 | 22 | func operationCreate(flags Flags) { 23 | // verify supplied flags 24 | requiredFlagsCreate(flags) 25 | 26 | // load manifest 27 | manifest, err := ParseManifest(flags.Manifest) 28 | if err != nil { 29 | exitError(EXIT_FILE_READ_ERROR, "Failed to load manifest %s", err.Error()) 30 | } 31 | 32 | // load private key 33 | sk, err := func() (*openpgp.Entity, error) { 34 | skData, err := ioutil.ReadFile(flags.PrivateKey) 35 | if err != nil { 36 | return nil, err 37 | } 38 | sk, err := fugl.PGPLoadPrivateKey(skData) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if sk.PrivateKey.Encrypted { 43 | fmt.Println("Private key encrypted, please enter passphrase:") 44 | passwd, err := terminal.ReadPassword(int(os.Stdin.Fd())) 45 | if err != nil { 46 | return nil, err 47 | } 48 | err = sk.PrivateKey.Decrypt(passwd) 49 | if err != nil { 50 | return nil, errors.New("Failed to decrypt key") 51 | } 52 | } 53 | return sk, err 54 | }() 55 | if err != nil { 56 | fmt.Fprintf(os.Stderr, "Failed to read private key: %s", err.Error()) 57 | os.Exit(EXIT_FILE_READ_ERROR) 58 | } 59 | 60 | // create canary 61 | now := time.Now() 62 | expire := now.Add(time.Duration(manifest.Delta) * time.Second) 63 | canary := fugl.Canary{ 64 | Version: fugl.CanaryVersion, 65 | Author: manifest.Author, 66 | Creation: fugl.CanaryTime(now), 67 | Expiry: fugl.CanaryTime(expire), 68 | Nonce: fugl.GetRandStr(fugl.CanaryNonceSize), 69 | Final: manifest.Final, 70 | } 71 | 72 | // sign canary, producing proof 73 | proof, err := fugl.SealProof(sk, canary, manifest.Description) 74 | if err != nil { 75 | fmt.Fprintf(os.Stderr, "Failed to sign canary: %s", err.Error()) 76 | os.Exit(EXIT_FILE_READ_ERROR) 77 | } 78 | 79 | // write to output 80 | err = ioutil.WriteFile(flags.Proof, []byte(proof), 0644) 81 | if err != nil { 82 | exitError(EXIT_FILE_WRITE_ERROR, "Failed to write proof to file: %s", err.Error()) 83 | } 84 | fmt.Println("Saved new proof to:", flags.Proof) 85 | if manifest.Final { 86 | fmt.Println("WARNING: This canary is final!") 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cmd/server/log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "log" 7 | "os" 8 | "time" 9 | ) 10 | 11 | const ( 12 | TIME_FORMAT = "2006-01-02 15:04:05 MST" 13 | ) 14 | 15 | type logWriter struct { 16 | output io.Writer 17 | } 18 | 19 | func (writer logWriter) Write(msg []byte) (int, error) { 20 | logged := []byte(time.Now().Format(TIME_FORMAT) + " ") 21 | logged = append(logged, msg...) 22 | return writer.output.Write(logged) 23 | } 24 | 25 | var ( 26 | LogEnableDebug bool 27 | LogEnableInfo bool 28 | LogEnableWarning bool 29 | LogEnableError bool 30 | ) 31 | 32 | func initLogging(config Config) { 33 | // Expand logging level 34 | switch config.Logging.Level { 35 | case "debug": 36 | LogEnableDebug = true 37 | LogEnableInfo = true 38 | LogEnableWarning = true 39 | LogEnableError = true 40 | case "info": 41 | LogEnableDebug = false 42 | LogEnableInfo = true 43 | LogEnableWarning = true 44 | LogEnableError = true 45 | case "warning": 46 | LogEnableDebug = false 47 | LogEnableInfo = false 48 | LogEnableWarning = true 49 | LogEnableError = true 50 | case "error": 51 | LogEnableDebug = false 52 | LogEnableInfo = false 53 | LogEnableWarning = false 54 | LogEnableError = true 55 | default: 56 | logFatal("Config: log_level must be \"error\", \"warning\", \"info\" or \"debug\"") 57 | } 58 | 59 | // Multiplex output 60 | var output io.Writer = os.Stdout 61 | if config.Logging.File != "" { 62 | logFile, err := os.OpenFile(config.Logging.File, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 63 | if err != nil { 64 | log.Panicln("Failed to open log:", config.Logging.File, err) 65 | } 66 | output = io.MultiWriter(output, logFile) 67 | } 68 | log.SetFlags(0) 69 | log.SetOutput(logWriter{output}) 70 | } 71 | 72 | func logPrefix(s interface{}, v []interface{}) { 73 | args := make([]interface{}, 0, len(v)+1) 74 | args = append(args, s) 75 | args = append(args, v...) 76 | log.Println(args...) 77 | } 78 | 79 | func logDebug(v ...interface{}) { 80 | if LogEnableDebug { 81 | logPrefix("[DEBUG] :", v) 82 | } 83 | } 84 | 85 | func logInfo(v ...interface{}) { 86 | if LogEnableInfo { 87 | logPrefix("[INFO] :", v) 88 | } 89 | } 90 | 91 | func logWarning(v ...interface{}) { 92 | if LogEnableWarning { 93 | logPrefix("[WARNING] :", v) 94 | } 95 | } 96 | 97 | func logError(v ...interface{}) { 98 | if LogEnableError { 99 | logPrefix("[ERROR] :", v) 100 | } 101 | } 102 | 103 | func logFatal(v ...interface{}) { 104 | logPrefix("[FATAL] :", v) 105 | panic(errors.New("The server experienced a critical error, see the log for details")) 106 | } 107 | 108 | func logCheck(err error) { 109 | if err != nil { 110 | logFatal(err) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /canary.go: -------------------------------------------------------------------------------- 1 | package fugl 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "path" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | /* contains helper functions and initalization routines for handlers 13 | */ 14 | 15 | const ( 16 | CanaryVersion = 0 17 | CanaryTimeFormat = time.RFC3339 18 | ProofFileTimeFormat = "20060102150405" // must be a valid filename and sortable 19 | CanaryNonceSize = 32 20 | ProofFileExtension = ".proof" 21 | ProofFileName = "proof-%s-%s" + ProofFileExtension 22 | ) 23 | 24 | func CheckCanary(new *Canary, old *Canary, now time.Time) error { 25 | err := CheckCanaryFormat(new, now) 26 | if err != nil { 27 | return err 28 | } 29 | return CheckCanaryPrevious(new, old) 30 | } 31 | 32 | func CheckCanaryFormat(canary *Canary, now time.Time) error { 33 | if canary.Version != CanaryVersion { 34 | return errors.New("Unsupported canary version") 35 | } 36 | if len(canary.Nonce) != CanaryNonceSize { 37 | return errors.New(fmt.Sprintf("Nonce must be %d characters long", CanaryNonceSize)) 38 | } 39 | if now.After(canary.Expiry.Time()) { 40 | return errors.New("Canary has expired") 41 | } 42 | if canary.Author == "" { 43 | return errors.New("Author field is empty") 44 | } 45 | if canary.Creation.Time().After(now) { 46 | return errors.New("Creation time cannot be in the future (canary not valid yet)") 47 | } 48 | return nil 49 | } 50 | 51 | func CheckCanaryPrevious(new *Canary, old *Canary) error { 52 | if old == nil { 53 | return nil 54 | } 55 | if old.Final { 56 | return errors.New("Current canary is final") 57 | } 58 | if old.Creation.Time().After(new.Creation.Time()) { 59 | return errors.New("Current canary has creation after new") 60 | } 61 | if !new.Expiry.Time().After(old.Expiry.Time()) { 62 | return errors.New("New canary does not have expiry time after old") 63 | } 64 | return nil 65 | } 66 | 67 | func LoadLatestProof(dir string) (string, error) { 68 | // find newest proof 69 | var proofFile string 70 | files, _ := ioutil.ReadDir(dir) 71 | for _, file := range files { 72 | // check if proof file 73 | if file.IsDir() { 74 | return "", errors.New("Directory found in store") 75 | } 76 | if !strings.HasSuffix(file.Name(), ProofFileExtension) { 77 | return "", errors.New("Non-proof file in store: " + file.Name()) 78 | } 79 | proofFile = file.Name() 80 | } 81 | 82 | // read proof 83 | if proofFile != "" { 84 | proof, err := ioutil.ReadFile(path.Join(dir, proofFile)) 85 | return string(proof), err 86 | } 87 | return "", nil 88 | } 89 | 90 | func SaveToDirectory(proof string, dir string, when time.Time) error { 91 | hash := HashString(proof) 92 | date := time.Time(when).Format(ProofFileTimeFormat) 93 | fileName := fmt.Sprintf(ProofFileName, date, hash) 94 | filePath := path.Join(dir, fileName) 95 | return ioutil.WriteFile(filePath, []byte(proof), 0600) 96 | } 97 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rot256/fugl" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | func createDir(path string) error { 12 | if _, err := os.Stat(path); os.IsNotExist(err) { 13 | return os.MkdirAll(path, os.ModePerm) 14 | } 15 | return nil 16 | } 17 | 18 | func createState(config Config) *ServerState { 19 | // read public key 20 | var state ServerState 21 | key, err := ioutil.ReadFile(config.Canary.KeyFile) 22 | if err != nil { 23 | logFatal("Unable to load public key from:", config.Canary.KeyFile) 24 | } 25 | state.canaryKeyArmor = string(key) 26 | state.canaryKey, err = fugl.PGPLoadPublicKey(key) 27 | if err != nil { 28 | logFatal("Unable to parse PGP key:", err) 29 | } 30 | 31 | // load latest proof 32 | err = createDir(config.Canary.Store) 33 | if err != nil { 34 | logFatal("Unable to create store:", err) 35 | } 36 | state.latestProof, err = fugl.LoadLatestProof(config.Canary.Store) 37 | if err != nil { 38 | logFatal("Failed to load latest proof") 39 | } 40 | 41 | // parse latest proof 42 | if state.latestProof != "" { 43 | state.latestCanary, _, err = fugl.OpenProof(state.canaryKey, state.latestProof) 44 | if err != nil { 45 | logFatal("Failed to load latest canary:", err.Error()) 46 | } 47 | } 48 | state.storeDir = config.Canary.Store 49 | return &state 50 | } 51 | 52 | func buildHandler(config Config) (http.Handler, *ServerState) { 53 | state := createState(config) 54 | handler := http.NewServeMux() 55 | if config.Server.EnableViewSubmit { 56 | logInfo("Enable view: Submit") 57 | handler.Handle(fugl.SERVER_SUBMIT_PATH, &SubmitHandler{state: state}) 58 | } 59 | if config.Server.EnableViewLatest { 60 | logInfo("Enable view: Latest") 61 | handler.Handle(fugl.SERVER_LATEST_PATH, &LatestHandler{state: state}) 62 | } 63 | if config.Server.EnableViewGetKey { 64 | logInfo("Enable view: GetKey") 65 | handler.Handle(fugl.SERVER_GETKEY_PATH, &GetKeyHandler{state: state}) 66 | } 67 | return handler, state 68 | } 69 | 70 | func main() { 71 | // initalize logger 72 | config, err := loadConfig() 73 | if err != nil { 74 | logFatal("Unable to load config") 75 | } 76 | initLogging(config) 77 | 78 | // build handler and server state 79 | handler, state := buildHandler(config) 80 | go actionRunner(config.Canary.OnFailure, state) 81 | 82 | // build server 83 | bind := fmt.Sprintf( 84 | "%s:%d", 85 | config.Server.Address, 86 | config.Server.Port) 87 | server := &http.Server{ 88 | ReadTimeout: config.Server.TimeoutRead, 89 | WriteTimeout: config.Server.TimeoutWrite, 90 | Addr: bind, 91 | Handler: handler, 92 | MaxHeaderBytes: 1 << 20, 93 | } 94 | 95 | // run http(s) server 96 | if config.Server.CertFile == "" || config.Server.KeyFile == "" { 97 | logInfo("Starting HTTP server on:", bind) 98 | err = server.ListenAndServe() 99 | } else { 100 | logInfo("Starting HTTPS server on:", bind) 101 | err = server.ListenAndServeTLS( 102 | config.Server.CertFile, 103 | config.Server.KeyFile) 104 | } 105 | logFatal("Server terminated with:", err) 106 | } 107 | -------------------------------------------------------------------------------- /cmd/server/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/rot256/fugl" 5 | "golang.org/x/crypto/openpgp" 6 | "net/http" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type ServerState struct { 12 | storeDir string // directory for storing new canaries 13 | latestCanary *fugl.Canary // cached latest canary (parsed proof) 14 | latestProof string // newest proof 15 | canaryKey *openpgp.Entity // parsed public key 16 | canaryKeyArmor string // ascii armored pgp key 17 | canaryLock sync.RWMutex 18 | } 19 | 20 | func SendRequestError(w http.ResponseWriter, msg string) { 21 | w.WriteHeader(http.StatusBadRequest) 22 | w.Write([]byte(msg)) 23 | return 24 | } 25 | 26 | /* Serves the public key */ 27 | 28 | type GetKeyHandler struct { 29 | state *ServerState 30 | } 31 | 32 | func (h *GetKeyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 33 | if h.state.canaryKeyArmor == "" { 34 | w.WriteHeader(http.StatusNoContent) 35 | return 36 | } 37 | w.Header().Set("Content-Type", "text/plain") 38 | w.Write([]byte(h.state.canaryKeyArmor)) 39 | } 40 | 41 | /* Serves the latest published canary */ 42 | 43 | type LatestHandler struct { 44 | state *ServerState 45 | } 46 | 47 | func (h *LatestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 48 | h.state.canaryLock.RLock() 49 | defer h.state.canaryLock.RUnlock() 50 | if h.state.latestProof == "" { 51 | w.WriteHeader(http.StatusNoContent) 52 | return 53 | } 54 | w.Header().Set("Content-Type", "text/plain") 55 | w.Write([]byte(h.state.latestProof)) 56 | } 57 | 58 | /* Add a new canary */ 59 | 60 | type SubmitHandler struct { 61 | state *ServerState 62 | } 63 | 64 | func (h *SubmitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 65 | // parse and verify signature 66 | proof := r.PostFormValue(fugl.SERVER_SUBMIT_FIELD_NAME) 67 | if proof == "" { 68 | SendRequestError(w, "No proof form attribute specified") 69 | return 70 | } 71 | logDebug("New proof submission:\n", proof) 72 | 73 | canary, _, err := fugl.OpenProof(h.state.canaryKey, proof) 74 | if err != nil { 75 | SendRequestError(w, err.Error()) 76 | return 77 | } 78 | if canary == nil { 79 | SendRequestError(w, "Unable to load canary from proof") 80 | return 81 | } 82 | 83 | // check version field 84 | if canary.Version != fugl.CanaryVersion { 85 | logWarning("Invalid canary version field") 86 | SendRequestError(w, "Unsupported canary version") 87 | return 88 | } 89 | 90 | // verify expires in the future 91 | if time.Now().After(canary.Expiry.Time()) { 92 | SendRequestError(w, "Canary must have a deadline in the future") 93 | return 94 | } 95 | 96 | // take write lock 97 | h.state.canaryLock.Lock() 98 | defer h.state.canaryLock.Unlock() 99 | 100 | // verify canary fields 101 | err = fugl.CheckCanary(canary, h.state.latestCanary, time.Now()) 102 | if err != nil { 103 | SendRequestError(w, err.Error()) 104 | return 105 | } 106 | 107 | // save to disk 108 | err = fugl.SaveToDirectory(proof, h.state.storeDir, canary.Expiry.Time()) 109 | if err != nil { 110 | logError("Failed to save valid proof to store:", err) 111 | w.WriteHeader(http.StatusInternalServerError) 112 | return 113 | } 114 | h.state.latestProof = proof 115 | h.state.latestCanary = canary 116 | logInfo("Succesfully added a new canary") 117 | w.WriteHeader(http.StatusNoContent) 118 | } 119 | -------------------------------------------------------------------------------- /pgp_test.go: -------------------------------------------------------------------------------- 1 | package fugl 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "golang.org/x/crypto/openpgp" 7 | "golang.org/x/crypto/openpgp/armor" 8 | "testing" 9 | ) 10 | 11 | var ( 12 | pair keypair 13 | ) 14 | 15 | func init() { 16 | p, err := newKeypair() 17 | if err != nil { 18 | panic(fmt.Sprintf("error creating pgp keys = %v", err)) 19 | } 20 | 21 | if len(p.public) == 0 || len(p.private) == 0 { 22 | panic(fmt.Sprint("public (len=%d) or private (len=%d) keys don't contain data", len(p.public), len(p.private))) 23 | } 24 | 25 | pair = p 26 | } 27 | 28 | func TestPGP__LoadKeypair(t *testing.T) { 29 | // private 30 | priv, err := PGPLoadPrivateKey([]byte(pair.private)) 31 | if err != nil { 32 | t.Fatalf("error reading private key, err=%v", err) 33 | } 34 | if priv == nil { 35 | t.Fatalf("private key is read as nil") 36 | } 37 | 38 | // public 39 | pub, err := PGPLoadPublicKey([]byte(pair.public)) 40 | if err != nil { 41 | t.Fatalf("error reading public key, err=%v", err) 42 | } 43 | if pub == nil { 44 | t.Fatalf("public key is read as nil") 45 | } 46 | 47 | } 48 | 49 | func TestPGP__LoadInvalidKeypair(t *testing.T) { 50 | // private 51 | priv, err := PGPLoadPrivateKey([]byte("")) 52 | if err == nil { 53 | t.Fatal("no error reading invalid key") 54 | } 55 | if priv != nil { 56 | t.Fatal("given a non-nil private key") 57 | } 58 | 59 | // public 60 | pub, err := PGPLoadPublicKey([]byte("")) 61 | if err == nil { 62 | t.Fatal("no error reading invalid pub key") 63 | } 64 | if pub != nil { 65 | t.Fatal("given a non-nil public key") 66 | } 67 | } 68 | 69 | func TestPGP__SignAndVerify(t *testing.T) { 70 | // ignore errors, they should be picked up by __LoadKeypair 71 | pub, _ := PGPLoadPublicKey([]byte(pair.public)) 72 | priv, _ := PGPLoadPrivateKey([]byte(pair.private)) 73 | 74 | // sign and verify a message 75 | message := []byte("this is a test message") 76 | sig, err := PGPSign(priv, message) 77 | if err != nil { 78 | t.Fatalf("error signing message, err=%v", err) 79 | } 80 | if len(sig) == 0 { 81 | t.Fatal("empty signature created") 82 | } 83 | 84 | block, err := PGPVerify(pub, []byte(sig)) 85 | if err != nil { 86 | t.Fatalf("error verifying signature, err=%v", err) 87 | } 88 | if block == nil || len(block.Bytes) == 0 { 89 | t.Fatal("verify block is empty") 90 | } 91 | } 92 | 93 | func TestPGP__InvalidSignAndVerify(t *testing.T) { 94 | pub, _ := PGPLoadPublicKey([]byte(pair.public)) 95 | 96 | // sign and verify a message 97 | message := []byte("this is a test message") 98 | sig, err := PGPSign(pub, message) 99 | if err == nil { 100 | t.Fatal("expected an error when signing with wrong key") 101 | } 102 | if len(sig) != 0 { 103 | t.Fatal("should not get signature on error") 104 | } 105 | 106 | block, err := PGPVerify(pub, []byte("")) 107 | if err == nil { 108 | t.Fatal("expected an error verifying an invalid signature") 109 | } 110 | if block != nil { 111 | t.Fatal("should not get a block on invalid verify") 112 | } 113 | } 114 | 115 | // helper functions to generate keys 116 | 117 | type keypair struct { 118 | public, private string 119 | } 120 | func (p *keypair) empty() bool { 121 | return len(p.public) == 0 || len(p.private) == 0 122 | } 123 | 124 | func newKeypair() (keypair, error) { 125 | p := keypair{} 126 | 127 | entity, err := openpgp.NewEntity("", "", "", nil) 128 | entity.Subkeys = entity.Subkeys[:0] 129 | if len(entity.Identities) != 1 { 130 | return p, nil 131 | } 132 | 133 | // Serialize private key 134 | var secretArmor bytes.Buffer 135 | secArmIn, err := armor.Encode(&secretArmor, openpgp.PrivateKeyType, nil) 136 | if err != nil { 137 | return p, err 138 | } 139 | err = entity.SerializePrivate(secArmIn, nil) 140 | if err != nil { 141 | return p, err 142 | } 143 | err = secArmIn.Close() 144 | if err != nil { 145 | return p, err 146 | } 147 | p.private = secretArmor.String() 148 | 149 | // Serialize public key 150 | var publicArmor bytes.Buffer 151 | pubArmIn, err := armor.Encode(&publicArmor, openpgp.PublicKeyType, nil) 152 | if err != nil { 153 | return p, err 154 | } 155 | err = entity.Serialize(pubArmIn) 156 | if err != nil { 157 | return p, err 158 | } 159 | err = pubArmIn.Close() 160 | if err != nil { 161 | return p, err 162 | } 163 | p.public = publicArmor.String() 164 | 165 | return p, nil 166 | } 167 | -------------------------------------------------------------------------------- /cmd/client/flags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/fatih/color" 7 | "github.com/rot256/fugl" 8 | "os" 9 | "time" 10 | ) 11 | 12 | const tagFlags = "flag" 13 | 14 | type Flags struct { 15 | Proof string // path to proof 16 | PublicKey string // path to pgp public key 17 | PrivateKey string // path to pgp private key 18 | Author string // creator of canary 19 | Description string // file containing canary description 20 | Expire time.Duration // expiration delta 21 | Proxy string // proxy to use (e.g 127.0.0.1:9050) 22 | Address string // address of submission point 23 | Manifest string // manifest, for creating canaries 24 | Operation string // operation to apply 25 | Debug bool // used during development 26 | Json bool // enable json output 27 | Help bool // print help 28 | } 29 | 30 | const ( 31 | FlagNamePublicKey = "public-key" 32 | FlagNamePrivateKey = "private-key" 33 | FlagNameProxy = "proxy" 34 | FlagNameAddress = "address" 35 | FlagNameOperation = "operation" 36 | FlagNameDebug = "debug" 37 | FlagNameHelp = "help" 38 | FlagNameJson = "json" 39 | FlagNameManifest = "manifest" 40 | FlagNameProof = "proof" 41 | ) 42 | 43 | func init() { 44 | flag.Usage = printHelp 45 | } 46 | 47 | type FlagOpt struct { 48 | required map[string]bool 49 | optional map[string]bool 50 | } 51 | 52 | func (opt *FlagOpt) Required(name string, isEnabled bool) { 53 | if opt.required == nil { 54 | opt.required = make(map[string]bool) 55 | } 56 | opt.required[name] = isEnabled 57 | } 58 | 59 | func (opt *FlagOpt) Optional(name string, isEnabled bool) { 60 | if opt.optional == nil { 61 | opt.optional = make(map[string]bool) 62 | } 63 | opt.optional[name] = isEnabled 64 | } 65 | 66 | func exitError(code int, format string, args ...interface{}) { 67 | fmt.Fprintf(os.Stderr, format, args...) 68 | fmt.Fprintf(os.Stderr, "\n") 69 | os.Exit(code) 70 | } 71 | 72 | func (opt FlagOpt) Check() { 73 | okay := true 74 | for name := range opt.required { 75 | okay = okay && opt.required[name] 76 | } 77 | if !okay { 78 | for name := range opt.required { 79 | if !opt.required[name] { 80 | line := fmt.Sprintf("required argument: '%s' (missing)\n", name) 81 | color.Red(line) 82 | } else { 83 | line := fmt.Sprintf("required argument: '%s' (supplied)\n", name) 84 | color.Green(line) 85 | } 86 | } 87 | for name := range opt.optional { 88 | if !opt.optional[name] { 89 | line := fmt.Sprintf("optional argument: '%s' (missing)\n", name) 90 | color.Blue(line) 91 | } else { 92 | line := fmt.Sprintf("optional argument: '%s' (supplied)\n", name) 93 | color.Green(line) 94 | } 95 | } 96 | os.Exit(EXIT_INVALID_ARGUMENTS) 97 | } 98 | } 99 | 100 | func requiredArgument(name string) string { 101 | return fmt.Sprintf("requires argument: '%s'\n", name) 102 | } 103 | 104 | func optionalArgument(name string) string { 105 | return fmt.Sprintf("optional argument: '%s'\n", name) 106 | } 107 | 108 | func parseFlags() Flags { 109 | var flags Flags 110 | flag.StringVar(&flags.Manifest, FlagNameManifest, "./manifest.toml", "canary manifest, for creating new canaries") 111 | flag.StringVar(&flags.Proof, FlagNameProof, "./temp"+fugl.ProofFileExtension, "path to proof") 112 | flag.StringVar(&flags.PrivateKey, FlagNamePrivateKey, "", "path to a PGP private key") 113 | flag.StringVar(&flags.PublicKey, FlagNamePublicKey, "", "path to a PGP public key") 114 | flag.StringVar(&flags.Proxy, FlagNameProxy, "", "socks5 proxy") 115 | flag.StringVar(&flags.Address, FlagNameAddress, "", "address of canary server") 116 | flag.StringVar(&flags.Operation, FlagNameOperation, "", "operation, supported: pull, push, verify") 117 | flag.BoolVar(&flags.Debug, FlagNameDebug, false, "enable debugging") 118 | flag.BoolVar(&flags.Help, FlagNameHelp, false, "print this help page") 119 | flag.Parse() 120 | if flags.Debug { 121 | fmt.Println("flags:", flags) 122 | } 123 | if flags.Help { 124 | printHelp() 125 | os.Exit(EXIT_SUCCESS) 126 | } 127 | if flags.Json { 128 | fmt.Fprintf(os.Stderr, "JSON output not yet supported") 129 | os.Exit(EXIT_INVALID_ARGUMENTS) 130 | } 131 | return flags 132 | } 133 | 134 | func printHelp() { 135 | msg := `Help: 136 | 1. Getting started 137 | This is a client for the fugl canary system. 138 | To use this client you must specify 1 of three operations: 139 | 140 | push : uploads a new canary to a server 141 | pull : downloads the latest canary from the remote 142 | verify : verifies a locally stored canary 143 | create : creates a new canary locally 144 | 145 | Using --operation=[action] 146 | You may specify any one of these to see what arguments they require. 147 | 148 | 2. List of flags: 149 | ` 150 | fmt.Fprintf(os.Stderr, msg) 151 | flag.PrintDefaults() 152 | } 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/rot256/fugl.svg?branch=master)](https://travis-ci.org/rot256/fugl) 2 | 3 | # Fugl 4 | 5 | Fugl ("bird" in Danish) is a [warrant canary](https://en.wikipedia.org/wiki/Warrant_canary) solution/system. 6 | The project attempts to mitigate problems with existing canary solutions: 7 | 8 | > We have seen canaries that are updated on a daily basis and canaries which are updated once per year. 9 | > We have seen canaries that were created once and then never updated again. 10 | > Again, the fact that canaries are non-standard makes it difficult to automatically monitor them for changes or takedowns. 11 | > 12 | > - EFF (Canary Watch – One Year Later) 13 | 14 | The goal is to provide canaries which are: 15 | 16 | - Readable for humans 17 | - Easy to parse for machines 18 | - Simple to update and validate systematically 19 | 20 | Unlike existing canaries this allows for: 21 | 22 | - Automatic creation and submission of new canaries 23 | - Automatic validation of canaries from hundreds of different organizations 24 | - The creation of central services which verify canaries and notifies subscribed users 25 | 26 | ## The format 27 | 28 | Canaries are PGP (clear) signed messages, with a metadata header. 29 | The metadata is stored as JSON and is used for automation, 30 | the remaining content is free text. 31 | Within the project we call a signed canary a proof: 32 | Here is an example of such a proof: 33 | 34 | ``` 35 | -----BEGIN PGP SIGNED MESSAGE----- 36 | Hash: SHA256 37 | 38 | # Test canary 39 | 40 | You can: 41 | 42 | * Explain the purpose of the canary 43 | * Maintain a human readable canary 44 | * Win peoples heart 45 | * Or just, Lorem ipsum dolor sit amet, consectetuer adipiscing elit. 46 | 47 | The description should be valid markdown, to please your fellow humans. 48 | The machines should have no problem understanding it regardless 49 | (just avoid using the "Metadata" header) 50 | 51 | # Metadata 52 | 53 | { 54 | "version": 0, 55 | "author": "Test author", 56 | "creation": "2017-02-11T11:27:54+01:00", 57 | "expiry": "2017-02-21T11:27:54+01:00", 58 | "promises": null, 59 | "nonce": "EujmfYMdYu32Uw-F1LCy-dJjWXmsS2Rm", 60 | "final": false 61 | } 62 | -----BEGIN PGP SIGNATURE----- 63 | 64 | wsBcBAEBCAAQBQJYnucqCRB2OicLdup5+QAAAuQIAFplK8DbIxy1WeasCmuGBYpK 65 | 9xSHFLHB8zulHZ65zpf2sSDQcFWlF3AXfPsP2GpxDyqY16CaUlxYfdJJt1oN4Vzj 66 | h7SrxsRf8/TdimFB6hpc88KOrQp4VfnECJQOpoC/Aqphmp6ZlcM4TPKrxLNv4zYw 67 | neycggHjqp8Od/PwY8tg26H6FJ0waREE6PfKenac2xp4oWVRGlDQyW6tmWN0Zkb5 68 | RcVToAwQi3FgOhrwZfsJhbFZQ3jUZqUSDrSnGOpbXTjXelVzrCmigBjB41MN8U6/ 69 | 4/rk1r3HuZGrpHrAZt1T5oADCzMpXAOgYHIr7Zd7yuaOCkVCBv+F7kzKY8QkI9k= 70 | =B3uj 71 | -----END PGP SIGNATURE----- 72 | ``` 73 | 74 | ## Metadata 75 | 76 | The following fields are found in the current version: 77 | 78 | Name | Description 79 | ---------|------------------------------------------------------------------------------- 80 | Version | Canary structure version 81 | Author | The purported author of the proof 82 | Creation | Time of creation for the canary 83 | Expiry | Time of expiry, a new canary must be submitted before this time 84 | Promises | A set of statements, if a statement is removed from the set users are notified 85 | Final | Flag used for graceful termination of the canary service (see below) 86 | Nonce | Random nonce 87 | 88 | ### Termination 89 | 90 | An organization no longer wishing to supply canaries can set the "Final" flag, 91 | which will indicate to all followers that this canary is the last 92 | and users should expect no further canaries from this source. 93 | 94 | Proofs are generated by the client (see cmd/client) and saved to a file. 95 | This file can then be directly submitted to the server or moved across an air-gap and submitted from another machine. 96 | The submission process is a simply HTTP post request (see next section). 97 | 98 | ## The server 99 | 100 | The canary server is a simple self-contained HTTP server and does not rely on a database server. 101 | All proofs are verified upon submission (using a specified public key) and saved in a directory on the server (sorted by expiry date). 102 | The server serves the proofs and the public key, allowing a client to start tracking the proofs. 103 | 104 | In addition the Fugl canary server can be used as digital [Dead man's switch](https://en.wikipedia.org/wiki/Dead_man's_switch), 105 | by specifying an action (system command) which should be executed by the server if a canary has not been submitted before the expiry time. 106 | 107 | Fugl was explicitly designed so that it does not rely on a single model of distribution. 108 | If you want to save and store the proofs on e.g. an FTP server this is also possible -- as long as clients know how to retrieve the proofs. 109 | The server is included to simplify distribution and automation, the client is the essential part of Fugl. 110 | 111 | ## Getting started 112 | 113 | You can start using Fugl, by setting up a [go environment](https://golang.org/doc/install) and running `make` in the `cmd/client` and `cmd/server` directories. 114 | 115 | If there is interest I will provide pre-compiled binaries (but given the setting I would advise against it). 116 | 117 | ## Todo 118 | 119 | - Further documentation 120 | - JSON input/output option for client 121 | 122 | ## Contributing 123 | 124 | Fugl is still a work in progress, 125 | if you want to contribute the best way to do so is by: 126 | 127 | - Read the code (it is short and quite readable) 128 | - Provide suggestions for Canary format/API changes 129 | 130 | Simplicity is prioritized over new features! 131 | 132 | This repo WILL NOT contain an automatic system for tracking the canaries, 133 | such a system should be easy to implement using the client (and I encourage people to do so). 134 | Alternatively this repository can be used as a library. 135 | --------------------------------------------------------------------------------