"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
}