From d3eb674b30f65012e50cbc8013f7da7588ac5011 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Fri, 21 Aug 2015 09:22:52 +0200 Subject: [PATCH] Add a signature package and stsigtool CLI utility --- cmd/stsigtool/main.go | 102 +++++++++++++++++ lib/signature/signature.go | 195 ++++++++++++++++++++++++++++++++ lib/signature/signature_test.go | 81 +++++++++++++ 3 files changed, 378 insertions(+) create mode 100644 cmd/stsigtool/main.go create mode 100644 lib/signature/signature.go create mode 100644 lib/signature/signature_test.go 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") + } +}