diff --git a/.gitignore b/.gitignore index ab1052d36..946c7554b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -./syncthing +syncthing +!gui/syncthing +!Godeps/_workspace/src/github.com/syncthing syncthing.exe *.tar.gz *.zip @@ -9,8 +11,7 @@ files/pidx bin perfstats*.csv coverage.xml -!gui/scripts/syncthing -syncthing.md5 -syncthing.exe.md5 +syncthing.sig +syncthing.exe.sig RELEASE deb diff --git a/build.go b/build.go index 293cea2a0..c708bfb40 100644 --- a/build.go +++ b/build.go @@ -13,7 +13,6 @@ import ( "archive/zip" "bytes" "compress/gzip" - "crypto/md5" "flag" "fmt" "io" @@ -28,16 +27,19 @@ import ( "strconv" "strings" "time" + + "github.com/syncthing/syncthing/lib/signature" ) var ( - versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`) - goarch string - goos string - noupgrade bool - version string - goVersion float64 - race bool + versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`) + goarch string + goos string + noupgrade bool + version string + goVersion float64 + race bool + signingKey string ) const minGoVersion = 1.3 @@ -62,6 +64,7 @@ func main() { flag.BoolVar(&noupgrade, "no-upgrade", noupgrade, "Disable upgrade functionality") flag.StringVar(&version, "version", getVersion(), "Set compiled in version string") flag.BoolVar(&race, "race", race, "Use race detector") + flag.StringVar(&signingKey, "sign", signingKey, "Private key file for signing binaries") flag.Parse() switch goarch { @@ -215,7 +218,7 @@ func build(pkg string, tags []string) { binary += ".exe" } - rmr(binary, binary+".md5") + rmr(binary, binary+".sig") args := []string{"build", "-ldflags", ldflags()} if len(tags) > 0 { args = append(args, "-tags", strings.Join(tags, ",")) @@ -227,11 +230,13 @@ func build(pkg string, tags []string) { setBuildEnv() runPrint("go", args...) - // Create an md5 checksum of the binary, to be included in the archive for - // automatic upgrades. - err := md5File(binary) - if err != nil { - log.Fatal(err) + if signingKey != "" { + // Create an signature of the binary, to be included in the archive for + // automatic upgrades. + err := signFile(signingKey, binary) + if err != nil { + log.Fatal(err) + } } } @@ -249,7 +254,10 @@ func buildTar() { {src: "LICENSE", dst: name + "/LICENSE.txt"}, {src: "AUTHORS", dst: name + "/AUTHORS.txt"}, {src: "syncthing", dst: name + "/syncthing"}, - {src: "syncthing.md5", dst: name + "/syncthing.md5"}, + } + + if _, err := os.Stat("syncthing.sig"); err == nil { + files = append(files, archiveFile{src: "syncthing.sig", dst: name + "/syncthing.sig"}) } for _, file := range listFiles("etc") { @@ -277,7 +285,10 @@ func buildZip() { {src: "LICENSE", dst: name + "/LICENSE.txt"}, {src: "AUTHORS", dst: name + "/AUTHORS.txt"}, {src: "syncthing.exe", dst: name + "/syncthing.exe"}, - {src: "syncthing.exe.md5", dst: name + "/syncthing.exe.md5"}, + } + + if _, err := os.Stat("syncthing.exe.sig"); err == nil { + files = append(files, archiveFile{src: "syncthing.exe.sig", dst: name + "/syncthing.exe.sig"}) } for _, file := range listFiles("extra") { @@ -712,29 +723,31 @@ func zipFile(out string, files []archiveFile) { } } -func md5File(file string) error { +func signFile(keyname, file string) error { + privkey, err := ioutil.ReadFile(keyname) + if err != nil { + return err + } + fd, err := os.Open(file) if err != nil { return err } defer fd.Close() - h := md5.New() - _, err = io.Copy(h, fd) + sig, err := signature.Sign(privkey, fd) if err != nil { return err } - out, err := os.Create(file + ".md5") + out, err := os.Create(file + ".sig") if err != nil { return err } - - _, err = fmt.Fprintf(out, "%x\n", h.Sum(nil)) + _, err = out.Write(sig) if err != nil { return err } - return out.Close() } diff --git a/cmd/stsigtool/main.go b/cmd/stsigtool/main.go new file mode 100644 index 000000000..6e10d6fc8 --- /dev/null +++ b/cmd/stsigtool/main.go @@ -0,0 +1,102 @@ +// Copyright (C) 2015 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package main + +import ( + "flag" + "io/ioutil" + "log" + "os" + + "github.com/syncthing/syncthing/lib/signature" +) + +func main() { + log.SetFlags(0) + log.SetOutput(os.Stdout) + + flag.Parse() + + if flag.NArg() < 1 { + log.Println(`Usage: + stsigtool + +Where command is one of: + + gen + - generate a new key pair + + sign + - sign a file + + verify + - verify a signature +`) + } + + switch flag.Arg(0) { + case "gen": + gen() + case "sign": + sign(flag.Arg(1), flag.Arg(2)) + case "verify": + verify(flag.Arg(1), flag.Arg(2), flag.Arg(3)) + } +} + +func gen() { + priv, pub, err := signature.GenerateKeys() + if err != nil { + log.Fatal(err) + } + + os.Stdout.Write(priv) + os.Stdout.Write(pub) +} + +func sign(keyname, dataname string) { + privkey, err := ioutil.ReadFile(keyname) + if err != nil { + log.Fatal(err) + } + + fd, err := os.Open(dataname) + if err != nil { + log.Fatal(err) + } + defer fd.Close() + + sig, err := signature.Sign(privkey, fd) + if err != nil { + log.Fatal(err) + } + + os.Stdout.Write(sig) +} + +func verify(keyname, signame, dataname string) { + pubkey, err := ioutil.ReadFile(keyname) + if err != nil { + log.Fatal(err) + } + + sig, err := ioutil.ReadFile(signame) + if err != nil { + log.Fatal(err) + } + + fd, err := os.Open(dataname) + if err != nil { + log.Fatal(err) + } + defer fd.Close() + + err = signature.Verify(pubkey, sig, fd) + if err != nil { + log.Fatal(err) + } +} diff --git a/lib/signature/signature.go b/lib/signature/signature.go new file mode 100644 index 000000000..d988cb8b9 --- /dev/null +++ b/lib/signature/signature.go @@ -0,0 +1,195 @@ +// Copyright (C) 2015 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package signature provides simple methods to create and verify signatures +// in PEM format. +package signature + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + "errors" + "fmt" + "io" + "math/big" +) + +// GenerateKeys returns a new key pair, with the private and public key +// encoded in PEM format. +func GenerateKeys() (privKey []byte, pubKey []byte, err error) { + // Generate a new key pair + key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + return nil, nil, err + } + + // Marshal the private key + bs, err := x509.MarshalECPrivateKey(key) + if err != nil { + return nil, nil, err + } + + // Encode it in PEM format + privKey = pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: bs, + }) + + // Marshal the public key + bs, err = x509.MarshalPKIXPublicKey(key.Public()) + if err != nil { + return nil, nil, err + } + + // Encode it in PEM format + pubKey = pem.EncodeToMemory(&pem.Block{ + Type: "EC PUBLIC KEY", + Bytes: bs, + }) + + return +} + +// Sign computes the hash of data and signs it with the private key, returning +// a signature in PEM format. +func Sign(privKeyPEM []byte, data io.Reader) ([]byte, error) { + // Parse the private key + key, err := loadPrivateKey(privKeyPEM) + if err != nil { + return nil, err + } + + // Hash the reader data + hash, err := hashReader(data) + if err != nil { + return nil, err + } + + // Sign the hash + r, s, err := ecdsa.Sign(rand.Reader, key, hash) + if err != nil { + return nil, err + } + + // Marshal the signature using ASN.1 + sig, err := marshalSignature(r, s) + if err != nil { + return nil, err + } + + // Encode it in a PEM block + bs := pem.EncodeToMemory(&pem.Block{ + Type: "SIGNATURE", + Bytes: sig, + }) + + return bs, nil +} + +// Verify computes the hash of data and compares it to the signature using the +// given public key. Returns nil if the signature is correct. +func Verify(pubKeyPEM []byte, signature []byte, data io.Reader) error { + // Parse the public key + key, err := loadPublicKey(pubKeyPEM) + if err != nil { + return err + } + + // Parse the signature + block, _ := pem.Decode(signature) + r, s, err := unmarshalSignature(block.Bytes) + if err != nil { + return err + } + + // Compute the hash of the data + hash, err := hashReader(data) + if err != nil { + return err + } + + // Verify the signature + if !ecdsa.Verify(key, hash, r, s) { + return errors.New("incorrect signature") + } + + return nil +} + +// hashReader returns the SHA256 hash of the reader +func hashReader(r io.Reader) ([]byte, error) { + h := sha256.New() + if _, err := io.Copy(h, r); err != nil { + return nil, err + } + hash := []byte(fmt.Sprintf("%x", h.Sum(nil))) + return hash, nil +} + +// loadPrivateKey returns the ECDSA private key structure for the given PEM +// data. +func loadPrivateKey(bs []byte) (*ecdsa.PrivateKey, error) { + block, _ := pem.Decode(bs) + return x509.ParseECPrivateKey(block.Bytes) +} + +// loadPublicKey returns the ECDSA public key structure for the given PEM +// data. +func loadPublicKey(bs []byte) (*ecdsa.PublicKey, error) { + // Decode and parse the public key PEM block + block, _ := pem.Decode(bs) + intf, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + // It should be an ECDSA public key + pk, ok := intf.(*ecdsa.PublicKey) + if !ok { + return nil, errors.New("unsupported public key format") + } + + return pk, nil +} + +// A wrapper around the signature integers so that we can marshal and +// unmarshal them. +type signature struct { + R, S *big.Int +} + +// marhalSignature returns ASN.1 encoded bytes for the given integers, +// suitable for PEM encoding. +func marshalSignature(r, s *big.Int) ([]byte, error) { + sig := signature{ + R: r, + S: s, + } + + bs, err := asn1.Marshal(sig) + if err != nil { + return nil, err + } + + return bs, nil +} + +// unmarshalSignature returns the R and S integers from the given ASN.1 +// encoded signature. +func unmarshalSignature(sig []byte) (r *big.Int, s *big.Int, err error) { + var ts signature + _, err = asn1.Unmarshal(sig, &ts) + if err != nil { + return nil, nil, err + } + + return ts.R, ts.S, nil +} diff --git a/lib/signature/signature_test.go b/lib/signature/signature_test.go new file mode 100644 index 000000000..dfe1bcedd --- /dev/null +++ b/lib/signature/signature_test.go @@ -0,0 +1,81 @@ +// Copyright (C) 2015 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package signature_test + +import ( + "bytes" + "testing" + + "github.com/syncthing/syncthing/lib/signature" +) + +var ( + // A private key for signing + privKey = []byte(`-----BEGIN EC PRIVATE KEY----- +MIHbAgEBBEFGXB1IgefFF6kSyE17xIAU7fDIn07sPnGf1kLOCVrEZyUbnAmNFk8u +lUt/knnvo+Gw1i9ucFjmtYtzDevrhSlG5aAHBgUrgQQAI6GBiQOBhgAEASlcbcgJ +4PN+TSnAYiMlA0I/PRtFrDCgrt27K7hR+U7Afjc4KqW+QYwoRLvxueNh7gUK+zc0 +Aqrk3z+O1epiQTq8ACikHUXsx/bSzEFlPdMygUAAj3hChlgCL6/vOocuRUbtAqc6 +Zr0L9px+J4L0K+uqhyhKya7y6QLJrYPovFq3A7AK +-----END EC PRIVATE KEY-----`) + + // The matching public key + pubKey = []byte(`-----BEGIN EC PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBKVxtyAng835NKcBiIyUDQj89G0Ws +MKCu3bsruFH5TsB+Nzgqpb5BjChEu/G542HuBQr7NzQCquTfP47V6mJBOrwAKKQd +RezH9tLMQWU90zKBQACPeEKGWAIvr+86hy5FRu0CpzpmvQv2nH4ngvQr66qHKErJ +rvLpAsmtg+i8WrcDsAo= +-----END EC PUBLIC KEY-----`) + + // A signature of "this is a string to sign" created with the private key + // above + exampleSig = []byte(`-----BEGIN SIGNATURE----- +MIGGAkFdHjdarlFOrtcnCqcb0BX7Mjjq/Sbgp4mopCxBwXmfamtCeRGhZJ5MikyD +VXScaJ2Dq2Ov7L4/gTcYj9fZwcrWgQJBc7+tcw5fpO0/y8DNq0t3g9bqt2MkmoNm +eSAM8Fze4usVXHEi+QeMuYM2IKeVPyAR3iyl5gflVul9NRXS3OPAH3A= +-----END SIGNATURE-----`) +) + +func TestGenerateKeys(t *testing.T) { + priv, pub, err := signature.GenerateKeys() + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(priv, []byte("PRIVATE KEY")) { + t.Fatal("should be a private key") + } + if !bytes.Contains(pub, []byte("PUBLIC KEY")) { + t.Fatal("should be a private key") + } +} + +func TestSign(t *testing.T) { + data := bytes.NewReader([]byte("this is a string to sign")) + + s, err := signature.Sign(privKey, data) + if err != nil { + t.Fatal(err) + } + + if !bytes.Contains(s, []byte("SIGNATURE")) { + t.Error("should be a signature") + } +} + +func TestVerify(t *testing.T) { + data := bytes.NewReader([]byte("this is a string to sign")) + err := signature.Verify(pubKey, exampleSig, data) + if err != nil { + t.Fatal(err) + } + + data = bytes.NewReader([]byte("thus is a string to sign")) + err = signature.Verify(pubKey, exampleSig, data) + if err == nil { + t.Fatal("signature should not match") + } +} diff --git a/lib/upgrade/signingkey.go b/lib/upgrade/signingkey.go new file mode 100644 index 000000000..9619d46c1 --- /dev/null +++ b/lib/upgrade/signingkey.go @@ -0,0 +1,19 @@ +// Copyright (C) 2015 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package upgrade + +// This is the public key used to verify signed upgrades. It must match the +// private key used to sign binaries for the built in upgrade mechanism to +// accept an upgrade. Keys and signatures can be created and verified with the +// stsigtool utility. The build script creates signed binaries when given the +// -sign option. +var SigningKey = []byte(`-----BEGIN EC PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA1iRk+p+DsmolixxVKcpEVlMDPOeQ +1dWthURMqsjxoJuDAe5I98P/A0kXSdBI7avm5hXhX2opJ5TAyBZLHPpDTRoBg4WN +7jUpeAjtPoVVxvOh37qDeDVcjCgJbbDTPKbjxq/Ae3SHlQMRcoes7lVY1+YJ8dPk +2oPfjA6jtmo9aVbf/uo= +-----END EC PUBLIC KEY-----`) diff --git a/lib/upgrade/upgrade_supported.go b/lib/upgrade/upgrade_supported.go index 5dc6371b4..2abb8be87 100644 --- a/lib/upgrade/upgrade_supported.go +++ b/lib/upgrade/upgrade_supported.go @@ -13,7 +13,7 @@ import ( "archive/zip" "bytes" "compress/gzip" - "crypto/md5" + "crypto/tls" "encoding/json" "fmt" "io" @@ -25,12 +25,27 @@ import ( "runtime" "sort" "strings" + + "github.com/syncthing/syncthing/lib/signature" ) +// This is an HTTP/HTTPS client that does *not* perform certificate +// validation. We do this because some systems where Syncthing runs have +// issues with old or missing CA roots. It doesn't actually matter that we +// load the upgrade insecurely as we verify an ECDSA signature of the actual +// binary contents before accepting the upgrade. +var insecureHTTP = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, +} + // LatestGithubReleases returns the latest releases, including prereleases or // not depending on the argument func LatestGithubReleases(version string) ([]Release, error) { - resp, err := http.Get("https://api.github.com/repos/syncthing/syncthing/releases?per_page=30") + resp, err := insecureHTTP.Get("https://api.github.com/repos/syncthing/syncthing/releases?per_page=30") if err != nil { return nil, err } @@ -144,7 +159,7 @@ func readRelease(dir, url string) (string, error) { } req.Header.Add("Accept", "application/octet-stream") - resp, err := http.DefaultClient.Do(req) + resp, err := insecureHTTP.Do(req) if err != nil { return "", err } @@ -166,10 +181,10 @@ func readTarGz(dir string, r io.Reader) (string, error) { tr := tar.NewReader(gr) - var tempName, actualMD5, expectedMD5 string + var tempName string + var sig []byte // Iterate through the files in the archive. -fileLoop: for { hdr, err := tr.Next() if err == io.EOF { @@ -186,50 +201,21 @@ fileLoop: l.Debugf("considering file %q", shortName) } - switch shortName { - case "syncthing": - if debug { - l.Debugln("writing and hashing binary") - } - tempName, actualMD5, err = writeBinary(dir, tr) - if err != nil { - return "", err - } + err = archiveFileVisitor(dir, &tempName, &sig, shortName, tr) + if err != nil { + return "", err + } - if expectedMD5 != "" { - // We're done - break fileLoop - } - - case "syncthing.md5": - bs, err := ioutil.ReadAll(tr) - if err != nil { - return "", err - } - - expectedMD5 = strings.TrimSpace(string(bs)) - if debug { - l.Debugln("expected md5 is", actualMD5) - } - - if actualMD5 != "" { - // We're done - break fileLoop - } + if tempName != "" && sig != nil { + break } } - if tempName != "" { - // We found and saved something to disk. - if expectedMD5 == "" || actualMD5 == expectedMD5 { - return tempName, nil - } - os.Remove(tempName) - // There was an md5 file included in the archive, and it doesn't - // match what we just wrote to disk. - return "", fmt.Errorf("incorrect MD5 checksum") + if err := verifyUpgrade(tempName, sig); err != nil { + return "", err } - return "", fmt.Errorf("no upgrade found") + + return tempName, nil } func readZip(dir string, r io.Reader) (string, error) { @@ -243,10 +229,10 @@ func readZip(dir string, r io.Reader) (string, error) { return "", err } - var tempName, actualMD5, expectedMD5 string + var tempName string + var sig []byte // Iterate through the files in the archive. -fileLoop: for _, file := range archive.File { shortName := path.Base(file.Name) @@ -254,94 +240,108 @@ fileLoop: l.Debugf("considering file %q", shortName) } - switch shortName { - case "syncthing.exe": - if debug { - l.Debugln("writing and hashing binary") - } + inFile, err := file.Open() + if err != nil { + return "", err + } - inFile, err := file.Open() - if err != nil { - return "", err - } - tempName, actualMD5, err = writeBinary(dir, inFile) - if err != nil { - return "", err - } + err = archiveFileVisitor(dir, &tempName, &sig, shortName, inFile) + inFile.Close() + if err != nil { + return "", err + } - if expectedMD5 != "" { - // We're done - break fileLoop - } - - case "syncthing.exe.md5": - inFile, err := file.Open() - if err != nil { - return "", err - } - bs, err := ioutil.ReadAll(inFile) - if err != nil { - return "", err - } - - expectedMD5 = strings.TrimSpace(string(bs)) - if debug { - l.Debugln("expected md5 is", actualMD5) - } - - if actualMD5 != "" { - // We're done - break fileLoop - } + if tempName != "" && sig != nil { + break } } - if tempName != "" { - // We found and saved something to disk. - if expectedMD5 == "" || actualMD5 == expectedMD5 { - return tempName, nil - } - os.Remove(tempName) - // There was an md5 file included in the archive, and it doesn't - // match what we just wrote to disk. - return "", fmt.Errorf("incorrect MD5 checksum") + if err := verifyUpgrade(tempName, sig); err != nil { + return "", err } - return "", fmt.Errorf("No upgrade found") + + return tempName, nil } -func writeBinary(dir string, inFile io.Reader) (filename, md5sum string, err error) { - outFile, err := ioutil.TempFile(dir, "syncthing") - if err != nil { - return "", "", err +// archiveFileVisitor is called for each file in an archive. It may set +// tempFile and signature. +func archiveFileVisitor(dir string, tempFile *string, signature *[]byte, filename string, filedata io.Reader) error { + var err error + switch filename { + case "syncthing", "syncthing.exe": + if debug { + l.Debugln("reading binary") + } + *tempFile, err = writeBinary(dir, filedata) + if err != nil { + return err + } + + case "syncthing.sig", "syncthing.exe.sig": + if debug { + l.Debugln("reading signature") + } + *signature, err = ioutil.ReadAll(filedata) + if err != nil { + return err + } } - // Write the binary both a temporary file and to the MD5 hasher. + return nil +} - h := md5.New() - mw := io.MultiWriter(h, outFile) +func verifyUpgrade(tempName string, sig []byte) error { + if tempName == "" { + return fmt.Errorf("no upgrade found") + } + if sig == nil { + return fmt.Errorf("no signature found") + } - _, err = io.Copy(mw, inFile) + if debug { + l.Debugf("checking signature\n%s", sig) + } + + fd, err := os.Open(tempName) + if err != nil { + return err + } + err = signature.Verify(SigningKey, sig, fd) + fd.Close() + + if err != nil { + os.Remove(tempName) + return err + } + + return nil +} + +func writeBinary(dir string, inFile io.Reader) (filename string, err error) { + // Write the binary to a temporary file. + + outFile, err := ioutil.TempFile(dir, "syncthing") + if err != nil { + return "", err + } + + _, err = io.Copy(outFile, inFile) if err != nil { os.Remove(outFile.Name()) - return "", "", err + return "", err } err = outFile.Close() if err != nil { os.Remove(outFile.Name()) - return "", "", err + return "", err } err = os.Chmod(outFile.Name(), os.FileMode(0755)) if err != nil { os.Remove(outFile.Name()) - return "", "", err + return "", err } - actualMD5 := fmt.Sprintf("%x", h.Sum(nil)) - if debug { - l.Debugln("actual md5 is", actualMD5) - } - - return outFile.Name(), actualMD5, nil + return outFile.Name(), nil }