"veyron/security/serialization": Serialization Library

This CL adds a general serialization library that  defines:
(1) A SigningWriter for writing data (to a given data writer) and a
signature of the data (to a given signature writer)
(2) A VerifyingReader for reading and verifying data written by a
SigningWriter.

The IDManager within wspr is modified to use this serialization library.

Change-Id: Ib685215943a089149f72133706579c4fa6cb63b6
diff --git a/security/serialization/serialization.go b/security/serialization/serialization.go
new file mode 100644
index 0000000..50a52e3
--- /dev/null
+++ b/security/serialization/serialization.go
@@ -0,0 +1,5 @@
+// Package serialization defines a general-purpose io.WriteCloser
+// for writing data along with an appropriate signature that
+// establishes integrity and authenticity of data, and an io.Reader
+// for reading the data after verifying the signature.
+package serialization
diff --git a/security/serialization/serialization_test.go b/security/serialization/serialization_test.go
new file mode 100644
index 0000000..524e6f3
--- /dev/null
+++ b/security/serialization/serialization_test.go
@@ -0,0 +1,142 @@
+package serialization
+
+import (
+	"bytes"
+	"crypto/ecdsa"
+	"fmt"
+	"io"
+	"io/ioutil"
+	mrand "math/rand"
+	"reflect"
+	"strings"
+	"testing"
+
+	"veyron/lib/testutil"
+
+	"veyron2/rt"
+	"veyron2/security"
+)
+
+type bufferCloser struct {
+	bytes.Buffer
+}
+
+func (*bufferCloser) Close() error {
+	return nil
+}
+
+func signingWrite(d, s io.WriteCloser, signer security.Signer, writeList [][]byte, opts *Options) error {
+	swc, err := NewSigningWriteCloser(d, s, signer, opts)
+	if err != nil {
+		return fmt.Errorf("NewSigningWriteCloser failed: %s", err)
+	}
+	for _, b := range writeList {
+		if _, err := swc.Write(b); err != nil {
+			return fmt.Errorf("signingWriteCloser.Write failed: %s", err)
+		}
+	}
+	if err := swc.Close(); err != nil {
+		return fmt.Errorf("signingWriteCloser.Close failed: %s", err)
+	}
+	return nil
+}
+
+func verifyingRead(d, s io.Reader, key *ecdsa.PublicKey) ([]byte, error) {
+	vr, err := NewVerifyingReader(d, s, key)
+	if err != nil {
+		return nil, fmt.Errorf("NewVerifyingReader failed: %s", err)
+	}
+	return ioutil.ReadAll(vr)
+}
+
+func newSigner() security.Signer {
+	// TODO(ashankar,ataly): Remove use of "rt" here and replace with a factory
+	// function for PrivateID/Signer when possible.
+	r, err := rt.New()
+	if err != nil {
+		panic(err)
+	}
+	id, err := r.NewIdentity("irrelevant")
+	if err != nil {
+		panic(err)
+	}
+	return id
+}
+
+func matchesErrorPattern(err error, pattern string) bool {
+	if (len(pattern) == 0) != (err == nil) {
+		return false
+	}
+	return err == nil || strings.Index(err.Error(), pattern) >= 0
+}
+
+func TestRoundTrip(t *testing.T) {
+	signer := newSigner()
+	d, s := &bufferCloser{}, &bufferCloser{}
+
+	testdata := []struct {
+		writeList [][]byte
+		opts      *Options
+	}{
+		{[][]byte{testutil.RandomBytes(1)}, nil},
+		{[][]byte{testutil.RandomBytes(100)}, nil},
+		{[][]byte{testutil.RandomBytes(100)}, &Options{ChunkSizeBytes: 10}},
+		{[][]byte{testutil.RandomBytes(25), testutil.RandomBytes(15), testutil.RandomBytes(60), testutil.RandomBytes(5)}, &Options{ChunkSizeBytes: 7}},
+	}
+	for _, test := range testdata {
+		d.Reset()
+		s.Reset()
+
+		if err := signingWrite(d, s, signer, test.writeList, test.opts); err != nil {
+			t.Errorf("signingWrite(_, _, %v, %v) failed: %s", test.writeList, test.opts, err)
+			continue
+		}
+		dataRead, err := verifyingRead(d, s, signer.PublicKey())
+		if err != nil {
+			t.Errorf("verifyingRead failed: %s", err)
+			continue
+		}
+
+		dataWritten := bytes.Join(test.writeList, nil)
+		if !reflect.DeepEqual(dataRead, dataWritten) {
+			t.Errorf("Read-Write mismatch: data read: %v, data written: %v", dataRead, dataWritten)
+			continue
+		}
+	}
+}
+
+func TestIntegrityAndAuthenticity(t *testing.T) {
+	tamper := func(b []byte) []byte {
+		c := make([]byte, len(b))
+		copy(c, b)
+		c[mrand.Int()%len(b)] += 1
+		return c
+	}
+
+	signer := newSigner()
+	d, s := &bufferCloser{}, &bufferCloser{}
+	if err := signingWrite(d, s, signer, [][]byte{testutil.RandomBytes(100)}, &Options{ChunkSizeBytes: 7}); err != nil {
+		t.Fatalf("signingWrite failed: %s", err)
+	}
+
+	// copy the data and signature bytes written.
+	dataBytes := d.Bytes()
+	sigBytes := s.Bytes()
+
+	// Test that any tampering of the data bytes, or any change
+	// to the signer causes a verifyingRead to fail.
+	testdata := []struct {
+		dataBytes, sigBytes []byte
+		key                 *ecdsa.PublicKey
+		wantErr             string
+	}{
+		{dataBytes, sigBytes, signer.PublicKey(), ""},
+		{dataBytes, sigBytes, newSigner().PublicKey(), "signature verification failed"},
+		{tamper(dataBytes), sigBytes, signer.PublicKey(), "data has been modified"},
+	}
+	for _, test := range testdata {
+		if _, err := verifyingRead(&bufferCloser{*bytes.NewBuffer(test.dataBytes)}, &bufferCloser{*bytes.NewBuffer(test.sigBytes)}, test.key); !matchesErrorPattern(err, test.wantErr) {
+			t.Errorf("verifyingRead: got error: %s, want to match: %v", err, test.wantErr)
+		}
+	}
+}
diff --git a/security/serialization/signing_writer.go b/security/serialization/signing_writer.go
new file mode 100644
index 0000000..643fe69
--- /dev/null
+++ b/security/serialization/signing_writer.go
@@ -0,0 +1,145 @@
+package serialization
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"hash"
+	"io"
+
+	"veyron2/security"
+	"veyron2/vom"
+)
+
+const defaultChunkSizeBytes = 1 << 20
+
+type header struct {
+	ChunkSizeBytes int
+}
+
+// signingWriter implements io.WriteCloser.
+type signingWriter struct {
+	data      io.WriteCloser
+	signature io.WriteCloser
+	signer    security.Signer
+
+	chunkSizeBytes int
+	curChunk       bytes.Buffer
+	signatureHash  hash.Hash
+	sigEnc         *vom.Encoder
+}
+
+func (w *signingWriter) Write(p []byte) (int, error) {
+	bytesWritten := 0
+	for len(p) > 0 {
+		pLimited := p
+		curChunkFreeBytes := w.chunkSizeBytes - w.curChunk.Len()
+		if len(pLimited) > curChunkFreeBytes {
+			pLimited = pLimited[:curChunkFreeBytes]
+		}
+
+		n, err := w.curChunk.Write(pLimited)
+		bytesWritten = bytesWritten + n
+		if err != nil {
+			return bytesWritten, err
+		}
+		p = p[n:]
+
+		if err := w.commitChunk(false); err != nil {
+			return bytesWritten, err
+		}
+	}
+	return bytesWritten, nil
+}
+
+func (w *signingWriter) Close() error {
+	if w.curChunk.Len() > 0 {
+		if err := w.commitChunk(true); err != nil {
+			defer w.close()
+			return err
+		}
+	}
+	if err := w.commitSignature(); err != nil {
+		defer w.close()
+		return err
+	}
+	return w.close()
+}
+
+// Options specifies parameters to tune a SigningWriteCloser.
+type Options struct {
+	// ChunkSizeBytes controls the maximum amount of memory devoted to buffering
+	// data provided to Write calls. See NewSigningWriteCloser.
+	ChunkSizeBytes int
+}
+
+// NewSigningWriteCloser returns an io.WriteCloser that writes data along
+// with an appropriate signature that establishes the integrity and
+// authenticity of the data. It behaves as follows:
+// * A Write call writes chunks (of size provided by the Options or 1MB by default)
+//   of data to the provided data WriteCloser and a hash of the chunks to the provided
+//   signature WriteCloser.
+// * A Close call writes a signature (computed using the provided signer) of
+//   all the hashes written, and then closes the data and signature WriteClosers.
+func NewSigningWriteCloser(data, signature io.WriteCloser, s security.Signer, opts *Options) (io.WriteCloser, error) {
+	if (data == nil) || (signature == nil) {
+		return nil, errors.New("data or signature WriteCloser is nil")
+	}
+	w := &signingWriter{data: data, signature: signature, signer: s, signatureHash: sha256.New(), chunkSizeBytes: defaultChunkSizeBytes, sigEnc: vom.NewEncoder(signature)}
+
+	if opts != nil {
+		w.chunkSizeBytes = opts.ChunkSizeBytes
+	}
+
+	if err := w.commitHeader(); err != nil {
+		return nil, err
+	}
+	return w, nil
+}
+
+func (w *signingWriter) commitHeader() error {
+	if err := binary.Write(w.signatureHash, binary.LittleEndian, int64(w.chunkSizeBytes)); err != nil {
+		return err
+	}
+	if err := w.sigEnc.Encode(header{w.chunkSizeBytes}); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (w *signingWriter) commitChunk(force bool) error {
+	if !force && w.curChunk.Len() < w.chunkSizeBytes {
+		return nil
+	}
+
+	hashBytes := sha256.Sum256(w.curChunk.Bytes())
+	if _, err := io.CopyN(w.data, &w.curChunk, int64(w.curChunk.Len())); err != nil {
+		return err
+	}
+	if _, err := w.signatureHash.Write(hashBytes[:]); err != nil {
+		return err
+	}
+	return w.sigEnc.Encode(hashBytes)
+}
+
+func (w *signingWriter) commitSignature() error {
+	sig, err := w.signer.Sign(w.signatureHash.Sum(nil))
+	if err != nil {
+		return fmt.Errorf("signing failed: %s", err)
+	}
+
+	return w.sigEnc.Encode(sig)
+}
+
+func (w *signingWriter) close() error {
+	var closeErr error
+	if err := w.data.Close(); err != nil && closeErr == nil {
+		closeErr = err
+	}
+	if err := w.signature.Close(); err != nil && closeErr == nil {
+		closeErr = err
+	}
+	return closeErr
+}
diff --git a/security/serialization/verifying_reader.go b/security/serialization/verifying_reader.go
new file mode 100644
index 0000000..89ce5cc
--- /dev/null
+++ b/security/serialization/verifying_reader.go
@@ -0,0 +1,123 @@
+package serialization
+
+import (
+	"bytes"
+	"crypto/ecdsa"
+	"crypto/sha256"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+
+	"veyron2/security"
+	"veyron2/vom"
+)
+
+// verifyingReader implements io.Reader.
+type verifyingReader struct {
+	data io.Reader
+
+	chunkSizeBytes int
+	curChunk       bytes.Buffer
+	hashes         bytes.Buffer
+}
+
+func (r *verifyingReader) Read(p []byte) (int, error) {
+	bytesRead := 0
+	for len(p) > 0 {
+		if err := r.readChunk(); err != nil {
+			return bytesRead, err
+		}
+
+		n, err := r.curChunk.Read(p)
+		bytesRead = bytesRead + n
+		if err != nil {
+			return bytesRead, err
+		}
+
+		p = p[n:]
+	}
+	return bytesRead, nil
+}
+
+// NewVerifyingReader returns an io.Reader that ensures that all data returned
+// by Read calls was written using a NewSigningWriter (by a principal possessing
+// a signer corresponding to the provided public key), and has not been modified
+// since (ensuring integrity and authenticity of data).
+func NewVerifyingReader(data, signature io.Reader, key *ecdsa.PublicKey) (io.Reader, error) {
+	if (data == nil) && (signature == nil) {
+		return nil, nil
+	}
+	if (data == nil) || (signature == nil) {
+		return nil, errors.New("data or signature Reader is nil")
+	}
+	r := &verifyingReader{data: data}
+	if err := r.verifySignature(signature, key); err != nil {
+		return nil, err
+	}
+	return r, nil
+}
+
+func (r *verifyingReader) readChunk() error {
+	if r.curChunk.Len() > 0 {
+		return nil
+	}
+	hash := make([]byte, sha256.Size)
+	if _, err := r.hashes.Read(hash); err == io.EOF {
+		return nil
+	} else if err != nil {
+		return err
+	}
+
+	if _, err := io.CopyN(&r.curChunk, r.data, int64(r.chunkSizeBytes)); err != nil && err != io.EOF {
+		return err
+	}
+
+	if wantHash := sha256.Sum256(r.curChunk.Bytes()); !bytes.Equal(hash, wantHash[:]) {
+		return errors.New("data has been modified since being written")
+	}
+	return nil
+}
+
+func (r *verifyingReader) verifySignature(signature io.Reader, key *ecdsa.PublicKey) error {
+	dec := vom.NewDecoder(signature)
+	signatureHash := sha256.New()
+
+	var h header
+	if err := dec.Decode(&h); err != nil {
+		return fmt.Errorf("failed to decode header: %v", err)
+	}
+	r.chunkSizeBytes = h.ChunkSizeBytes
+	if err := binary.Write(signatureHash, binary.LittleEndian, int64(r.chunkSizeBytes)); err != nil {
+		return err
+	}
+
+	var signatureFound bool
+	for !signatureFound {
+		var i interface{}
+		if err := dec.Decode(&i); err == io.EOF {
+			break
+		} else if err != nil {
+			return err
+		}
+
+		switch v := i.(type) {
+		case [sha256.Size]byte:
+			if _, err := io.MultiWriter(&r.hashes, signatureHash).Write(v[:]); err != nil {
+				return err
+			}
+		case security.Signature:
+			signatureFound = true
+			if !v.Verify(key, signatureHash.Sum(nil)) {
+				return errors.New("signature verification failed")
+			}
+		default:
+			return fmt.Errorf("invalid data of type: %T read from signature Reader", i)
+		}
+	}
+	// Verify that no more data can be read from the signature Reader.
+	if _, err := signature.Read(make([]byte, 1)); err != io.EOF {
+		return fmt.Errorf("unexpected data found after signature")
+	}
+	return nil
+}
diff --git a/services/wsprd/identity/identity.go b/services/wsprd/identity/identity.go
index d3d1d4b..51b8ec6 100644
--- a/services/wsprd/identity/identity.go
+++ b/services/wsprd/identity/identity.go
@@ -13,12 +13,13 @@
 package identity
 
 import (
-	"crypto/sha256"
 	"io"
 	"net/url"
 	"sync"
 	"time"
 
+	"veyron/security/serialization"
+
 	"veyron2"
 	"veyron2/security"
 	"veyron2/verror"
@@ -43,24 +44,15 @@
 	Accounts map[string]security.PrivateID
 }
 
-// Serializer is a factory for managing the readers and writers used by the IDManager
-// for serialization and deserialization
+// Serializer is a factory for managing the readers and writers used by the
+// IDManager for serialization and deserialization
 type Serializer interface {
-	// DataWriter returns a writer that is used to write the data portion
-	// of the IDManager
-	DataWriter() io.WriteCloser
-
-	// SignatureWriter returns a writer that is used to write the signature
-	// of the serialized data.
-	SignatureWriter() io.WriteCloser
-
-	// DataReader returns a reader that is used to read the serialized data.
-	// If nil is returned, then there is no seralized data to load.
-	DataReader() io.Reader
-
-	// SignatureReader returns a reader that is used to read the signature of the
-	// serialized data.  If nil is returned, then there is no signature to load.
-	SignatureReader() io.Reader
+	// Readers returns io.Readers for reading the IDManager's serialized
+	// data and its signature.
+	Readers() (data io.Reader, signature io.Reader, err error)
+	// Writers returns io.WriteClosers for writing the IDManager's
+	// serialized data and integrity its signature.
+	Writers() (data io.WriteCloser, signature io.WriteCloser, err error)
 }
 
 var OriginDoesNotExist = verror.NotFoundf("origin not found")
@@ -77,7 +69,8 @@
 	serializer Serializer
 }
 
-// NewIDManager creates a new IDManager from the reader passed in. serializer can't be nil
+// NewIDManager creates a new IDManager by reading it from the serializer passed in.
+// serializer can't be nil
 func NewIDManager(rt veyron2.Runtime, serializer Serializer) (*IDManager, error) {
 	result := &IDManager{
 		rt: rt,
@@ -88,68 +81,38 @@
 		serializer: serializer,
 	}
 
-	reader := serializer.DataReader()
-	var hadData bool
-	hash := sha256.New()
-	if reader != nil {
-		hadData = true
-		if err := vom.NewDecoder(io.TeeReader(reader, hash)).Decode(&result.state); err != nil {
-			return nil, err
-		}
-
+	data, signature, err := serializer.Readers()
+	if err != nil {
+		return nil, err
 	}
-	signed := hash.Sum(nil)
-
-	var sig security.Signature
-
-	reader = serializer.SignatureReader()
-
-	var hadSig bool
-	if reader != nil {
-		hadSig = true
-		if err := vom.NewDecoder(serializer.SignatureReader()).Decode(&sig); err != nil {
-			return nil, err
-		}
+	vr, err := serialization.NewVerifyingReader(data, signature, rt.Identity().PublicKey())
+	if err != nil {
+		return nil, err
 	}
-
-	if !hadSig && !hadData {
+	if vr == nil {
+		// No serialized data exists, returning aan empty IDManager.
 		return result, nil
 	}
-
-	if !sig.Verify(rt.Identity().PublicID().PublicKey(), signed) {
-		return nil, verror.NotAuthorizedf("signature verification failed")
+	if err := vom.NewDecoder(vr).Decode(&result.state); err != nil {
+		return nil, err
 	}
-
 	return result, nil
 }
 
-// Save serializes the IDManager to the writer.
 func (i *IDManager) save() error {
-	hash := sha256.New()
-	writer := i.serializer.DataWriter()
-
-	if err := vom.NewEncoder(io.MultiWriter(writer, hash)).Encode(i.state); err != nil {
-		return err
-	}
-
-	if err := writer.Close(); err != nil {
-		return err
-	}
-
-	signed := hash.Sum(nil)
-	signature, err := i.rt.Identity().Sign(signed)
-
+	data, signature, err := i.serializer.Writers()
 	if err != nil {
 		return err
 	}
 
-	writer = i.serializer.SignatureWriter()
-
-	if err := vom.NewEncoder(writer).Encode(signature); err != nil {
+	swc, err := serialization.NewSigningWriteCloser(data, signature, i.rt.Identity(), nil)
+	if err != nil {
 		return err
 	}
-
-	return writer.Close()
+	if err := vom.NewEncoder(swc).Encode(i.state); err != nil {
+		return err
+	}
+	return swc.Close()
 }
 
 // Identity returns the identity for an origin.  Returns OriginDoesNotExist if
@@ -245,3 +208,7 @@
 	}
 	return blessee, nil
 }
+
+func init() {
+	vom.Register(&persistentState{})
+}
diff --git a/services/wsprd/identity/in_memory_serializer.go b/services/wsprd/identity/in_memory_serializer.go
index c412bb6..99a565d 100644
--- a/services/wsprd/identity/in_memory_serializer.go
+++ b/services/wsprd/identity/in_memory_serializer.go
@@ -5,6 +5,7 @@
 	"io"
 )
 
+// bufferCloser implements io.ReadWriteCloser.
 type bufferCloser struct {
 	bytes.Buffer
 }
@@ -13,33 +14,27 @@
 	return nil
 }
 
+// InMemorySerializer implements Serializer. This Serializer should only be
+// used in tests.
+// TODO(ataly, bjornick): Get rid of all uses of this Serializer from non-test
+// code and use a file backed (or some persistent storage backed) Serializer there
+// instead.
 type InMemorySerializer struct {
 	data      bufferCloser
 	signature bufferCloser
 	hasData   bool
 }
 
-func (s *InMemorySerializer) DataWriter() io.WriteCloser {
+func (s *InMemorySerializer) Readers() (io.Reader, io.Reader, error) {
+	if !s.hasData {
+		return nil, nil, nil
+	}
+	return &s.data, &s.signature, nil
+}
+
+func (s *InMemorySerializer) Writers() (io.WriteCloser, io.WriteCloser, error) {
 	s.hasData = true
 	s.data.Reset()
-	return &s.data
-}
-
-func (s *InMemorySerializer) SignatureWriter() io.WriteCloser {
 	s.signature.Reset()
-	return &s.signature
-}
-
-func (s *InMemorySerializer) DataReader() io.Reader {
-	if s.hasData {
-		return &s.data
-	}
-	return nil
-}
-
-func (s *InMemorySerializer) SignatureReader() io.Reader {
-	if s.hasData {
-		return &s.signature
-	}
-	return nil
+	return &s.data, &s.signature, nil
 }