make envelope fields private & validate on unmarshal

This commit is contained in:
Yusef Napora 2019-11-15 13:11:04 -05:00
parent a8a530e24f
commit 3724a31efe
4 changed files with 123 additions and 102 deletions

View File

@ -16,14 +16,13 @@ import (
type SignedEnvelope struct { type SignedEnvelope struct {
// The public key that can be used to verify the signature and derive the peer id of the signer. // The public key that can be used to verify the signature and derive the peer id of the signer.
PublicKey PubKey publicKey PubKey
// A binary identifier that indicates what kind of data is contained in the payload. // A binary identifier that indicates what kind of data is contained in the payload.
// TODO(yusef): enforce multicodec prefix // TODO(yusef): enforce multicodec prefix
PayloadType []byte payloadType []byte
// The envelope payload. This is private to discourage accessing the payload without verifying the signature. // The envelope payload.
// To access, use the Open method.
payload []byte payload []byte
// The signature of the domain string, type hint, and payload. // The signature of the domain string, type hint, and payload.
@ -31,6 +30,7 @@ type SignedEnvelope struct {
} }
var errEmptyDomain = errors.New("envelope domain must not be empty") var errEmptyDomain = errors.New("envelope domain must not be empty")
var errInvalidSignature = errors.New("invalid signature or incorrect domain")
// MakeEnvelope constructs a new SignedEnvelope using the given privateKey. // MakeEnvelope constructs a new SignedEnvelope using the given privateKey.
// //
@ -38,27 +38,42 @@ var errEmptyDomain = errors.New("envelope domain must not be empty")
// and must be supplied when verifying the signature. // and must be supplied when verifying the signature.
// //
// The 'payloadType' field indicates what kind of data is contained and may be empty. // The 'payloadType' field indicates what kind of data is contained and may be empty.
func MakeEnvelope(privateKey PrivKey, domain string, payloadType []byte, contents []byte) (*SignedEnvelope, error) { func MakeEnvelope(privateKey PrivKey, domain string, payloadType []byte, payload []byte) (*SignedEnvelope, error) {
if len(domain) == 0 { if len(domain) == 0 {
return nil, errEmptyDomain return nil, errEmptyDomain
} }
toSign := makeSigBuffer(domain, payloadType, contents) toSign := makeSigBuffer(domain, payloadType, payload)
sig, err := privateKey.Sign(toSign) sig, err := privateKey.Sign(toSign)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &SignedEnvelope{ return &SignedEnvelope{
PublicKey: privateKey.GetPublic(), publicKey: privateKey.GetPublic(),
PayloadType: payloadType, payloadType: payloadType,
payload: contents, payload: payload,
signature: sig, signature: sig,
}, nil }, nil
} }
// UnmarshalEnvelope converts a serialized protobuf representation of an envelope // OpenEnvelope unmarshals a serialized SignedEnvelope, validating its signature
// into a SignedEnvelope struct. // using the provided 'domain' string.
func UnmarshalEnvelope(serializedEnvelope []byte) (*SignedEnvelope, error) { func OpenEnvelope(envelopeBytes []byte, domain string) (*SignedEnvelope, error) {
e, err := UnmarshalEnvelopeWithoutValidating(envelopeBytes)
if err != nil {
return nil, err
}
err = e.validate(domain)
if err != nil {
return nil, err
}
return e, nil
}
// UnmarshalEnvelopeWithoutValidating unmarshals a serialized SignedEnvelope protobuf message,
// without validating its contents. Should not be used unless you have a very good reason
// (e.g. testing)!
func UnmarshalEnvelopeWithoutValidating(serializedEnvelope []byte) (*SignedEnvelope, error) {
e := pb.SignedEnvelope{} e := pb.SignedEnvelope{}
if err := proto.Unmarshal(serializedEnvelope, &e); err != nil { if err := proto.Unmarshal(serializedEnvelope, &e); err != nil {
return nil, err return nil, err
@ -68,57 +83,63 @@ func UnmarshalEnvelope(serializedEnvelope []byte) (*SignedEnvelope, error) {
return nil, err return nil, err
} }
return &SignedEnvelope{ return &SignedEnvelope{
PublicKey: key, publicKey: key,
PayloadType: e.PayloadType, payloadType: e.PayloadType,
payload: e.Payload, payload: e.Payload,
signature: e.Signature, signature: e.Signature,
}, nil }, nil
} }
// Validate returns true if the envelope signature is valid for the given 'domain', // PublicKey returns the public key that can be used to verify the signature and derive the peer id of the signer.
// or false if it is invalid. May return an error if signature validation fails. func (e *SignedEnvelope) PublicKey() PubKey {
func (e *SignedEnvelope) Validate(domain string) (bool, error) { return e.publicKey
toVerify := makeSigBuffer(domain, e.PayloadType, e.payload) }
return e.PublicKey.Verify(toVerify, e.signature)
// PayloadType returns a binary identifier that indicates what kind of data is contained in the payload.
func (e *SignedEnvelope) PayloadType() []byte {
return e.payloadType
}
// Payload returns the binary payload of a SignedEnvelope.
func (e *SignedEnvelope) Payload() []byte {
return e.payload
} }
// Marshal returns a []byte containing a serialized protobuf representation of
// the SignedEnvelope.
func (e *SignedEnvelope) Marshal() ([]byte, error) { func (e *SignedEnvelope) Marshal() ([]byte, error) {
key, err := PublicKeyToProto(e.PublicKey) key, err := PublicKeyToProto(e.publicKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
msg := pb.SignedEnvelope{ msg := pb.SignedEnvelope{
PublicKey: key, PublicKey: key,
PayloadType: e.PayloadType, PayloadType: e.payloadType,
Payload: e.payload, Payload: e.payload,
Signature: e.signature, Signature: e.signature,
} }
return proto.Marshal(&msg) return proto.Marshal(&msg)
} }
// Open validates the signature (within the given 'domain') and returns
// the payload of the envelope. Will fail with an error if the signature
// is invalid.
func (e *SignedEnvelope) Open(domain string) ([]byte, error) {
valid, err := e.Validate(domain)
if err != nil {
return nil, err
}
if !valid {
return nil, errors.New("invalid signature or incorrect domain")
}
return e.payload, nil
}
func (e *SignedEnvelope) Equals(other *SignedEnvelope) bool { func (e *SignedEnvelope) Equals(other *SignedEnvelope) bool {
return e.PublicKey.Equals(other.PublicKey) && return e.publicKey.Equals(other.publicKey) &&
bytes.Compare(e.PayloadType, other.PayloadType) == 0 && bytes.Compare(e.payloadType, other.payloadType) == 0 &&
bytes.Compare(e.payload, other.payload) == 0 && bytes.Compare(e.payload, other.payload) == 0 &&
bytes.Compare(e.signature, other.signature) == 0 bytes.Compare(e.signature, other.signature) == 0
} }
// validate returns true if the envelope signature is valid for the given 'domain',
// or false if it is invalid. May return an error if signature validation fails.
func (e *SignedEnvelope) validate(domain string) error {
toVerify := makeSigBuffer(domain, e.payloadType, e.payload)
valid, err := e.publicKey.Verify(toVerify, e.signature)
if err != nil {
return err
}
if !valid {
return errInvalidSignature
}
return nil
}
// makeSigBuffer is a helper function that prepares a buffer to sign or verify. // makeSigBuffer is a helper function that prepares a buffer to sign or verify.
func makeSigBuffer(domain string, typeHint []byte, content []byte) []byte { func makeSigBuffer(domain string, typeHint []byte, content []byte) []byte {
b := bytes.Buffer{} b := bytes.Buffer{}

View File

@ -2,7 +2,8 @@ package crypto_test
import ( import (
"bytes" "bytes"
"github.com/gogo/protobuf/proto" "github.com/golang/protobuf/proto"
. "github.com/libp2p/go-libp2p-core/crypto" . "github.com/libp2p/go-libp2p-core/crypto"
pb "github.com/libp2p/go-libp2p-core/crypto/pb" pb "github.com/libp2p/go-libp2p-core/crypto/pb"
"github.com/libp2p/go-libp2p-core/test" "github.com/libp2p/go-libp2p-core/test"
@ -23,39 +24,27 @@ func TestEnvelopeHappyPath(t *testing.T) {
t.Errorf("error constructing envelope: %v", err) t.Errorf("error constructing envelope: %v", err)
} }
if !envelope.PublicKey.Equals(pub) { if !envelope.PublicKey().Equals(pub) {
t.Error("envelope has unexpected public key") t.Error("envelope has unexpected public key")
} }
if bytes.Compare(payloadType, envelope.PayloadType) != 0 { if bytes.Compare(payloadType, envelope.PayloadType()) != 0 {
t.Error("PayloadType does not match payloadType used to construct envelope") t.Error("PayloadType does not match payloadType used to construct envelope")
} }
valid, err := envelope.Validate(domain)
if err != nil {
t.Errorf("error validating envelope: %v", err)
}
if !valid {
t.Error("envelope should be valid, but Valid returns false")
}
c, err := envelope.Open(domain)
if err != nil {
t.Errorf("error opening envelope: %v", err)
}
if bytes.Compare(c, payload) != 0 {
t.Error("payload of envelope do not match input")
}
serialized, err := envelope.Marshal() serialized, err := envelope.Marshal()
if err != nil { if err != nil {
t.Errorf("error serializing envelope: %v", err) t.Errorf("error serializing envelope: %v", err)
} }
deserialized, err := UnmarshalEnvelope(serialized) deserialized, err := OpenEnvelope(serialized, domain)
if err != nil { if err != nil {
t.Errorf("error deserializing envelope: %v", err) t.Errorf("error deserializing envelope: %v", err)
} }
if bytes.Compare(deserialized.Payload(), payload) != 0 {
t.Error("payload of envelope does not match input")
}
if !envelope.Equals(deserialized) { if !envelope.Equals(deserialized) {
t.Error("round-trip serde results in unequal envelope structures") t.Error("round-trip serde results in unequal envelope structures")
} }
@ -73,13 +62,11 @@ func TestEnvelopeValidateFailsForDifferentDomain(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("error constructing envelope: %v", err) t.Errorf("error constructing envelope: %v", err)
} }
serialized, err := envelope.Marshal()
valid, err := envelope.Validate("other-domain") // try to open our modified envelope
if err != nil { _, err = OpenEnvelope(serialized, "wrong-domain")
t.Errorf("error validating envelope: %v", err) if err == nil {
} t.Error("should not be able to open envelope with incorrect domain")
if valid {
t.Error("envelope should be invalid, but Valid returns true")
} }
} }
@ -95,13 +82,13 @@ func TestEnvelopeValidateFailsIfTypeHintIsAltered(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("error constructing envelope: %v", err) t.Errorf("error constructing envelope: %v", err)
} }
envelope.PayloadType = []byte("foo") serialized := alterMessageAndMarshal(t, envelope, func(msg *pb.SignedEnvelope) {
valid, err := envelope.Validate("other-domain") msg.PayloadType = []byte("foo")
if err != nil { })
t.Errorf("error validating envelope: %v", err) // try to open our modified envelope
} _, err = OpenEnvelope(serialized, domain)
if valid { if err == nil {
t.Error("envelope should be invalid, but Valid returns true") t.Error("should not be able to open envelope with modified payloadType")
} }
} }
@ -118,33 +105,35 @@ func TestEnvelopeValidateFailsIfContentsAreAltered(t *testing.T) {
t.Errorf("error constructing envelope: %v", err) t.Errorf("error constructing envelope: %v", err)
} }
// since the payload field is private, we'll serialize and alter the serialized := alterMessageAndMarshal(t, envelope, func(msg *pb.SignedEnvelope) {
// serialized protobuf msg.Payload = []byte("totally legit, trust me")
})
// try to open our modified envelope
_, err = OpenEnvelope(serialized, domain)
if err == nil {
t.Error("should not be able to open envelope with modified payload")
}
}
// Since we're outside of the crypto package (to avoid import cycles with test package),
// we can't alter the fields in a SignedEnvelope directly. This helper marshals
// the envelope to a protobuf and calls the alterMsg function, which should
// alter the protobuf message.
// Returns the serialized altered protobuf message.
func alterMessageAndMarshal(t *testing.T, envelope *SignedEnvelope, alterMsg func(*pb.SignedEnvelope)) []byte {
serialized, err := envelope.Marshal() serialized, err := envelope.Marshal()
if err != nil { if err != nil {
t.Errorf("error serializing envelope: %v", err) t.Errorf("error marshaling envelope: %v", err)
} }
msg := pb.SignedEnvelope{} msg := pb.SignedEnvelope{}
err = proto.Unmarshal(serialized, &msg) err = proto.Unmarshal(serialized, &msg)
if err != nil { if err != nil {
t.Errorf("error deserializing envelope: %v", err) t.Errorf("error unmarshaling envelope: %v", err)
} }
msg.Payload = []byte("totally legit, trust me") alterMsg(&msg)
serialized, err = proto.Marshal(&msg) serialized, err = msg.Marshal()
// unmarshal our altered envelope
deserialized, err := UnmarshalEnvelope(serialized)
if err != nil { if err != nil {
t.Errorf("error deserializing envelope: %v", err) t.Errorf("error marshaling envelope: %v", err)
}
// verify that it's now invalid
valid, err := deserialized.Validate(domain)
if err != nil {
t.Errorf("error validating envelope: %v", err)
}
if valid {
t.Error("envelope should be invalid, but Valid returns true")
} }
return serialized
} }

View File

@ -97,8 +97,8 @@ type AddrBook interface {
AddAddrs(p peer.ID, addrs []ma.Multiaddr, ttl time.Duration) AddAddrs(p peer.ID, addrs []ma.Multiaddr, ttl time.Duration)
// AddCertifiedAddrs adds addresses from a routing.RoutingState record // AddCertifiedAddrs adds addresses from a routing.RoutingState record
// contained in the given SignedEnvelope. // contained in a serialized SignedEnvelope.
AddCertifiedAddrs(envelope *ic.SignedEnvelope, ttl time.Duration) error AddCertifiedAddrs(envelopeBytes []byte, ttl time.Duration) error
// SetAddr calls mgr.SetAddrs(p, addr, ttl) // SetAddr calls mgr.SetAddrs(p, addr, ttl)
SetAddr(p peer.ID, addr ma.Multiaddr, ttl time.Duration) SetAddr(p peer.ID, addr ma.Multiaddr, ttl time.Duration)
@ -130,9 +130,17 @@ type AddrBook interface {
// PeersWithAddrs returns all of the peer IDs stored in the AddrBook // PeersWithAddrs returns all of the peer IDs stored in the AddrBook
PeersWithAddrs() peer.IDSlice PeersWithAddrs() peer.IDSlice
// SignedRoutingState returns a SignedEnvelope containing a RoutingState // SignedRoutingState returns a signed RoutingState record for the
// record, if one exists for the given peer. // given peer id, if one exists in the peerstore. The record is
SignedRoutingState(p peer.ID) *ic.SignedEnvelope // returned as a byte slice containing a serialized SignedEnvelope.
// Returns nil if no routing state exists for the peer.
SignedRoutingState(p peer.ID) []byte
// SignedRoutingStates returns signed RoutingState records for each of
// the given peer ids, if one exists in the peerstore.
// Returns a map of peer ids to serialized SignedEnvelope messages. If
// no routing state exists for a peer, their map entry will be nil.
SignedRoutingStates(peers ...peer.ID) map[peer.ID][]byte
} }
// KeyBook tracks the keys of Peers. // KeyBook tracks the keys of Peers.

View File

@ -75,16 +75,19 @@ func UnmarshalRoutingState(serialized []byte) (*RoutingState, error) {
// RoutingStateFromEnvelope unwraps a peer RoutingState record from a SignedEnvelope. // RoutingStateFromEnvelope unwraps a peer RoutingState record from a SignedEnvelope.
// This method will fail if the signature is invalid, or if the record // This method will fail if the signature is invalid, or if the record
// belongs to a peer other than the one that signed the envelope. // belongs to a peer other than the one that signed the envelope.
func RoutingStateFromEnvelope(envelope *crypto.SignedEnvelope) (*RoutingState, error) { func RoutingStateFromEnvelope(envelopeBytes []byte) (*RoutingState, error) {
msgBytes, err := envelope.Open(StateEnvelopeDomain) envelope, err := crypto.OpenEnvelope(envelopeBytes, StateEnvelopeDomain)
if err != nil { if err != nil {
return nil, err return nil, err
} }
state, err := UnmarshalRoutingState(msgBytes) if bytes.Compare(envelope.PayloadType(), StateEnvelopePayloadType) != 0 {
return nil, errors.New("unexpected envelope payload type")
}
state, err := UnmarshalRoutingState(envelope.Payload())
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !state.PeerID.MatchesPublicKey(envelope.PublicKey) { if !state.PeerID.MatchesPublicKey(envelope.PublicKey()) {
return nil, errors.New("peer id in routing state record does not match signing key") return nil, errors.New("peer id in routing state record does not match signing key")
} }
return state, nil return state, nil