Просмотр исходного кода

Merge pull request #2179 from calmh/signedrels

Use signed releases for automatic upgrade
Audrius Butkevicius 10 лет назад
Родитель
Сommit
c46db0761e
7 измененных файлов с 544 добавлено и 133 удалено
  1. 5 4
      .gitignore
  2. 36 23
      build.go
  3. 102 0
      cmd/stsigtool/main.go
  4. 195 0
      lib/signature/signature.go
  5. 81 0
      lib/signature/signature_test.go
  6. 19 0
      lib/upgrade/signingkey.go
  7. 106 106
      lib/upgrade/upgrade_supported.go

+ 5 - 4
.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

+ 36 - 23
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()
 }
 

+ 102 - 0
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 <command>
+
+Where command is one of:
+
+	gen
+		- generate a new key pair
+
+	sign <privkeyfile> <datafile>
+		- sign a file
+
+	verify <pubkeyfile> <signaturefile> <datafile>
+		- 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)
+	}
+}

+ 195 - 0
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
+}

+ 81 - 0
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")
+	}
+}

+ 19 - 0
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-----`)

+ 106 - 106
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
-			}
-
-			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)
-			}
+		err = archiveFileVisitor(dir, &tempName, &sig, shortName, tr)
+		if err != nil {
+			return "", err
+		}
 
-			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
-			}
+		if tempName != "" && sig != nil {
+			break
+		}
+	}
 
-		case "syncthing.exe.md5":
-			inFile, err := file.Open()
-			if err != nil {
-				return "", err
-			}
-			bs, err := ioutil.ReadAll(inFile)
-			if err != nil {
-				return "", err
-			}
+	if err := verifyUpgrade(tempName, sig); err != nil {
+		return "", err
+	}
 
-			expectedMD5 = strings.TrimSpace(string(bs))
-			if debug {
-				l.Debugln("expected md5 is", actualMD5)
-			}
+	return tempName, nil
+}
 
-			if actualMD5 != "" {
-				// We're done
-				break fileLoop
-			}
+// 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
 		}
-	}
 
-	if tempName != "" {
-		// We found and saved something to disk.
-		if expectedMD5 == "" || actualMD5 == expectedMD5 {
-			return tempName, nil
+	case "syncthing.sig", "syncthing.exe.sig":
+		if debug {
+			l.Debugln("reading signature")
+		}
+		*signature, err = ioutil.ReadAll(filedata)
+		if err != nil {
+			return err
 		}
-		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")
 	}
-	return "", fmt.Errorf("No upgrade found")
+
+	return nil
 }
 
-func writeBinary(dir string, inFile io.Reader) (filename, md5sum string, err error) {
-	outFile, err := ioutil.TempFile(dir, "syncthing")
+func verifyUpgrade(tempName string, sig []byte) error {
+	if tempName == "" {
+		return fmt.Errorf("no upgrade found")
+	}
+	if sig == nil {
+		return fmt.Errorf("no signature found")
+	}
+
+	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 {
-		return "", "", err
+		os.Remove(tempName)
+		return err
 	}
 
-	// Write the binary both a temporary file and to the MD5 hasher.
+	return nil
+}
+
+func writeBinary(dir string, inFile io.Reader) (filename string, err error) {
+	// Write the binary to a temporary file.
 
-	h := md5.New()
-	mw := io.MultiWriter(h, outFile)
+	outFile, err := ioutil.TempFile(dir, "syncthing")
+	if err != nil {
+		return "", err
+	}
 
-	_, err = io.Copy(mw, inFile)
+	_, 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
-	}
-
-	actualMD5 := fmt.Sprintf("%x", h.Sum(nil))
-	if debug {
-		l.Debugln("actual md5 is", actualMD5)
+		return "", err
 	}
 
-	return outFile.Name(), actualMD5, nil
+	return outFile.Name(), nil
 }