diff --git a/.gitignore b/.gitignore index ebd0ef239b..9781154449 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ crypto/libs *~ *.swp *.swo +*.swn # Mac .DS_Store diff --git a/Makefile b/Makefile index a418c7e35e..b9ad2e319b 100644 --- a/Makefile +++ b/Makefile @@ -85,7 +85,7 @@ GOLDFLAGS := $(GOLDFLAGS_BASE) \ UNIT_TEST_SOURCES := $(sort $(shell GOPATH=$(GOPATH) && GO111MODULE=off && go list ./... | grep -v /go-algorand/test/ )) ALGOD_API_PACKAGES := $(sort $(shell GOPATH=$(GOPATH) && GO111MODULE=off && cd daemon/algod/api; go list ./... )) -MSGP_GENERATE := ./protocol ./protocol/test ./crypto ./crypto/merklearray ./crypto/merklesignature ./crypto/stateproof ./data/basics ./data/transactions ./data/stateproofmsg ./data/committee ./data/bookkeeping ./data/hashable ./agreement ./rpcs ./node ./ledger ./ledger/ledgercore ./ledger/store ./ledger/encoded ./stateproof ./data/account ./daemon/algod/api/spec/v2 +MSGP_GENERATE := ./protocol ./protocol/test ./crypto ./crypto/merklearray ./crypto/merklesignature ./crypto/stateproof ./data/basics ./data/transactions ./data/stateproofmsg ./data/committee ./data/bookkeeping ./data/hashable ./agreement ./rpcs ./network ./node ./ledger ./ledger/ledgercore ./ledger/store ./ledger/encoded ./stateproof ./data/account ./daemon/algod/api/spec/v2 default: build diff --git a/config/localTemplate.go b/config/localTemplate.go index 020361918d..cdf284c320 100644 --- a/config/localTemplate.go +++ b/config/localTemplate.go @@ -56,7 +56,11 @@ type Local struct { // 1 * time.Minute = 60000000000 ns ReconnectTime time.Duration `version[0]:"60" version[1]:"60000000000"` - // what we should tell people to connect to + // The public address to connect to that is advertised to other nodes. + // For MainNet relays, make sure this entry includes the full SRV host name + // plus the publicly-accessible port number. + // A valid entry will avoid "self-gossip" and is used for identity exchange + // to deduplicate redundant connections PublicAddress string `version[0]:""` MaxConnectionsPerIP int `version[3]:"30" version[27]:"15"` diff --git a/network/connPerfMon.go b/network/connPerfMon.go index e74614ae10..d254aa1279 100644 --- a/network/connPerfMon.go +++ b/network/connPerfMon.go @@ -25,6 +25,7 @@ import ( "github.com/algorand/go-algorand/crypto" ) +//msgp:ignore pmStage type pmStage int const ( diff --git a/network/msgp_gen.go b/network/msgp_gen.go new file mode 100644 index 0000000000..70b03ccfc7 --- /dev/null +++ b/network/msgp_gen.go @@ -0,0 +1,1088 @@ +package network + +// Code generated by github.com/algorand/msgp DO NOT EDIT. + +import ( + "github.com/algorand/msgp/msgp" +) + +// The following msgp objects are implemented in this file: +// disconnectReason +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// identityChallenge +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// identityChallengeResponse +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// identityChallengeResponseSigned +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// identityChallengeSigned +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// identityChallengeValue +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// identityVerificationMessage +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// identityVerificationMessageSigned +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + +// MarshalMsg implements msgp.Marshaler +func (z disconnectReason) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendString(o, string(z)) + return +} + +func (_ disconnectReason) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(disconnectReason) + if !ok { + _, ok = (z).(*disconnectReason) + } + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *disconnectReason) UnmarshalMsg(bts []byte) (o []byte, err error) { + { + var zb0001 string + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + (*z) = disconnectReason(zb0001) + } + o = bts + return +} + +func (_ *disconnectReason) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*disconnectReason) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z disconnectReason) Msgsize() (s int) { + s = msgp.StringPrefixSize + len(string(z)) + return +} + +// MsgIsZero returns whether this is a zero value +func (z disconnectReason) MsgIsZero() bool { + return z == "" +} + +// MarshalMsg implements msgp.Marshaler +func (z *identityChallenge) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0002Len := uint32(3) + var zb0002Mask uint8 /* 4 bits */ + if len((*z).PublicAddress) == 0 { + zb0002Len-- + zb0002Mask |= 0x2 + } + if (*z).Challenge == (identityChallengeValue{}) { + zb0002Len-- + zb0002Mask |= 0x4 + } + if (*z).Key.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x8 + } + // variable map header, size zb0002Len + o = append(o, 0x80|uint8(zb0002Len)) + if zb0002Len != 0 { + if (zb0002Mask & 0x2) == 0 { // if not empty + // string "a" + o = append(o, 0xa1, 0x61) + o = msgp.AppendBytes(o, (*z).PublicAddress) + } + if (zb0002Mask & 0x4) == 0 { // if not empty + // string "c" + o = append(o, 0xa1, 0x63) + o = msgp.AppendBytes(o, ((*z).Challenge)[:]) + } + if (zb0002Mask & 0x8) == 0 { // if not empty + // string "pk" + o = append(o, 0xa2, 0x70, 0x6b) + o = (*z).Key.MarshalMsg(o) + } + } + return +} + +func (_ *identityChallenge) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*identityChallenge) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *identityChallenge) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0002 int + var zb0003 bool + zb0002, zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0002, zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0002 > 0 { + zb0002-- + bts, err = (*z).Key.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Key") + return + } + } + if zb0002 > 0 { + zb0002-- + bts, err = msgp.ReadExactBytes(bts, ((*z).Challenge)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Challenge") + return + } + } + if zb0002 > 0 { + zb0002-- + var zb0004 int + zb0004, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "PublicAddress") + return + } + if zb0004 > maxAddressLen { + err = msgp.ErrOverflow(uint64(zb0004), uint64(maxAddressLen)) + return + } + (*z).PublicAddress, bts, err = msgp.ReadBytesBytes(bts, (*z).PublicAddress) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "PublicAddress") + return + } + } + if zb0002 > 0 { + err = msgp.ErrTooManyArrayFields(zb0002) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0003 { + (*z) = identityChallenge{} + } + for zb0002 > 0 { + zb0002-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "pk": + bts, err = (*z).Key.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Key") + return + } + case "c": + bts, err = msgp.ReadExactBytes(bts, ((*z).Challenge)[:]) + if err != nil { + err = msgp.WrapError(err, "Challenge") + return + } + case "a": + var zb0005 int + zb0005, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "PublicAddress") + return + } + if zb0005 > maxAddressLen { + err = msgp.ErrOverflow(uint64(zb0005), uint64(maxAddressLen)) + return + } + (*z).PublicAddress, bts, err = msgp.ReadBytesBytes(bts, (*z).PublicAddress) + if err != nil { + err = msgp.WrapError(err, "PublicAddress") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *identityChallenge) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*identityChallenge) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *identityChallenge) Msgsize() (s int) { + s = 1 + 3 + (*z).Key.Msgsize() + 2 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 2 + msgp.BytesPrefixSize + len((*z).PublicAddress) + return +} + +// MsgIsZero returns whether this is a zero value +func (z *identityChallenge) MsgIsZero() bool { + return ((*z).Key.MsgIsZero()) && ((*z).Challenge == (identityChallengeValue{})) && (len((*z).PublicAddress) == 0) +} + +// MarshalMsg implements msgp.Marshaler +func (z *identityChallengeResponse) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0003Len := uint32(3) + var zb0003Mask uint8 /* 4 bits */ + if (*z).Challenge == (identityChallengeValue{}) { + zb0003Len-- + zb0003Mask |= 0x2 + } + if (*z).Key.MsgIsZero() { + zb0003Len-- + zb0003Mask |= 0x4 + } + if (*z).ResponseChallenge == (identityChallengeValue{}) { + zb0003Len-- + zb0003Mask |= 0x8 + } + // variable map header, size zb0003Len + o = append(o, 0x80|uint8(zb0003Len)) + if zb0003Len != 0 { + if (zb0003Mask & 0x2) == 0 { // if not empty + // string "c" + o = append(o, 0xa1, 0x63) + o = msgp.AppendBytes(o, ((*z).Challenge)[:]) + } + if (zb0003Mask & 0x4) == 0 { // if not empty + // string "pk" + o = append(o, 0xa2, 0x70, 0x6b) + o = (*z).Key.MarshalMsg(o) + } + if (zb0003Mask & 0x8) == 0 { // if not empty + // string "rc" + o = append(o, 0xa2, 0x72, 0x63) + o = msgp.AppendBytes(o, ((*z).ResponseChallenge)[:]) + } + } + return +} + +func (_ *identityChallengeResponse) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*identityChallengeResponse) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *identityChallengeResponse) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0003 int + var zb0004 bool + zb0003, zb0004, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0003, zb0004, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0003 > 0 { + zb0003-- + bts, err = (*z).Key.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Key") + return + } + } + if zb0003 > 0 { + zb0003-- + bts, err = msgp.ReadExactBytes(bts, ((*z).Challenge)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Challenge") + return + } + } + if zb0003 > 0 { + zb0003-- + bts, err = msgp.ReadExactBytes(bts, ((*z).ResponseChallenge)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ResponseChallenge") + return + } + } + if zb0003 > 0 { + err = msgp.ErrTooManyArrayFields(zb0003) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0004 { + (*z) = identityChallengeResponse{} + } + for zb0003 > 0 { + zb0003-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "pk": + bts, err = (*z).Key.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Key") + return + } + case "c": + bts, err = msgp.ReadExactBytes(bts, ((*z).Challenge)[:]) + if err != nil { + err = msgp.WrapError(err, "Challenge") + return + } + case "rc": + bts, err = msgp.ReadExactBytes(bts, ((*z).ResponseChallenge)[:]) + if err != nil { + err = msgp.WrapError(err, "ResponseChallenge") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *identityChallengeResponse) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*identityChallengeResponse) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *identityChallengeResponse) Msgsize() (s int) { + s = 1 + 3 + (*z).Key.Msgsize() + 2 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 3 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + return +} + +// MsgIsZero returns whether this is a zero value +func (z *identityChallengeResponse) MsgIsZero() bool { + return ((*z).Key.MsgIsZero()) && ((*z).Challenge == (identityChallengeValue{})) && ((*z).ResponseChallenge == (identityChallengeValue{})) +} + +// MarshalMsg implements msgp.Marshaler +func (z *identityChallengeResponseSigned) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0001Len := uint32(2) + var zb0001Mask uint8 /* 3 bits */ + if (*z).Msg.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).Signature.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x4 + } + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "icr" + o = append(o, 0xa3, 0x69, 0x63, 0x72) + o = (*z).Msg.MarshalMsg(o) + } + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "sig" + o = append(o, 0xa3, 0x73, 0x69, 0x67) + o = (*z).Signature.MarshalMsg(o) + } + } + return +} + +func (_ *identityChallengeResponseSigned) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*identityChallengeResponseSigned) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *identityChallengeResponseSigned) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 int + var zb0002 bool + zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).Msg.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Msg") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).Signature.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Signature") + return + } + } + if zb0001 > 0 { + err = msgp.ErrTooManyArrayFields(zb0001) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0002 { + (*z) = identityChallengeResponseSigned{} + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "icr": + bts, err = (*z).Msg.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Msg") + return + } + case "sig": + bts, err = (*z).Signature.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Signature") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *identityChallengeResponseSigned) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*identityChallengeResponseSigned) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *identityChallengeResponseSigned) Msgsize() (s int) { + s = 1 + 4 + (*z).Msg.Msgsize() + 4 + (*z).Signature.Msgsize() + return +} + +// MsgIsZero returns whether this is a zero value +func (z *identityChallengeResponseSigned) MsgIsZero() bool { + return ((*z).Msg.MsgIsZero()) && ((*z).Signature.MsgIsZero()) +} + +// MarshalMsg implements msgp.Marshaler +func (z *identityChallengeSigned) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0001Len := uint32(2) + var zb0001Mask uint8 /* 3 bits */ + if (*z).Msg.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).Signature.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x4 + } + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "ic" + o = append(o, 0xa2, 0x69, 0x63) + o = (*z).Msg.MarshalMsg(o) + } + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "sig" + o = append(o, 0xa3, 0x73, 0x69, 0x67) + o = (*z).Signature.MarshalMsg(o) + } + } + return +} + +func (_ *identityChallengeSigned) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*identityChallengeSigned) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *identityChallengeSigned) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 int + var zb0002 bool + zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).Msg.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Msg") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).Signature.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Signature") + return + } + } + if zb0001 > 0 { + err = msgp.ErrTooManyArrayFields(zb0001) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0002 { + (*z) = identityChallengeSigned{} + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "ic": + bts, err = (*z).Msg.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Msg") + return + } + case "sig": + bts, err = (*z).Signature.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Signature") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *identityChallengeSigned) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*identityChallengeSigned) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *identityChallengeSigned) Msgsize() (s int) { + s = 1 + 3 + (*z).Msg.Msgsize() + 4 + (*z).Signature.Msgsize() + return +} + +// MsgIsZero returns whether this is a zero value +func (z *identityChallengeSigned) MsgIsZero() bool { + return ((*z).Msg.MsgIsZero()) && ((*z).Signature.MsgIsZero()) +} + +// MarshalMsg implements msgp.Marshaler +func (z *identityChallengeValue) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendBytes(o, (*z)[:]) + return +} + +func (_ *identityChallengeValue) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*identityChallengeValue) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *identityChallengeValue) UnmarshalMsg(bts []byte) (o []byte, err error) { + bts, err = msgp.ReadExactBytes(bts, (*z)[:]) + if err != nil { + err = msgp.WrapError(err) + return + } + o = bts + return +} + +func (_ *identityChallengeValue) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*identityChallengeValue) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *identityChallengeValue) Msgsize() (s int) { + s = msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + return +} + +// MsgIsZero returns whether this is a zero value +func (z *identityChallengeValue) MsgIsZero() bool { + return (*z) == (identityChallengeValue{}) +} + +// MarshalMsg implements msgp.Marshaler +func (z *identityVerificationMessage) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0002Len := uint32(1) + var zb0002Mask uint8 /* 2 bits */ + if (*z).ResponseChallenge == (identityChallengeValue{}) { + zb0002Len-- + zb0002Mask |= 0x2 + } + // variable map header, size zb0002Len + o = append(o, 0x80|uint8(zb0002Len)) + if zb0002Len != 0 { + if (zb0002Mask & 0x2) == 0 { // if not empty + // string "rc" + o = append(o, 0xa2, 0x72, 0x63) + o = msgp.AppendBytes(o, ((*z).ResponseChallenge)[:]) + } + } + return +} + +func (_ *identityVerificationMessage) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*identityVerificationMessage) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *identityVerificationMessage) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0002 int + var zb0003 bool + zb0002, zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0002, zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0002 > 0 { + zb0002-- + bts, err = msgp.ReadExactBytes(bts, ((*z).ResponseChallenge)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ResponseChallenge") + return + } + } + if zb0002 > 0 { + err = msgp.ErrTooManyArrayFields(zb0002) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0003 { + (*z) = identityVerificationMessage{} + } + for zb0002 > 0 { + zb0002-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "rc": + bts, err = msgp.ReadExactBytes(bts, ((*z).ResponseChallenge)[:]) + if err != nil { + err = msgp.WrapError(err, "ResponseChallenge") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *identityVerificationMessage) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*identityVerificationMessage) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *identityVerificationMessage) Msgsize() (s int) { + s = 1 + 3 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + return +} + +// MsgIsZero returns whether this is a zero value +func (z *identityVerificationMessage) MsgIsZero() bool { + return ((*z).ResponseChallenge == (identityChallengeValue{})) +} + +// MarshalMsg implements msgp.Marshaler +func (z *identityVerificationMessageSigned) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0002Len := uint32(2) + var zb0002Mask uint8 /* 3 bits */ + if (*z).Msg.ResponseChallenge == (identityChallengeValue{}) { + zb0002Len-- + zb0002Mask |= 0x2 + } + if (*z).Signature.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x4 + } + // variable map header, size zb0002Len + o = append(o, 0x80|uint8(zb0002Len)) + if zb0002Len != 0 { + if (zb0002Mask & 0x2) == 0 { // if not empty + // string "ivm" + o = append(o, 0xa3, 0x69, 0x76, 0x6d) + // omitempty: check for empty values + zb0003Len := uint32(1) + var zb0003Mask uint8 /* 2 bits */ + if (*z).Msg.ResponseChallenge == (identityChallengeValue{}) { + zb0003Len-- + zb0003Mask |= 0x2 + } + // variable map header, size zb0003Len + o = append(o, 0x80|uint8(zb0003Len)) + if (zb0003Mask & 0x2) == 0 { // if not empty + // string "rc" + o = append(o, 0xa2, 0x72, 0x63) + o = msgp.AppendBytes(o, ((*z).Msg.ResponseChallenge)[:]) + } + } + if (zb0002Mask & 0x4) == 0 { // if not empty + // string "sig" + o = append(o, 0xa3, 0x73, 0x69, 0x67) + o = (*z).Signature.MarshalMsg(o) + } + } + return +} + +func (_ *identityVerificationMessageSigned) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*identityVerificationMessageSigned) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *identityVerificationMessageSigned) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0002 int + var zb0003 bool + zb0002, zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0002, zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0002 > 0 { + zb0002-- + var zb0004 int + var zb0005 bool + zb0004, zb0005, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0004, zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Msg") + return + } + if zb0004 > 0 { + zb0004-- + bts, err = msgp.ReadExactBytes(bts, ((*z).Msg.ResponseChallenge)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Msg", "struct-from-array", "ResponseChallenge") + return + } + } + if zb0004 > 0 { + err = msgp.ErrTooManyArrayFields(zb0004) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Msg", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Msg") + return + } + if zb0005 { + (*z).Msg = identityVerificationMessage{} + } + for zb0004 > 0 { + zb0004-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Msg") + return + } + switch string(field) { + case "rc": + bts, err = msgp.ReadExactBytes(bts, ((*z).Msg.ResponseChallenge)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Msg", "ResponseChallenge") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Msg") + return + } + } + } + } + } + if zb0002 > 0 { + zb0002-- + bts, err = (*z).Signature.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Signature") + return + } + } + if zb0002 > 0 { + err = msgp.ErrTooManyArrayFields(zb0002) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0003 { + (*z) = identityVerificationMessageSigned{} + } + for zb0002 > 0 { + zb0002-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "ivm": + var zb0006 int + var zb0007 bool + zb0006, zb0007, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0006, zb0007, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Msg") + return + } + if zb0006 > 0 { + zb0006-- + bts, err = msgp.ReadExactBytes(bts, ((*z).Msg.ResponseChallenge)[:]) + if err != nil { + err = msgp.WrapError(err, "Msg", "struct-from-array", "ResponseChallenge") + return + } + } + if zb0006 > 0 { + err = msgp.ErrTooManyArrayFields(zb0006) + if err != nil { + err = msgp.WrapError(err, "Msg", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "Msg") + return + } + if zb0007 { + (*z).Msg = identityVerificationMessage{} + } + for zb0006 > 0 { + zb0006-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "Msg") + return + } + switch string(field) { + case "rc": + bts, err = msgp.ReadExactBytes(bts, ((*z).Msg.ResponseChallenge)[:]) + if err != nil { + err = msgp.WrapError(err, "Msg", "ResponseChallenge") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "Msg") + return + } + } + } + } + case "sig": + bts, err = (*z).Signature.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Signature") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *identityVerificationMessageSigned) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*identityVerificationMessageSigned) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *identityVerificationMessageSigned) Msgsize() (s int) { + s = 1 + 4 + 1 + 3 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 4 + (*z).Signature.Msgsize() + return +} + +// MsgIsZero returns whether this is a zero value +func (z *identityVerificationMessageSigned) MsgIsZero() bool { + return ((*z).Msg.ResponseChallenge == (identityChallengeValue{})) && ((*z).Signature.MsgIsZero()) +} diff --git a/network/msgp_gen_test.go b/network/msgp_gen_test.go new file mode 100644 index 0000000000..1046371a1a --- /dev/null +++ b/network/msgp_gen_test.go @@ -0,0 +1,435 @@ +//go:build !skip_msgp_testing +// +build !skip_msgp_testing + +package network + +// Code generated by github.com/algorand/msgp DO NOT EDIT. + +import ( + "testing" + + "github.com/algorand/msgp/msgp" + + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/partitiontest" +) + +func TestMarshalUnmarshalidentityChallenge(t *testing.T) { + partitiontest.PartitionTest(t) + v := identityChallenge{} + bts := v.MarshalMsg(nil) + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingidentityChallenge(t *testing.T) { + protocol.RunEncodingTest(t, &identityChallenge{}) +} + +func BenchmarkMarshalMsgidentityChallenge(b *testing.B) { + v := identityChallenge{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgidentityChallenge(b *testing.B) { + v := identityChallenge{} + bts := make([]byte, 0, v.Msgsize()) + bts = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalidentityChallenge(b *testing.B) { + v := identityChallenge{} + bts := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalidentityChallengeResponse(t *testing.T) { + partitiontest.PartitionTest(t) + v := identityChallengeResponse{} + bts := v.MarshalMsg(nil) + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingidentityChallengeResponse(t *testing.T) { + protocol.RunEncodingTest(t, &identityChallengeResponse{}) +} + +func BenchmarkMarshalMsgidentityChallengeResponse(b *testing.B) { + v := identityChallengeResponse{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgidentityChallengeResponse(b *testing.B) { + v := identityChallengeResponse{} + bts := make([]byte, 0, v.Msgsize()) + bts = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalidentityChallengeResponse(b *testing.B) { + v := identityChallengeResponse{} + bts := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalidentityChallengeResponseSigned(t *testing.T) { + partitiontest.PartitionTest(t) + v := identityChallengeResponseSigned{} + bts := v.MarshalMsg(nil) + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingidentityChallengeResponseSigned(t *testing.T) { + protocol.RunEncodingTest(t, &identityChallengeResponseSigned{}) +} + +func BenchmarkMarshalMsgidentityChallengeResponseSigned(b *testing.B) { + v := identityChallengeResponseSigned{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgidentityChallengeResponseSigned(b *testing.B) { + v := identityChallengeResponseSigned{} + bts := make([]byte, 0, v.Msgsize()) + bts = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalidentityChallengeResponseSigned(b *testing.B) { + v := identityChallengeResponseSigned{} + bts := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalidentityChallengeSigned(t *testing.T) { + partitiontest.PartitionTest(t) + v := identityChallengeSigned{} + bts := v.MarshalMsg(nil) + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingidentityChallengeSigned(t *testing.T) { + protocol.RunEncodingTest(t, &identityChallengeSigned{}) +} + +func BenchmarkMarshalMsgidentityChallengeSigned(b *testing.B) { + v := identityChallengeSigned{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgidentityChallengeSigned(b *testing.B) { + v := identityChallengeSigned{} + bts := make([]byte, 0, v.Msgsize()) + bts = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalidentityChallengeSigned(b *testing.B) { + v := identityChallengeSigned{} + bts := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalidentityChallengeValue(t *testing.T) { + partitiontest.PartitionTest(t) + v := identityChallengeValue{} + bts := v.MarshalMsg(nil) + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingidentityChallengeValue(t *testing.T) { + protocol.RunEncodingTest(t, &identityChallengeValue{}) +} + +func BenchmarkMarshalMsgidentityChallengeValue(b *testing.B) { + v := identityChallengeValue{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgidentityChallengeValue(b *testing.B) { + v := identityChallengeValue{} + bts := make([]byte, 0, v.Msgsize()) + bts = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalidentityChallengeValue(b *testing.B) { + v := identityChallengeValue{} + bts := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalidentityVerificationMessage(t *testing.T) { + partitiontest.PartitionTest(t) + v := identityVerificationMessage{} + bts := v.MarshalMsg(nil) + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingidentityVerificationMessage(t *testing.T) { + protocol.RunEncodingTest(t, &identityVerificationMessage{}) +} + +func BenchmarkMarshalMsgidentityVerificationMessage(b *testing.B) { + v := identityVerificationMessage{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgidentityVerificationMessage(b *testing.B) { + v := identityVerificationMessage{} + bts := make([]byte, 0, v.Msgsize()) + bts = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalidentityVerificationMessage(b *testing.B) { + v := identityVerificationMessage{} + bts := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalidentityVerificationMessageSigned(t *testing.T) { + partitiontest.PartitionTest(t) + v := identityVerificationMessageSigned{} + bts := v.MarshalMsg(nil) + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingidentityVerificationMessageSigned(t *testing.T) { + protocol.RunEncodingTest(t, &identityVerificationMessageSigned{}) +} + +func BenchmarkMarshalMsgidentityVerificationMessageSigned(b *testing.B) { + v := identityVerificationMessageSigned{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgidentityVerificationMessageSigned(b *testing.B) { + v := identityVerificationMessageSigned{} + bts := make([]byte, 0, v.Msgsize()) + bts = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalidentityVerificationMessageSigned(b *testing.B) { + v := identityVerificationMessageSigned{} + bts := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/network/netidentity.go b/network/netidentity.go new file mode 100644 index 0000000000..dd8abceff0 --- /dev/null +++ b/network/netidentity.go @@ -0,0 +1,412 @@ +// Copyright (C) 2019-2023 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package network + +import ( + "encoding/base64" + "fmt" + "net/http" + "sync/atomic" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/protocol" +) + +// netidentity.go implements functionality to participate in an "Identity Challenge Exchange" +// with the purpose of identifying redundant connections between peers, and preventing them. +// The identity challenge exchange protocol is a 3 way handshake that exchanges signed messages. +// +// Message 1 (Identity Challenge): when a request is made to start a gossip connection, an +// identityChallengeSigned message is added to HTTP request headers, containing: +// - a 32 byte random challenge +// - the requester's "identity" PublicKey +// - the PublicAddress of the intended recipient +// - Signature on the above by the requester's PublicKey +// +// Message 2 (Identity Challenge Response): when responding to the gossip connection request, +// if the identity challenge is valid, an identityChallengeResponseSigned message is added +// to the HTTP response headers, containing: +// - the original 32 byte random challenge from Message 1 +// - a new "response" 32 byte random challenge +// - the responder's "identity" PublicKey +// - Signature on the above by the responder's PublicKey +// +// Message 3 (Identity Verification): if the identityChallengeResponse is valid, the requester +// sends a NetIDVerificationTag message over websockets to verify it owns its PublicKey, with: +// - Signature on the response challenge from Message 2, using the requester's PublicKey +// +// Upon receipt of Message 2, the requester has enough data to consider the responder's identity "verified". +// Upon receipt of Message 3, the responder has enough data to consider the requester's identity "verified". +// At each of these steps, if the peer's identity was verified, wsNetwork will attempt to add it to the +// identityTracker, which maintains a single peer per identity PublicKey. If the identity is already in use +// by another connected peer, we know this connection is a duplicate, and can be closed. +// +// Protocol Enablement: +// This exchange is optional, and is enabled by setting the configuration value "PublicAddress" to match the +// node's public endpoint address stored in other peers' phonebooks (like "r-aa.algorand-mainnet.network:4160"). +// +// Protocol Error Handling: +// Message 1 +// - If the Message is not included, assume the peer does not use identity exchange, and peer without attaching an identityChallengeResponse +// - If the Address included in the challenge is not this node's PublicAddress, peering continues without identity exchange. +// this is so that if an operator misconfigures PublicAddress, it does not decline well meaning peering attempts +// - If the Message is malformed or cannot be decoded, the peering attempt is stopped +// - If the Signature in the challenge does not verify to the included key, the peering attempt is stopped +// +// Message 2 +// - If the Message is not included, assume the peer does not use identity exchange, and do not send Message 3 +// - If the Message is malformed or cannot be decoded, the peering attempt is stopped +// - If the original 32 byte challenge does not match the one sent in Message 1, the peering attempt is stopped +// - If the Signature in the challenge does not verify to the included key, the peering attempt is stopped +// +// Message 3 +// - If the Message is malformed or cannot be decoded, the peer is disconnected +// - If the Signature in the challenge does not verify peer's assumed PublicKey and assigned Challenge Bytes, the peer is disconnected +// - If the Message is not received, no action is taken to disconnect the peer. + +const maxAddressLen = 256 + 32 // Max DNS (255) + margin for port specification + +// identityChallengeValue is 32 random bytes used for identity challenge exchange +type identityChallengeValue [32]byte + +func newIdentityChallengeValue() identityChallengeValue { + var ret identityChallengeValue + crypto.RandBytes(ret[:]) + return ret +} + +type identityChallengeScheme interface { + AttachChallenge(attachTo http.Header, addr string) identityChallengeValue + VerifyRequestAndAttachResponse(attachTo http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) + VerifyResponse(h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) +} + +// identityChallengePublicKeyScheme implements IdentityChallengeScheme by +// exchanging and verifying public key challenges and attaching them to headers, +// or returning the message payload to be sent +type identityChallengePublicKeyScheme struct { + dedupName string + identityKeys *crypto.SignatureSecrets +} + +// NewIdentityChallengeScheme will create a default Identification Scheme +func NewIdentityChallengeScheme(dn string) *identityChallengePublicKeyScheme { + // without an deduplication name, there is no identityto manage, so just return an empty scheme + if dn == "" { + return &identityChallengePublicKeyScheme{} + } + var seed crypto.Seed + crypto.RandBytes(seed[:]) + + return &identityChallengePublicKeyScheme{ + dedupName: dn, + identityKeys: crypto.GenerateSignatureSecrets(seed), + } +} + +// AttachChallenge will generate a new identity challenge and will encode and attach the challenge +// as a header. It returns the identityChallengeValue used for this challenge, so the network can +// confirm it later (by passing it to VerifyResponse), or returns an empty challenge if dedupName is +// not set. +func (i identityChallengePublicKeyScheme) AttachChallenge(attachTo http.Header, addr string) identityChallengeValue { + if i.dedupName == "" || addr == "" { + return identityChallengeValue{} + } + c := identityChallenge{ + Key: i.identityKeys.SignatureVerifier, + Challenge: newIdentityChallengeValue(), + PublicAddress: []byte(addr), + } + + attachTo.Add(IdentityChallengeHeader, c.signAndEncodeB64(i.identityKeys)) + return c.Challenge +} + +// VerifyRequestAndAttachResponse checks headers for an Identity Challenge, and verifies: +// * the provided challenge bytes matches the one encoded in the header +// * the identity challenge verifies against the included key +// * the "Address" field matches what this scheme expects +// once verified, it will attach the header to the "attach" header +// and will return the challenge and identity of the peer for recording +// or returns empty values if the header did not end up getting set +func (i identityChallengePublicKeyScheme) VerifyRequestAndAttachResponse(attachTo http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) { + // if dedupName is not set, this scheme is not configured to exchange identity + if i.dedupName == "" { + return identityChallengeValue{}, crypto.PublicKey{}, nil + } + // if the headerString is not populated, the peer isn't participating in identity exchange + headerString := h.Get(IdentityChallengeHeader) + if headerString == "" { + return identityChallengeValue{}, crypto.PublicKey{}, nil + } + // decode the header to an identityChallenge + msg, err := base64.StdEncoding.DecodeString(headerString) + if err != nil { + return identityChallengeValue{}, crypto.PublicKey{}, err + } + idChal := identityChallengeSigned{} + err = protocol.Decode(msg, &idChal) + if err != nil { + return identityChallengeValue{}, crypto.PublicKey{}, err + } + if !idChal.Verify() { + return identityChallengeValue{}, crypto.PublicKey{}, fmt.Errorf("identity challenge incorrectly signed") + } + // if the address is not meant for this host, return without attaching headers, + // but also do not emit an error. This is because if an operator were to incorrectly + // specify their dedupName, it could result in inappropriate disconnections from valid peers + if string(idChal.Msg.PublicAddress) != i.dedupName { + return identityChallengeValue{}, crypto.PublicKey{}, nil + } + // make the response object, encode it and attach it to the header + r := identityChallengeResponse{ + Key: i.identityKeys.SignatureVerifier, + Challenge: idChal.Msg.Challenge, + ResponseChallenge: newIdentityChallengeValue(), + } + attachTo.Add(IdentityChallengeHeader, r.signAndEncodeB64(i.identityKeys)) + return r.ResponseChallenge, idChal.Msg.Key, nil +} + +// VerifyResponse will decode the identity challenge header from an HTTP response (containing an +// encoding of identityChallengeResponseSigned) and confirm it has a valid signature, and that the +// provided challenge (generated and added to the HTTP request by AttachChallenge) matches the one +// found in the header. If the response can be verified, it returns the identity of the peer and an +// encoded identityVerificationMessage to send to the peer. Otherwise, it returns empty values. +func (i identityChallengePublicKeyScheme) VerifyResponse(h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) { + // if we are not participating in identity challenge exchange, do nothing (no error and no value) + if i.dedupName == "" { + return crypto.PublicKey{}, []byte{}, nil + } + headerString := h.Get(IdentityChallengeHeader) + // if the header is not populated, assume the peer is not participating in identity exchange + if headerString == "" { + return crypto.PublicKey{}, []byte{}, nil + } + msg, err := base64.StdEncoding.DecodeString(headerString) + if err != nil { + return crypto.PublicKey{}, []byte{}, err + } + resp := identityChallengeResponseSigned{} + err = protocol.Decode(msg, &resp) + if err != nil { + return crypto.PublicKey{}, []byte{}, err + } + if resp.Msg.Challenge != c { + return crypto.PublicKey{}, []byte{}, fmt.Errorf("challenge response did not contain originally issued challenge value") + } + if !resp.Verify() { + return crypto.PublicKey{}, []byte{}, fmt.Errorf("challenge response incorrectly signed ") + } + return resp.Msg.Key, i.identityVerificationMessage(resp.Msg.ResponseChallenge), nil +} + +// identityVerificationMessage generates the 3rd message of the challenge exchange, +// which a wsNetwork can then send to a peer in order to verify their own identity. +// It is prefixed with the ID Verification tag and returned ready-to-send +func (i *identityChallengePublicKeyScheme) identityVerificationMessage(c identityChallengeValue) []byte { + signedMsg := identityVerificationMessage{ResponseChallenge: c}.Sign(i.identityKeys) + return append([]byte(protocol.NetIDVerificationTag), protocol.Encode(&signedMsg)...) +} + +// The initial challenge object, giving the peer a challenge to return (Challenge), +// the presumed identity of this node (Key), the intended recipient (Address). +type identityChallenge struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Key crypto.PublicKey `codec:"pk"` + Challenge identityChallengeValue `codec:"c"` + PublicAddress []byte `codec:"a,allocbound=maxAddressLen"` +} + +// identityChallengeSigned wraps an identityChallenge with a signature, similar to SignedTxn and +// netPrioResponseSigned. +type identityChallengeSigned struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Msg identityChallenge `codec:"ic"` + Signature crypto.Signature `codec:"sig"` +} + +// The response to an identityChallenge, containing the responder's public key, the original +// requestor's challenge, and a new challenge for the requestor. +type identityChallengeResponse struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Key crypto.PublicKey `codec:"pk"` + Challenge identityChallengeValue `codec:"c"` + ResponseChallenge identityChallengeValue `codec:"rc"` +} + +type identityChallengeResponseSigned struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Msg identityChallengeResponse `codec:"icr"` + Signature crypto.Signature `codec:"sig"` +} + +type identityVerificationMessage struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + ResponseChallenge identityChallengeValue `codec:"rc"` +} + +type identityVerificationMessageSigned struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Msg identityVerificationMessage `codec:"ivm"` + Signature crypto.Signature `codec:"sig"` +} + +func (i identityChallenge) signAndEncodeB64(s *crypto.SignatureSecrets) string { + signedChal := i.Sign(s) + return base64.StdEncoding.EncodeToString(protocol.Encode(&signedChal)) +} + +func (i identityChallenge) Sign(secrets *crypto.SignatureSecrets) identityChallengeSigned { + return identityChallengeSigned{Msg: i, Signature: secrets.Sign(i)} +} + +func (i identityChallenge) ToBeHashed() (protocol.HashID, []byte) { + return protocol.NetIdentityChallenge, protocol.Encode(&i) +} + +// Verify checks that the signature included in the identityChallenge was indeed created by the included Key +func (i identityChallengeSigned) Verify() bool { + return i.Msg.Key.Verify(i.Msg, i.Signature) +} + +func (i identityChallengeResponse) signAndEncodeB64(s *crypto.SignatureSecrets) string { + signedChalResp := i.Sign(s) + return base64.StdEncoding.EncodeToString(protocol.Encode(&signedChalResp)) +} + +func (i identityChallengeResponse) Sign(secrets *crypto.SignatureSecrets) identityChallengeResponseSigned { + return identityChallengeResponseSigned{Msg: i, Signature: secrets.Sign(i)} +} + +func (i identityChallengeResponse) ToBeHashed() (protocol.HashID, []byte) { + return protocol.NetIdentityChallengeResponse, protocol.Encode(&i) +} + +// Verify checks that the signature included in the identityChallengeResponse was indeed created by the included Key +func (i identityChallengeResponseSigned) Verify() bool { + return i.Msg.Key.Verify(i.Msg, i.Signature) +} + +func (i identityVerificationMessage) Sign(secrets *crypto.SignatureSecrets) identityVerificationMessageSigned { + return identityVerificationMessageSigned{Msg: i, Signature: secrets.Sign(i)} +} + +func (i identityVerificationMessage) ToBeHashed() (protocol.HashID, []byte) { + return protocol.NetIdentityVerificationMessage, protocol.Encode(&i) +} + +// Verify checks that the signature included in the identityVerificationMessage was indeed created by the included Key +func (i identityVerificationMessageSigned) Verify(key crypto.PublicKey) bool { + return key.Verify(i.Msg, i.Signature) +} + +// identityVerificationHandler receives a signature over websocket, and confirms it matches the +// sender's claimed identity and the challenge that was assigned to it. If the identity is available, +// the peer is loaded into the identity tracker. Otherwise, we ask the network to disconnect the peer. +func identityVerificationHandler(message IncomingMessage) OutgoingMessage { + peer := message.Sender.(*wsPeer) + // avoid doing work (crypto and potentially taking a lock) if the peer is already verified + if atomic.LoadUint32(&peer.identityVerified) == 1 { + return OutgoingMessage{} + } + localAddr, _ := peer.net.Address() + msg := identityVerificationMessageSigned{} + err := protocol.Decode(message.Data, &msg) + if err != nil { + networkPeerIdentityError.Inc(nil) + peer.net.log.With("err", err).With("remote", peer.OriginAddress()).With("local", localAddr).Warn("peer identity verification could not be decoded, disconnecting") + peer.net.disconnect(peer, disconnectBadIdentityData) + return OutgoingMessage{} + } + if peer.identityChallenge != msg.Msg.ResponseChallenge { + networkPeerIdentityError.Inc(nil) + peer.net.log.With("remote", peer.OriginAddress()).With("local", localAddr).Warn("peer identity verification challenge does not match, disconnecting") + peer.net.disconnect(peer, disconnectBadIdentityData) + return OutgoingMessage{} + } + if !msg.Verify(peer.identity) { + networkPeerIdentityError.Inc(nil) + peer.net.log.With("remote", peer.OriginAddress()).With("local", localAddr).Warn("peer identity verification is incorrectly signed, disconnecting") + peer.net.disconnect(peer, disconnectBadIdentityData) + return OutgoingMessage{} + } + atomic.StoreUint32(&peer.identityVerified, 1) + // if the identity could not be claimed by this peer, it means the identity is in use + peer.net.peersLock.Lock() + ok := peer.net.identityTracker.setIdentity(peer) + peer.net.peersLock.Unlock() + if !ok { + networkPeerIdentityDisconnect.Inc(nil) + peer.net.log.With("remote", peer.OriginAddress()).With("local", localAddr).Warn("peer identity already in use, disconnecting") + peer.net.disconnect(peer, disconnectDuplicateConnection) + } + return OutgoingMessage{} +} + +var identityHandlers = []TaggedMessageHandler{ + {protocol.NetIDVerificationTag, HandlerFunc(identityVerificationHandler)}, +} + +// identityTracker is used by wsNetwork to manage peer identities for connection deduplication +type identityTracker interface { + removeIdentity(p *wsPeer) + setIdentity(p *wsPeer) bool +} + +// publicKeyIdentTracker implements identityTracker by +// mapping from PublicKeys exchanged in identity challenges to a peer +// this structure is not thread-safe; it is protected by wn.peersLock. +type publicKeyIdentTracker struct { + peersByID map[crypto.PublicKey]*wsPeer +} + +// NewIdentityTracker returns a new publicKeyIdentTracker +func NewIdentityTracker() *publicKeyIdentTracker { + return &publicKeyIdentTracker{ + peersByID: make(map[crypto.PublicKey]*wsPeer), + } +} + +// setIdentity attempts to store a peer at its identity. +// returns false if it was unable to load the peer into the given identity +// or true otherwise (if the peer was already there, or if it was added) +func (t *publicKeyIdentTracker) setIdentity(p *wsPeer) bool { + existingPeer, exists := t.peersByID[p.identity] + if !exists { + // the identity is not occupied, so set it and return true + t.peersByID[p.identity] = p + return true + } + // the identity is occupied, so return false if it is occupied by some *other* peer + // or true if it is occupied by this peer + return existingPeer == p +} + +// removeIdentity removes the entry in the peersByID map if it exists +// and is occupied by the given peer +func (t *publicKeyIdentTracker) removeIdentity(p *wsPeer) { + if t.peersByID[p.identity] == p { + delete(t.peersByID, p.identity) + } +} diff --git a/network/netidentity_test.go b/network/netidentity_test.go new file mode 100644 index 0000000000..f3c72e3e8c --- /dev/null +++ b/network/netidentity_test.go @@ -0,0 +1,366 @@ +// Copyright (C) 2019-2023 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package network + +import ( + "encoding/base64" + "net/http" + "testing" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/partitiontest" + "github.com/stretchr/testify/require" +) + +// if the scheme has a dedup name, attach to headers. otherwise, don't +func TestIdentityChallengeSchemeAttachIfEnabled(t *testing.T) { + partitiontest.PartitionTest(t) + + h := http.Header{} + i := NewIdentityChallengeScheme("") + chal := i.AttachChallenge(h, "other") + require.Empty(t, h.Get(IdentityChallengeHeader)) + require.Empty(t, chal) + + j := NewIdentityChallengeScheme("yes") + chal = j.AttachChallenge(h, "other") + require.NotEmpty(t, h.Get(IdentityChallengeHeader)) + require.NotEmpty(t, chal) +} + +// TestIdentityChallengeSchemeVerifyRequestAndAttachResponse will confirm that the scheme +// attaches responses only if dedup name is set and the provided challenge verifies +func TestIdentityChallengeSchemeVerifyRequestAndAttachResponse(t *testing.T) { + partitiontest.PartitionTest(t) + + i := NewIdentityChallengeScheme("i1") + // author a challenge to the other scheme + h := http.Header{} + i.AttachChallenge(h, "i2") + require.NotEmpty(t, h.Get(IdentityChallengeHeader)) + + // without a dedup name, no response and no error + h = http.Header{} + i.AttachChallenge(h, "i2") + r := http.Header{} + i2 := NewIdentityChallengeScheme("") + chal, key, err := i2.VerifyRequestAndAttachResponse(r, h) + require.Empty(t, r.Get(IdentityChallengeHeader)) + require.Empty(t, chal) + require.Empty(t, key) + require.NoError(t, err) + + // if dedup name doesn't match, no response and no error + h = http.Header{} + i.AttachChallenge(h, "i2") + r = http.Header{} + i2 = NewIdentityChallengeScheme("not i2") + chal, key, err = i2.VerifyRequestAndAttachResponse(r, h) + require.Empty(t, r.Get(IdentityChallengeHeader)) + require.Empty(t, chal) + require.Empty(t, key) + require.NoError(t, err) + + // if the challenge can't be decoded or verified, error + h = http.Header{} + h.Add(IdentityChallengeHeader, "garbage") + r = http.Header{} + i2 = NewIdentityChallengeScheme("i2") + chal, key, err = i2.VerifyRequestAndAttachResponse(r, h) + require.Empty(t, r.Get(IdentityChallengeHeader)) + require.Empty(t, chal) + require.Empty(t, key) + require.Error(t, err) + + // happy path: response should be attached here + h = http.Header{} + i.AttachChallenge(h, "i2") + r = http.Header{} + i2 = NewIdentityChallengeScheme("i2") + chal, key, err = i2.VerifyRequestAndAttachResponse(r, h) + require.NotEmpty(t, r.Get(IdentityChallengeHeader)) + require.NotEmpty(t, chal) + require.NotEmpty(t, key) + require.NoError(t, err) +} + +func TestIdentityChallengeNoErrorWhenNotParticipating(t *testing.T) { + partitiontest.PartitionTest(t) + + // blank deduplication name will make the scheme a no-op + iNotParticipate := NewIdentityChallengeScheme("") + + // create a request header first + h := http.Header{} + i := NewIdentityChallengeScheme("i1") + origChal := i.AttachChallenge(h, "i1") + require.NotEmpty(t, h.Get(IdentityChallengeHeader)) + require.NotEmpty(t, origChal) + + // confirm a nil scheme will not return values or error + c, k, err := iNotParticipate.VerifyRequestAndAttachResponse(http.Header{}, h) + require.Empty(t, c) + require.Empty(t, k) + require.NoError(t, err) + + // create a response + h2 := http.Header{} + i2 := NewIdentityChallengeScheme("i2") + i2.VerifyRequestAndAttachResponse(h2, h) + + // confirm a nil scheme will not return values or error + k2, bytes, err := iNotParticipate.VerifyResponse(h2, identityChallengeValue{}) + require.Empty(t, k2) + require.Empty(t, bytes) + require.NoError(t, err) + + // add broken payload to a new header and try inspecting it with the empty scheme + h3 := http.Header{} + h3.Add(IdentityChallengeHeader, "broken text!") + c, k, err = iNotParticipate.VerifyRequestAndAttachResponse(http.Header{}, h) + require.Empty(t, c) + require.Empty(t, k) + require.NoError(t, err) + k2, bytes, err = iNotParticipate.VerifyResponse(h2, identityChallengeValue{}) + require.Empty(t, k2) + require.Empty(t, bytes) + require.NoError(t, err) +} + +// TestIdentityChallengeSchemeVerifyResponse confirms the scheme will +// attach responses only if dedup name is set and the provided challenge verifies +func TestIdentityChallengeSchemeVerifyResponse(t *testing.T) { + partitiontest.PartitionTest(t) + + h := http.Header{} + i := NewIdentityChallengeScheme("i1") + // author a challenge to ourselves + origChal := i.AttachChallenge(h, "i1") + require.NotEmpty(t, h.Get(IdentityChallengeHeader)) + require.NotEmpty(t, origChal) + r := http.Header{} + + respChal, key, err := i.VerifyRequestAndAttachResponse(r, h) + require.NotEmpty(t, r.Get(IdentityChallengeHeader)) + require.NotEmpty(t, respChal) + require.NotEmpty(t, key) + require.NoError(t, err) + + // respChal2 should match respChal as it is being passed back to the original peer + // while origChal will be used for verification + key2, verificationMsg, err := i.VerifyResponse(r, origChal) + require.NotEmpty(t, verificationMsg) + require.NoError(t, err) + // because we sent this to ourselves, we can confirm the keys match + require.Equal(t, key, key2) +} + +// TestIdentityChallengeSchemeBadSignature tests that the scheme will +// fail to verify and attach if the challenge is incorrectly signed +func TestIdentityChallengeSchemeBadSignature(t *testing.T) { + partitiontest.PartitionTest(t) + + h := http.Header{} + i := NewIdentityChallengeScheme("i1") + // Copy the logic of attaching the header and signing so we can sign it wrong + c := identityChallengeSigned{ + Msg: identityChallenge{ + Key: i.identityKeys.SignatureVerifier, + Challenge: newIdentityChallengeValue(), + PublicAddress: []byte("i1"), + }} + c.Signature = i.identityKeys.SignBytes([]byte("WRONG BYTES SIGNED")) + enc := protocol.Encode(&c) + b64enc := base64.StdEncoding.EncodeToString(enc) + h.Add(IdentityChallengeHeader, b64enc) + + // observe that VerifyRequestAndAttachResponse returns error on bad signature + r := http.Header{} + respChal, key, err := i.VerifyRequestAndAttachResponse(r, h) + require.Empty(t, r.Get(IdentityChallengeHeader)) + require.Empty(t, respChal) + require.Empty(t, key) + require.Error(t, err) +} + +// TestIdentityChallengeSchemeBadPayload tests that the scheme will +// fail to verify if the challenge can't be B64 decoded +func TestIdentityChallengeSchemeBadPayload(t *testing.T) { + partitiontest.PartitionTest(t) + + h := http.Header{} + i := NewIdentityChallengeScheme("i1") + h.Add(IdentityChallengeHeader, "NOT VALID BASE 64! :)") + + // observe that VerifyRequestAndAttachResponse won't do anything on bad signature + r := http.Header{} + respChal, key, err := i.VerifyRequestAndAttachResponse(r, h) + require.Empty(t, r.Get(IdentityChallengeHeader)) + require.Empty(t, respChal) + require.Empty(t, key) + require.Error(t, err) +} + +// TestIdentityChallengeSchemeBadResponseSignature tests that the scheme will +// fail to verify if the challenge response is incorrectly signed +func TestIdentityChallengeSchemeBadResponseSignature(t *testing.T) { + partitiontest.PartitionTest(t) + + h := http.Header{} + i := NewIdentityChallengeScheme("i1") + // author a challenge to ourselves + origChal := i.AttachChallenge(h, "i1") + require.NotEmpty(t, h.Get(IdentityChallengeHeader)) + require.NotEmpty(t, origChal) + + // use the code to sign and encode responses so we can sign incorrectly + r := http.Header{} + resp := identityChallengeResponseSigned{ + Msg: identityChallengeResponse{ + Key: i.identityKeys.SignatureVerifier, + Challenge: origChal, + ResponseChallenge: newIdentityChallengeValue(), + }} + resp.Signature = i.identityKeys.SignBytes([]byte("BAD BYTES FOR SIGNING")) + enc := protocol.Encode(&resp) + b64enc := base64.StdEncoding.EncodeToString(enc) + r.Add(IdentityChallengeHeader, b64enc) + + key2, verificationMsg, err := i.VerifyResponse(r, origChal) + require.Empty(t, key2) + require.Empty(t, verificationMsg) + require.Error(t, err) +} + +// TestIdentityChallengeSchemeBadResponsePayload tests that the scheme will +// fail to verify if the challenge response can't be B64 decoded +func TestIdentityChallengeSchemeBadResponsePayload(t *testing.T) { + partitiontest.PartitionTest(t) + + h := http.Header{} + i := NewIdentityChallengeScheme("i1") + // author a challenge to ourselves + origChal := i.AttachChallenge(h, "i1") + require.NotEmpty(t, h.Get(IdentityChallengeHeader)) + require.NotEmpty(t, origChal) + + // generate a bad payload that should not decode + r := http.Header{} + r.Add(IdentityChallengeHeader, "BAD B64 ENCODING :)") + + key2, verificationMsg, err := i.VerifyResponse(r, origChal) + require.Empty(t, key2) + require.Empty(t, verificationMsg) + require.Error(t, err) +} + +// TestIdentityChallengeSchemeWrongChallenge the scheme will +// return "0" if the challenge does not match upon return +func TestIdentityChallengeSchemeWrongChallenge(t *testing.T) { + partitiontest.PartitionTest(t) + + h := http.Header{} + i := NewIdentityChallengeScheme("i1") + // author a challenge to ourselves + origChal := i.AttachChallenge(h, "i1") + require.NotEmpty(t, h.Get(IdentityChallengeHeader)) + require.NotEmpty(t, origChal) + + r := http.Header{} + respChal, key, err := i.VerifyRequestAndAttachResponse(r, h) + require.NotEmpty(t, r.Get(IdentityChallengeHeader)) + require.NotEmpty(t, respChal) + require.NotEmpty(t, key) + require.NoError(t, err) + + // Attempt to verify against the wrong challenge + key2, verificationMsg, err := i.VerifyResponse(r, newIdentityChallengeValue()) + require.Empty(t, key2) + require.Empty(t, verificationMsg) + require.Error(t, err) +} + +func TestNewIdentityTracker(t *testing.T) { + partitiontest.PartitionTest(t) + + tracker := NewIdentityTracker() + require.Empty(t, tracker.peersByID) +} + +func TestIdentityTrackerRemoveIdentity(t *testing.T) { + partitiontest.PartitionTest(t) + + tracker := NewIdentityTracker() + id := crypto.PublicKey{} + p := wsPeer{identity: id} + + id2 := crypto.PublicKey{} + p2 := wsPeer{identity: id2} + + // Ensure the first attempt to insert populates the map + _, exists := tracker.peersByID[p.identity] + require.False(t, exists) + require.True(t, tracker.setIdentity(&p)) + _, exists = tracker.peersByID[p.identity] + require.True(t, exists) + + // check that removing a peer who does not exist in the map (but whos identity does) + // not not result in the wrong peer being removed + tracker.removeIdentity(&p2) + _, exists = tracker.peersByID[p.identity] + require.True(t, exists) + + tracker.removeIdentity(&p) + _, exists = tracker.peersByID[p.identity] + require.False(t, exists) +} + +func TestIdentityTrackerSetIdentity(t *testing.T) { + partitiontest.PartitionTest(t) + + tracker := NewIdentityTracker() + id := crypto.PublicKey{} + p := wsPeer{identity: id} + + // Ensure the first attempt to insert populates the map + _, exists := tracker.peersByID[p.identity] + require.False(t, exists) + require.True(t, tracker.setIdentity(&p)) + _, exists = tracker.peersByID[p.identity] + require.True(t, exists) + + // Ensure the next attempt to insert also returns true + require.True(t, tracker.setIdentity(&p)) + + // Ensure a different peer cannot take the map entry + otherP := wsPeer{identity: id} + require.False(t, tracker.setIdentity(&otherP)) + + // Ensure the entry in the map wasn't changed + require.Equal(t, tracker.peersByID[p.identity], &p) +} + +// Just tests that if a peer is already verified, it just returns OutgoingMessage{} +func TestHandlerGuard(t *testing.T) { + partitiontest.PartitionTest(t) + p := wsPeer{identityVerified: uint32(1)} + msg := IncomingMessage{ + Sender: &p, + } + require.Equal(t, OutgoingMessage{}, identityVerificationHandler(msg)) +} diff --git a/network/phonebook.go b/network/phonebook.go index 1ff3ed542d..ac5914a299 100644 --- a/network/phonebook.go +++ b/network/phonebook.go @@ -30,6 +30,8 @@ const getAllAddresses = math.MaxInt32 // PhoneBookEntryRoles defines the roles that a single entry on the phonebook can take. // currently, we have two roles : relay role and archiver role, which are mutually exclusive. +// +//msgp:ignore PhoneBookEntryRoles type PhoneBookEntryRoles int // PhoneBookEntryRelayRole used for all the relays that are provided either via the algobootstrap SRV record diff --git a/network/requestTracker.go b/network/requestTracker.go index d2f05d8bf6..025d75a5a6 100644 --- a/network/requestTracker.go +++ b/network/requestTracker.go @@ -148,6 +148,7 @@ func (ard *hostIncomingRequests) countConnections(rateLimitingWindowStartTime ti return uint(len(ard.requests) - i + len(ard.additionalHostRequests)) } +//msgp:ignore hostsIncomingMap type hostsIncomingMap map[string]*hostIncomingRequests // pruneRequests cleans stale items from the hostRequests maps diff --git a/network/topics.go b/network/topics.go index 762312585d..ded264a0cb 100644 --- a/network/topics.go +++ b/network/topics.go @@ -30,6 +30,8 @@ const ( ) // Topic is a key-value pair +// +//msgp:ignore Topic type Topic struct { key string data []byte @@ -43,6 +45,8 @@ func MakeTopic(key string, data []byte) Topic { // Topics is an array of type Topic // The maximum number of topics allowed is 32 // Each topic key can be 64 characters long and cannot be size 0 +// +//msgp:ignore Topics type Topics []Topic // MarshallTopics serializes the topics into a byte array diff --git a/network/wsNetwork.go b/network/wsNetwork.go index e8c9e97421..b537818c65 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -101,6 +101,9 @@ const slowWritingPeerMonitorInterval = 5 * time.Second // to the log file. Note that the log file itself would also json-encode these before placing them in the log file. const unprintableCharacterGlyph = "▯" +// match config.PublicAddress to this string to automatically set PublicAddress from Address() +const autoconfigPublicAddress = "auto" + var networkIncomingConnections = metrics.MakeGauge(metrics.NetworkIncomingConnections) var networkOutgoingConnections = metrics.MakeGauge(metrics.NetworkOutgoingConnections) @@ -113,6 +116,9 @@ var networkBroadcastSendMicros = metrics.MakeCounter(metrics.MetricName{Name: "a var networkBroadcastsDropped = metrics.MakeCounter(metrics.MetricName{Name: "algod_broadcasts_dropped_total", Description: "number of broadcast messages not sent to any peer"}) var networkPeerBroadcastDropped = metrics.MakeCounter(metrics.MetricName{Name: "algod_peer_broadcast_dropped_total", Description: "number of broadcast messages not sent to some peer"}) +var networkPeerIdentityDisconnect = metrics.MakeCounter(metrics.MetricName{Name: "algod_network_identity_duplicate", Description: "number of times identity challenge cause us to disconnect a peer"}) +var networkPeerIdentityError = metrics.MakeCounter(metrics.MetricName{Name: "algod_network_identity_error", Description: "number of times an error occurs (besides expected) when processing identity challenges"}) + var networkSlowPeerDrops = metrics.MakeCounter(metrics.MetricName{Name: "algod_network_slow_drops_total", Description: "number of peers dropped for being slow to send to"}) var networkIdlePeerDrops = metrics.MakeCounter(metrics.MetricName{Name: "algod_network_idle_drops_total", Description: "number of peers dropped due to idle connection"}) var networkBroadcastQueueFull = metrics.MakeCounter(metrics.MetricName{Name: "algod_network_broadcast_queue_full_total", Description: "number of messages that were drops due to full broadcast queue"}) @@ -141,6 +147,8 @@ const peerShutdownDisconnectionAckDuration = 50 * time.Millisecond type Peer interface{} // PeerOption allows users to specify a subset of peers to query +// +//msgp:ignore PeerOption type PeerOption int const ( @@ -258,6 +266,8 @@ type OutgoingMessage struct { } // ForwardingPolicy is an enum indicating to whom we should send a message +// +//msgp:ignore ForwardingPolicy type ForwardingPolicy int const ( @@ -376,6 +386,10 @@ type WebsocketNetwork struct { prioTracker *prioTracker prioResponseChan chan *wsPeer + // identity challenge scheme for creating challenges and responding + identityScheme identityChallengeScheme + identityTracker identityTracker + // outgoingMessagesBufferSize is the size used for outgoing messages. outgoingMessagesBufferSize int @@ -741,6 +755,8 @@ func (wn *WebsocketNetwork) setup() { config.Consensus[protocol.ConsensusCurrentVersion].DownCommitteeSize), ) + wn.identityTracker = NewIdentityTracker() + wn.broadcastQueueHighPrio = make(chan broadcastRequest, wn.outgoingMessagesBufferSize) wn.broadcastQueueBulk = make(chan broadcastRequest, 100) wn.meshUpdateRequests = make(chan meshRequest, 5) @@ -823,6 +839,25 @@ func (wn *WebsocketNetwork) Start() { } else { wn.scheme = "http" } + + // if PublicAddress set to automatic, pull the name from Address() + if wn.config.PublicAddress == autoconfigPublicAddress { + addr, ok := wn.Address() + if ok { + url, err := url.Parse(addr) + if err == nil { + wn.config.PublicAddress = fmt.Sprintf("%s:%s", url.Hostname(), url.Port()) + } + } + } + // if the network has a public address, use that as the name for connection deduplication + if wn.config.PublicAddress != "" { + wn.RegisterHandlers(identityHandlers) + } + if wn.identityScheme == nil && wn.config.PublicAddress != "" { + wn.identityScheme = NewIdentityChallengeScheme(wn.config.PublicAddress) + } + wn.meshUpdateRequests <- meshRequest{false, nil} if wn.prioScheme != nil { wn.RegisterHandlers(prioHandlers) @@ -1144,6 +1179,20 @@ func (wn *WebsocketNetwork) ServeHTTP(response http.ResponseWriter, request *htt challenge = wn.prioScheme.NewPrioChallenge() responseHeader.Set(PriorityChallengeHeader, challenge) } + + localAddr, _ := wn.Address() + var peerIDChallenge identityChallengeValue + var peerID crypto.PublicKey + if wn.identityScheme != nil { + var err error + peerIDChallenge, peerID, err = wn.identityScheme.VerifyRequestAndAttachResponse(responseHeader, request.Header) + if err != nil { + networkPeerIdentityError.Inc(nil) + wn.log.With("err", err).With("remote", trackedRequest.otherPublicAddr).With("local", localAddr).Warnf("peer (%s) supplied an invalid identity challenge, abandoning peering", trackedRequest.otherPublicAddr) + return + } + } + conn, err := wn.upgrader.Upgrade(response, request, responseHeader) if err != nil { wn.log.Info("ws upgrade fail ", err) @@ -1165,12 +1214,14 @@ func (wn *WebsocketNetwork) ServeHTTP(response http.ResponseWriter, request *htt prioChallenge: challenge, createTime: trackedRequest.created, version: matchingVersion, + identity: peerID, + identityChallenge: peerIDChallenge, + identityVerified: 0, features: decodePeerFeatures(matchingVersion, request.Header.Get(PeerFeaturesHeader)), } peer.TelemetryGUID = trackedRequest.otherTelemetryGUID peer.init(wn.config, wn.outgoingMessagesBufferSize) wn.addPeer(peer) - localAddr, _ := wn.Address() wn.log.With("event", "ConnectedIn").With("remote", trackedRequest.otherPublicAddr).With("local", localAddr).Infof("Accepted incoming connection from peer %s", trackedRequest.otherPublicAddr) wn.log.EventWithDetails(telemetryspec.Network, telemetryspec.ConnectPeerEvent, telemetryspec.PeerEventDetails{ @@ -1701,7 +1752,7 @@ func (wn *WebsocketNetwork) checkNewConnectionsNeeded() bool { newAddrs := wn.phonebook.GetAddresses(desired+numOutgoingTotal, PhoneBookEntryRelayRole) for _, na := range newAddrs { if na == wn.config.PublicAddress { - // filter out self-public address, so we won't try to connect to outselves. + // filter out self-public address, so we won't try to connect to ourselves. continue } gossipAddr, ok := wn.tryConnectReserveAddr(na) @@ -1957,6 +2008,9 @@ const InstanceNameHeader = "X-Algorand-InstanceName" // PriorityChallengeHeader HTTP header informs a client about the challenge it should sign to increase network priority. const PriorityChallengeHeader = "X-Algorand-PriorityChallenge" +// IdentityChallengeHeader is used to exchange IdentityChallenges +const IdentityChallengeHeader = "X-Algorand-IdentityChallenge" + // TooManyRequestsRetryAfterHeader HTTP header let the client know when to make the next connection attempt const TooManyRequestsRetryAfterHeader = "Retry-After" @@ -2113,6 +2167,12 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { for _, supportedProtocolVersion := range wn.supportedProtocolVersions { requestHeader.Add(ProtocolAcceptVersionHeader, supportedProtocolVersion) } + + var idChallenge identityChallengeValue + if wn.identityScheme != nil { + idChallenge = wn.identityScheme.AttachChallenge(requestHeader, addr) + } + // for backward compatibility, include the ProtocolVersion header as well. requestHeader.Set(ProtocolVersionHeader, wn.protocolVersion) // set the features header (comma-separated list) @@ -2164,13 +2224,41 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { return } + // if we abort before making a wsPeer this cleanup logic will close the connection + closeEarly := func(msg string) { + deadline := time.Now().Add(peerDisconnectionAckDuration) + err := conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseProtocolError, msg), deadline) + if err != nil { + wn.log.Infof("tryConnect: failed to write CloseMessage to connection for %s", conn.RemoteAddr().String()) + } + err = conn.CloseWithoutFlush() + if err != nil { + wn.log.Infof("tryConnect: failed to CloseWithoutFlush to connection for %s", conn.RemoteAddr().String()) + } + } + // no need to test the response.StatusCode since we know it's going to be http.StatusSwitchingProtocols, as it's already being tested inside websocketDialer.DialContext. // we need to examine the headers here to extract which protocol version we should be using. responseHeaderOk, matchingVersion := wn.checkServerResponseVariables(response.Header, gossipAddr) if !responseHeaderOk { // The error was already logged, so no need to log again. + closeEarly("Unsupported headers") return } + localAddr, _ := wn.Address() + + var peerID crypto.PublicKey + var idVerificationMessage []byte + if wn.identityScheme != nil { + // if the peer responded with an identity challenge response, but it can't be verified, don't proceed with peering + peerID, idVerificationMessage, err = wn.identityScheme.VerifyResponse(response.Header, idChallenge) + if err != nil { + networkPeerIdentityError.Inc(nil) + wn.log.With("err", err).With("remote", addr).With("local", localAddr).Warn("peer supplied an invalid identity response, abandoning peering") + closeEarly("Invalid identity response") + return + } + } throttledConnection := false if atomic.AddInt32(&wn.throttledOutgoingConnections, int32(-1)) >= 0 { @@ -2188,12 +2276,28 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { connMonitor: wn.connPerfMonitor, throttledOutgoingConnection: throttledConnection, version: matchingVersion, + identity: peerID, features: decodePeerFeatures(matchingVersion, response.Header.Get(PeerFeaturesHeader)), } peer.TelemetryGUID, peer.InstanceName, _ = getCommonHeaders(response.Header) + + // if there is a final verification message to send, it means this peer has a verified identity, + // attempt to set the peer and identityTracker + if len(idVerificationMessage) > 0 { + atomic.StoreUint32(&peer.identityVerified, uint32(1)) + wn.peersLock.Lock() + ok := wn.identityTracker.setIdentity(peer) + wn.peersLock.Unlock() + if !ok { + networkPeerIdentityDisconnect.Inc(nil) + wn.log.With("remote", addr).With("local", localAddr).Warn("peer deduplicated before adding because the identity is already known") + closeEarly("Duplicate connection") + return + } + } peer.init(wn.config, wn.outgoingMessagesBufferSize) wn.addPeer(peer) - localAddr, _ := wn.Address() + wn.log.With("event", "ConnectedOut").With("remote", addr).With("local", localAddr).Infof("Made outgoing connection to peer %v", addr) wn.log.EventWithDetails(telemetryspec.Network, telemetryspec.ConnectPeerEvent, telemetryspec.PeerEventDetails{ @@ -2206,6 +2310,14 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { wn.maybeSendMessagesOfInterest(peer, nil) + // if there is a final identification verification message to send, send it to the peer + if len(idVerificationMessage) > 0 { + sent := peer.writeNonBlock(context.Background(), idVerificationMessage, true, crypto.Digest{}, time.Now()) + if !sent { + wn.log.With("remote", addr).With("local", localAddr).Warn("could not send identity challenge verification") + } + } + peers.Set(uint64(wn.NumPeers())) outgoingPeers.Set(uint64(wn.numOutgoingPeers())) @@ -2333,6 +2445,7 @@ func (wn *WebsocketNetwork) removePeer(peer *wsPeer, reason disconnectReason) { if peer.peerIndex < len(wn.peers) && wn.peers[peer.peerIndex] == peer { heap.Remove(peersHeap{wn}, peer.peerIndex) wn.prioTracker.removePeer(peer) + wn.identityTracker.removeIdentity(peer) if peer.throttledOutgoingConnection { atomic.AddInt32(&wn.throttledOutgoingConnections, int32(1)) } @@ -2344,6 +2457,8 @@ func (wn *WebsocketNetwork) removePeer(peer *wsPeer, reason disconnectReason) { func (wn *WebsocketNetwork) addPeer(peer *wsPeer) { wn.peersLock.Lock() defer wn.peersLock.Unlock() + // simple duplicate *pointer* check. should never trigger given the callers to addPeer + // TODO: remove this after making sure it is safe to do so for _, p := range wn.peers { if p == peer { wn.log.Errorf("dup peer added %#v", peer) diff --git a/network/wsNetwork_test.go b/network/wsNetwork_test.go index ac9a757214..c86eed11a5 100644 --- a/network/wsNetwork_test.go +++ b/network/wsNetwork_test.go @@ -19,6 +19,7 @@ package network import ( "bytes" "context" + "encoding/base64" "encoding/binary" "encoding/json" "fmt" @@ -119,7 +120,7 @@ func init() { defaultConfig.MaxConnectionsPerIP = 30 } -func makeTestWebsocketNodeWithConfig(t testing.TB, conf config.Local) *WebsocketNetwork { +func makeTestWebsocketNodeWithConfig(t testing.TB, conf config.Local, opts ...testWebsocketOption) *WebsocketNetwork { log := logging.TestingLog(t) log.SetLevel(logging.Level(conf.BaseLoggerDebugLevel)) wn := &WebsocketNetwork{ @@ -129,13 +130,32 @@ func makeTestWebsocketNodeWithConfig(t testing.TB, conf config.Local) *Websocket GenesisID: "go-test-network-genesis", NetworkID: config.Devtestnet, } + // apply options to newly-created WebsocketNetwork, if provided + for _, opt := range opts { + opt.applyOpt(wn) + } + wn.setup() wn.eventualReadyDelay = time.Second return wn } -func makeTestWebsocketNode(t testing.TB) *WebsocketNetwork { - return makeTestWebsocketNodeWithConfig(t, defaultConfig) +// interface for providing extra options to makeTestWebsocketNode +type testWebsocketOption interface { + applyOpt(wn *WebsocketNetwork) +} + +// option to add KV to wn base logger +type testWebsocketLogNameOption struct{ logName string } + +func (o testWebsocketLogNameOption) applyOpt(wn *WebsocketNetwork) { + if o.logName != "" { + wn.log = wn.log.With("name", o.logName) + } +} + +func makeTestWebsocketNode(t testing.TB, opts ...testWebsocketOption) *WebsocketNetwork { + return makeTestWebsocketNodeWithConfig(t, defaultConfig, opts...) } type messageCounterHandler struct { @@ -1111,6 +1131,900 @@ func TestGetPeers(t *testing.T) { assert.Equal(t, expectAddrs, peerAddrs) } +// confirms that if the config PublicAddress is set to "auto", +// PublicAddress is loaded when possible with the value of Address() +func TestAutoPublicAddress(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + netA := makeTestWebsocketNode(t) + netA.config.PublicAddress = "auto" + netA.config.GossipFanout = 1 + + netA.Start() + + time.Sleep(100 * time.Millisecond) + + // check that "auto" has been overloaded + addr, ok := netA.Address() + addr = hostAndPort(addr) + require.True(t, ok) + require.NotEqual(t, "auto", netA.PublicAddress()) + require.Equal(t, addr, netA.PublicAddress()) +} + +// mock an identityTracker +type mockIdentityTracker struct { + isOccupied bool + setCount int + insertCount int + removeCount int + lock deadlock.Mutex + realTracker identityTracker +} + +func newMockIdentityTracker(realTracker identityTracker) *mockIdentityTracker { + return &mockIdentityTracker{ + isOccupied: false, + setCount: 0, + insertCount: 0, + removeCount: 0, + realTracker: realTracker, + } +} + +func (d *mockIdentityTracker) setIsOccupied(b bool) { + d.lock.Lock() + defer d.lock.Unlock() + d.isOccupied = b +} +func (d *mockIdentityTracker) removeIdentity(p *wsPeer) { + d.lock.Lock() + defer d.lock.Unlock() + d.removeCount++ + d.realTracker.removeIdentity(p) +} +func (d *mockIdentityTracker) getInsertCount() int { + d.lock.Lock() + defer d.lock.Unlock() + return d.insertCount +} +func (d *mockIdentityTracker) getRemoveCount() int { + d.lock.Lock() + defer d.lock.Unlock() + return d.removeCount +} +func (d *mockIdentityTracker) getSetCount() int { + d.lock.Lock() + defer d.lock.Unlock() + return d.setCount +} +func (d *mockIdentityTracker) setIdentity(p *wsPeer) bool { + d.lock.Lock() + defer d.lock.Unlock() + d.setCount++ + // isOccupied is true, meaning we're overloading the "ok" return to false + if d.isOccupied { + return false + } + ret := d.realTracker.setIdentity(p) + if ret { + d.insertCount++ + } + return ret +} + +func hostAndPort(u string) string { + url, err := url.Parse(u) + if err == nil { + return fmt.Sprintf("%s:%s", url.Hostname(), url.Port()) + } + return "" +} + +// TestPeeringWithIdentityChallenge tests the happy path of connecting with identity challenge: +// - both peers have correctly set PublicAddress +// - both should exchange identities and verify +// - both peers should be able to deduplicate connections +func TestPeeringWithIdentityChallenge(t *testing.T) { + partitiontest.PartitionTest(t) + + netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"}) + netA.identityTracker = newMockIdentityTracker(netA.identityTracker) + netA.config.PublicAddress = "auto" + netA.config.GossipFanout = 1 + + netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"}) + netB.identityTracker = newMockIdentityTracker(netB.identityTracker) + netB.config.PublicAddress = "auto" + netB.config.GossipFanout = 1 + + netA.Start() + defer netA.Stop() + netB.Start() + defer netB.Stop() + + addrA, ok := netA.Address() + require.True(t, ok) + gossipA, err := netA.addrToGossipAddr(addrA) + require.NoError(t, err) + + addrB, ok := netB.Address() + require.True(t, ok) + gossipB, err := netB.addrToGossipAddr(addrB) + require.NoError(t, err) + + // set addresses to just host:port to match phonebook/dns format + addrA = hostAndPort(addrA) + addrB = hostAndPort(addrB) + + // first connection should work just fine + if _, ok := netA.tryConnectReserveAddr(addrB); ok { + netA.wg.Add(1) + netA.tryConnect(addrB, gossipB) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + // just one A->B connection + assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut))) + + // confirm identity map was added to for both hosts + assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getInsertCount()) + + // netB has to wait for a final verification message over WS Handler, so pause a moment + time.Sleep(250 * time.Millisecond) + assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getInsertCount()) + + // bi-directional connection from B should not proceed + if _, ok := netB.tryConnectReserveAddr(addrA); ok { + netB.wg.Add(1) + netB.tryConnect(addrA, gossipA) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + + // still just one A->B connection + assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut))) + // netA never attempts to set identity as it never sees a verified identity + assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getSetCount()) + // netB would attempt to add the identity to the tracker + // but it would not end up being added + assert.Equal(t, 2, netB.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getInsertCount()) + + // Check deduplication again, this time from A + // the "ok" from tryConnectReserveAddr is overloaded here because isConnectedTo + // will prevent this connection from attempting in the first place + // in the real world, that isConnectedTo doesn't always trigger, if the hosts are behind + // a load balancer or other NAT + if _, ok := netA.tryConnectReserveAddr(addrB); ok || true { + netA.wg.Add(1) + netA.tryConnect(addrB, gossipB) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + + // netB never tries to add a new identity, since the connection gets abandoned before it is verified + assert.Equal(t, 2, netB.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getInsertCount()) + // still just one A->B connection + assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut))) + assert.Equal(t, 2, netA.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getInsertCount()) + + // Now have A connect to node C, which has the same PublicAddress as B (e.g., because it shares the + // same public load balancer endpoint). C will have a different identity keypair and so will not be + // considered a duplicate. + netC := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netC"}) + netC.identityTracker = newMockIdentityTracker(netC.identityTracker) + netC.config.PublicAddress = addrB + netC.config.GossipFanout = 1 + + netC.Start() + defer netC.Stop() + + addrC, ok := netC.Address() + require.True(t, ok) + gossipC, err := netC.addrToGossipAddr(addrC) + require.NoError(t, err) + addrC = hostAndPort(addrC) + + // A connects to C (but uses addrB here to simulate case where B & C have the same PublicAddress) + netA.wg.Add(1) + netA.tryConnect(addrB, gossipC) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + + // A->B and A->C both open + assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, 2, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut))) + assert.Equal(t, 1, len(netC.GetPeers(PeersConnectedIn))) + assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut))) + + // confirm identity map was added to for both hosts + assert.Equal(t, 3, netA.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 2, netA.identityTracker.(*mockIdentityTracker).getInsertCount()) + + // netC has to wait for a final verification message over WS Handler, so pause a moment + time.Sleep(250 * time.Millisecond) + assert.Equal(t, 1, netC.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 1, netC.identityTracker.(*mockIdentityTracker).getInsertCount()) + +} + +// TestPeeringSenderIdentityChallengeOnly will confirm that if only the Sender +// Uses Identity, no identity exchange happens in the connection +func TestPeeringSenderIdentityChallengeOnly(t *testing.T) { + partitiontest.PartitionTest(t) + + netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"}) + netA.identityTracker = newMockIdentityTracker(netA.identityTracker) + netA.config.PublicAddress = "auto" + netA.config.GossipFanout = 1 + + netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"}) + netB.identityTracker = newMockIdentityTracker(netB.identityTracker) + //netB.config.PublicAddress = "auto" + netB.config.GossipFanout = 1 + + netA.Start() + defer netA.Stop() + netB.Start() + defer netB.Stop() + + addrA, ok := netA.Address() + require.True(t, ok) + gossipA, err := netA.addrToGossipAddr(addrA) + require.NoError(t, err) + + addrB, ok := netB.Address() + require.True(t, ok) + gossipB, err := netB.addrToGossipAddr(addrB) + require.NoError(t, err) + + // set addresses to just host:port to match phonebook/dns format + addrA = hostAndPort(addrA) + addrB = hostAndPort(addrB) + + // first connection should work just fine + if _, ok := netA.tryConnectReserveAddr(addrB); ok { + netA.wg.Add(1) + netA.tryConnect(addrB, gossipB) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn))) + + // confirm identity map was not added to for either host + assert.Equal(t, 0, netA.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 0, netB.identityTracker.(*mockIdentityTracker).getSetCount()) + + // bi-directional connection should also work + if _, ok := netB.tryConnectReserveAddr(addrA); ok { + netB.wg.Add(1) + netB.tryConnect(addrA, gossipA) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + // the nodes are connected redundantly + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedOut))) + // confirm identity map was not added to for either host + assert.Equal(t, 0, netA.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 0, netB.identityTracker.(*mockIdentityTracker).getSetCount()) +} + +// TestPeeringReceiverIdentityChallengeOnly will confirm that if only the Receiver +// Uses Identity, no identity exchange happens in the connection +func TestPeeringReceiverIdentityChallengeOnly(t *testing.T) { + partitiontest.PartitionTest(t) + + netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"}) + netA.identityTracker = newMockIdentityTracker(netA.identityTracker) + //netA.config.PublicAddress = "auto" + netA.config.GossipFanout = 1 + + netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"}) + netB.identityTracker = newMockIdentityTracker(netB.identityTracker) + netB.config.PublicAddress = "auto" + netB.config.GossipFanout = 1 + + netA.Start() + defer netA.Stop() + netB.Start() + defer netB.Stop() + + addrA, ok := netA.Address() + require.True(t, ok) + gossipA, err := netA.addrToGossipAddr(addrA) + require.NoError(t, err) + + addrB, ok := netB.Address() + require.True(t, ok) + gossipB, err := netB.addrToGossipAddr(addrB) + require.NoError(t, err) + + // set addresses to just host:port to match phonebook/dns format + addrA = hostAndPort(addrA) + addrB = hostAndPort(addrB) + + // first connection should work just fine + if _, ok := netA.tryConnectReserveAddr(addrB); ok { + netA.wg.Add(1) + netA.tryConnect(addrB, gossipB) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + // single A->B connection + assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut))) + + // confirm identity map was not added to for either host + assert.Equal(t, 0, netA.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 0, netB.identityTracker.(*mockIdentityTracker).getSetCount()) + + // bi-directional connection should also work + if _, ok := netB.tryConnectReserveAddr(addrA); ok { + netB.wg.Add(1) + netB.tryConnect(addrA, gossipA) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedOut))) + // confirm identity map was not added to for either host + assert.Equal(t, 0, netA.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 0, netB.identityTracker.(*mockIdentityTracker).getSetCount()) +} + +// TestPeeringIncorrectDeduplicationName confirm that if the reciever can't match +// the Address in the challenge to its PublicAddress, identities aren't exchanged, but peering continues +func TestPeeringIncorrectDeduplicationName(t *testing.T) { + partitiontest.PartitionTest(t) + + netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"}) + netA.identityTracker = newMockIdentityTracker(netA.identityTracker) + netA.config.PublicAddress = "auto" + netA.config.GossipFanout = 1 + + netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"}) + netB.identityTracker = newMockIdentityTracker(netB.identityTracker) + netB.config.PublicAddress = "no:3333" + netB.config.GossipFanout = 1 + + netA.Start() + defer netA.Stop() + netB.Start() + defer netB.Stop() + + addrA, ok := netA.Address() + require.True(t, ok) + gossipA, err := netA.addrToGossipAddr(addrA) + require.NoError(t, err) + + addrB, ok := netB.Address() + require.True(t, ok) + gossipB, err := netB.addrToGossipAddr(addrB) + require.NoError(t, err) + + // set addresses to just host:port to match phonebook/dns format + addrA = hostAndPort(addrA) + addrB = hostAndPort(addrB) + + // first connection should work just fine + if _, ok := netA.tryConnectReserveAddr(addrB); ok { + netA.wg.Add(1) + netA.tryConnect(addrB, gossipB) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + // single A->B connection + assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut))) + + // confirm identity map was not added to for either host + // nor was "set" called at all + assert.Equal(t, 0, netA.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 0, netB.identityTracker.(*mockIdentityTracker).getSetCount()) + + // bi-directional connection should also work + // this second connection should set identities, because the reciever address matches now + if _, ok := netB.tryConnectReserveAddr(addrA); ok { + netB.wg.Add(1) + netB.tryConnect(addrA, gossipA) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + // confirm that at this point the identityTracker was called once per network + // and inserted once per network + assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getSetCount()) + assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getInsertCount()) + assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getInsertCount()) + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedOut))) +} + +// make a mockIdentityScheme which can accept overloaded behavior +// use this over the next few tests to check that when one peer misbehaves, peering continues/halts as expected +type mockIdentityScheme struct { + t *testing.T + realScheme *identityChallengePublicKeyScheme + attachChallenge func(attach http.Header, addr string) identityChallengeValue + verifyAndAttachResponse func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) + verifyResponse func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) +} + +func newMockIdentityScheme(t *testing.T) *mockIdentityScheme { + return &mockIdentityScheme{t: t, realScheme: NewIdentityChallengeScheme("any")} +} +func (i mockIdentityScheme) AttachChallenge(attach http.Header, addr string) identityChallengeValue { + if i.attachChallenge != nil { + return i.attachChallenge(attach, addr) + } + return i.realScheme.AttachChallenge(attach, addr) +} +func (i mockIdentityScheme) VerifyRequestAndAttachResponse(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) { + if i.verifyAndAttachResponse != nil { + return i.verifyAndAttachResponse(attach, h) + } + return i.realScheme.VerifyRequestAndAttachResponse(attach, h) +} +func (i mockIdentityScheme) VerifyResponse(h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) { + if i.verifyResponse != nil { + return i.verifyResponse(i.t, h, c) + } + return i.realScheme.VerifyResponse(h, c) +} + +// when the identity challenge is misconstructed in various ways, peering should behave as expected +func TestPeeringWithBadIdentityChallenge(t *testing.T) { + partitiontest.PartitionTest(t) + + type testCase struct { + name string + attachChallenge func(attach http.Header, addr string) identityChallengeValue + totalInA int + totalOutA int + totalInB int + totalOutB int + } + + testCases := []testCase{ + // when identityChallenge is not included, peering continues as normal + { + name: "not included", + attachChallenge: func(attach http.Header, addr string) identityChallengeValue { return identityChallengeValue{} }, + totalInA: 0, + totalOutA: 1, + totalInB: 1, + totalOutB: 0, + }, + // when the identityChallenge is malformed B64, peering halts + { + name: "malformed b64", + attachChallenge: func(attach http.Header, addr string) identityChallengeValue { + attach.Add(IdentityChallengeHeader, "this does not decode!") + return newIdentityChallengeValue() + }, + totalInA: 0, + totalOutA: 0, + totalInB: 0, + totalOutB: 0, + }, + // when the identityChallenge can't be unmarshalled, peering halts + { + name: "not msgp decodable", + attachChallenge: func(attach http.Header, addr string) identityChallengeValue { + attach.Add(IdentityChallengeHeader, base64.StdEncoding.EncodeToString([]byte("Bad!Data!"))) + return newIdentityChallengeValue() + }, + totalInA: 0, + totalOutA: 0, + totalInB: 0, + totalOutB: 0, + }, + // when the incorrect address is used, peering continues + { + name: "incorrect address", + attachChallenge: func(attach http.Header, addr string) identityChallengeValue { + s := NewIdentityChallengeScheme("does not matter") // make a scheme to use its keys + c := identityChallenge{ + Key: s.identityKeys.SignatureVerifier, + Challenge: newIdentityChallengeValue(), + PublicAddress: []byte("incorrect address!"), + } + attach.Add(IdentityChallengeHeader, c.signAndEncodeB64(s.identityKeys)) + return c.Challenge + }, + totalInA: 0, + totalOutA: 1, + totalInB: 1, + totalOutB: 0, + }, + // when the challenge is incorrectly signed, peering halts + { + name: "bad signature", + attachChallenge: func(attach http.Header, addr string) identityChallengeValue { + s := NewIdentityChallengeScheme("does not matter") // make a scheme to use its keys + c := identityChallenge{ + Key: s.identityKeys.SignatureVerifier, + Challenge: newIdentityChallengeValue(), + PublicAddress: []byte("incorrect address!"), + }.Sign(s.identityKeys) + c.Msg.Challenge = newIdentityChallengeValue() // change the challenge after signing the message, so the signature check fails + enc := protocol.Encode(&c) + b64enc := base64.StdEncoding.EncodeToString(enc) + attach.Add(IdentityChallengeHeader, b64enc) + return c.Msg.Challenge + }, + totalInA: 0, + totalOutA: 0, + totalInB: 0, + totalOutB: 0, + }, + } + + for _, tc := range testCases { + t.Logf("Running Peering with Identity Challenge Test: %s", tc.name) + netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"}) + netA.identityTracker = newMockIdentityTracker(netA.identityTracker) + netA.config.PublicAddress = "auto" + netA.config.GossipFanout = 1 + + scheme := newMockIdentityScheme(t) + scheme.attachChallenge = tc.attachChallenge + netA.identityScheme = scheme + + netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"}) + netB.identityTracker = newMockIdentityTracker(netB.identityTracker) + netB.config.PublicAddress = "auto" + netB.config.GossipFanout = 1 + + netA.Start() + defer netA.Stop() + netB.Start() + defer netB.Stop() + + addrB, ok := netB.Address() + require.True(t, ok) + gossipB, err := netB.addrToGossipAddr(addrB) + require.NoError(t, err) + + // set addresses to just host:port to match phonebook/dns format + addrB = hostAndPort(addrB) + + if _, ok := netA.tryConnectReserveAddr(addrB); ok { + netA.wg.Add(1) + netA.tryConnect(addrB, gossipB) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + assert.Equal(t, tc.totalInA, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, tc.totalOutA, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, tc.totalInB, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, tc.totalOutB, len(netB.GetPeers(PeersConnectedOut))) + } + +} + +// when the identity challenge response is misconstructed in various way, confirm peering behaves as expected +func TestPeeringWithBadIdentityChallengeResponse(t *testing.T) { + partitiontest.PartitionTest(t) + + type testCase struct { + name string + verifyAndAttachResponse func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) + totalInA int + totalOutA int + totalInB int + totalOutB int + } + + testCases := []testCase{ + // when there is no response to the identity challenge, peering should continue without ID + { + name: "not included", + verifyAndAttachResponse: func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) { + return identityChallengeValue{}, crypto.PublicKey{}, nil + }, + totalInA: 0, + totalOutA: 1, + totalInB: 1, + totalOutB: 0, + }, + // when the response is malformed, do not peer + { + name: "malformed b64", + verifyAndAttachResponse: func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) { + attach.Add(IdentityChallengeHeader, "this does not decode!") + return identityChallengeValue{}, crypto.PublicKey{}, nil + }, + totalInA: 0, + totalOutA: 0, + totalInB: 0, + totalOutB: 0, + }, + // when the response is malformed, do not peer + { + name: "not msgp decodable", + verifyAndAttachResponse: func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) { + attach.Add(IdentityChallengeHeader, base64.StdEncoding.EncodeToString([]byte("Bad!Data!"))) + return identityChallengeValue{}, crypto.PublicKey{}, nil + }, + totalInA: 0, + totalOutA: 0, + totalInB: 0, + totalOutB: 0, + }, + // when the original challenge isn't included, do not peer + { + name: "incorrect original challenge", + verifyAndAttachResponse: func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) { + s := NewIdentityChallengeScheme("does not matter") // make a scheme to use its keys + // decode the header to an identityChallenge + msg, _ := base64.StdEncoding.DecodeString(h.Get(IdentityChallengeHeader)) + idChal := identityChallenge{} + protocol.Decode(msg, &idChal) + // make the response object, with an incorrect challenge encode it and attach it to the header + r := identityChallengeResponse{ + Key: s.identityKeys.SignatureVerifier, + Challenge: newIdentityChallengeValue(), + ResponseChallenge: newIdentityChallengeValue(), + } + attach.Add(IdentityChallengeHeader, r.signAndEncodeB64(s.identityKeys)) + return r.ResponseChallenge, idChal.Key, nil + }, + totalInA: 0, + totalOutA: 0, + totalInB: 0, + totalOutB: 0, + }, + // when the message is incorrectly signed, do not peer + { + name: "bad signature", + verifyAndAttachResponse: func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) { + s := NewIdentityChallengeScheme("does not matter") // make a scheme to use its keys + // decode the header to an identityChallenge + msg, _ := base64.StdEncoding.DecodeString(h.Get(IdentityChallengeHeader)) + idChal := identityChallenge{} + protocol.Decode(msg, &idChal) + // make the response object, then change the signature and encode and attach + r := identityChallengeResponse{ + Key: s.identityKeys.SignatureVerifier, + Challenge: newIdentityChallengeValue(), + ResponseChallenge: newIdentityChallengeValue(), + }.Sign(s.identityKeys) + r.Msg.ResponseChallenge = newIdentityChallengeValue() // change the challenge after signing the message + enc := protocol.Encode(&r) + b64enc := base64.StdEncoding.EncodeToString(enc) + attach.Add(IdentityChallengeHeader, b64enc) + return r.Msg.ResponseChallenge, idChal.Key, nil + }, + totalInA: 0, + totalOutA: 0, + totalInB: 0, + totalOutB: 0, + }, + } + + for _, tc := range testCases { + t.Logf("Running Peering with Identity Challenge Response Test: %s", tc.name) + netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"}) + netA.identityTracker = newMockIdentityTracker(netA.identityTracker) + netA.config.PublicAddress = "auto" + netA.config.GossipFanout = 1 + + netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"}) + netB.identityTracker = newMockIdentityTracker(netB.identityTracker) + netB.config.PublicAddress = "auto" + netB.config.GossipFanout = 1 + + scheme := newMockIdentityScheme(t) + scheme.verifyAndAttachResponse = tc.verifyAndAttachResponse + netB.identityScheme = scheme + + netA.Start() + defer netA.Stop() + netB.Start() + defer netB.Stop() + + addrB, ok := netB.Address() + require.True(t, ok) + gossipB, err := netB.addrToGossipAddr(addrB) + require.NoError(t, err) + + // set addresses to just host:port to match phonebook/dns format + addrB = hostAndPort(addrB) + + if _, ok := netA.tryConnectReserveAddr(addrB); ok { + netA.wg.Add(1) + netA.tryConnect(addrB, gossipB) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + assert.Equal(t, tc.totalInA, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, tc.totalOutA, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, tc.totalInB, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, tc.totalOutB, len(netB.GetPeers(PeersConnectedOut))) + } + +} + +// when the identity challenge verification is misconstructed in various ways, peering should behave as expected +func TestPeeringWithBadIdentityVerification(t *testing.T) { + partitiontest.PartitionTest(t) + + type testCase struct { + name string + verifyResponse func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) + totalInA int + totalOutA int + totalInB int + totalOutB int + additionalSleep time.Duration + occupied bool + } + + testCases := []testCase{ + // in a totally unmodified scenario, the two peers stay connected even after the verification timeout + { + name: "happy path", + totalInA: 0, + totalOutA: 1, + totalInB: 1, + totalOutB: 0, + }, + // if the peer does not send a final message, the peers stay connected + { + name: "not included", + verifyResponse: func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) { + return crypto.PublicKey{}, []byte{}, nil + }, + totalInA: 0, + totalOutA: 1, + totalInB: 1, + totalOutB: 0, + }, + // when the identityVerification can't be unmarshalled, peer is disconnected + { + name: "not msgp decodable", + verifyResponse: func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) { + message := append([]byte(protocol.NetIDVerificationTag), []byte("Bad!Data!")[:]...) + return crypto.PublicKey{}, message, nil + }, + totalInA: 0, + totalOutA: 0, + totalInB: 0, + totalOutB: 0, + }, + { + // when the verification signature doesn't match the peer's expectation (the previously exchanged identity), peer is disconnected + name: "bad signature", + verifyResponse: func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) { + headerString := h.Get(IdentityChallengeHeader) + require.NotEmpty(t, headerString) + msg, err := base64.StdEncoding.DecodeString(headerString) + require.NoError(t, err) + resp := identityChallengeResponseSigned{} + err = protocol.Decode(msg, &resp) + require.NoError(t, err) + s := NewIdentityChallengeScheme("does not matter") // make a throwaway key + ver := identityVerificationMessageSigned{ + // fill in correct ResponseChallenge field + Msg: identityVerificationMessage{ResponseChallenge: resp.Msg.ResponseChallenge}, + Signature: s.identityKeys.SignBytes([]byte("bad bytes for signing")), + } + message := append([]byte(protocol.NetIDVerificationTag), protocol.Encode(&ver)[:]...) + return crypto.PublicKey{}, message, nil + }, + totalInA: 0, + totalOutA: 0, + totalInB: 0, + totalOutB: 0, + }, + { + // when the verification signature doesn't match the peer's expectation (the previously exchanged identity), peer is disconnected + name: "bad signature", + verifyResponse: func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) { + s := NewIdentityChallengeScheme("does not matter") // make a throwaway key + ver := identityVerificationMessageSigned{ + // fill in wrong ResponseChallenge field + Msg: identityVerificationMessage{ResponseChallenge: newIdentityChallengeValue()}, + Signature: s.identityKeys.SignBytes([]byte("bad bytes for signing")), + } + message := append([]byte(protocol.NetIDVerificationTag), protocol.Encode(&ver)[:]...) + return crypto.PublicKey{}, message, nil + }, + totalInA: 0, + totalOutA: 0, + totalInB: 0, + totalOutB: 0, + }, + { + // when the identity is already in use, peer is disconnected + name: "identity occupied", + verifyResponse: nil, + totalInA: 0, + totalOutA: 0, + totalInB: 0, + totalOutB: 0, + occupied: true, + }, + } + + for _, tc := range testCases { + t.Logf("Running Peering with Identity Verification Test: %s", tc.name) + netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"}) + netA.identityTracker = newMockIdentityTracker(netA.identityTracker) + netA.config.PublicAddress = "auto" + netA.config.GossipFanout = 1 + + scheme := newMockIdentityScheme(t) + scheme.verifyResponse = tc.verifyResponse + netA.identityScheme = scheme + + netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"}) + netB.identityTracker = newMockIdentityTracker(netB.identityTracker) + netB.config.PublicAddress = "auto" + netB.config.GossipFanout = 1 + // if the key is occupied, make the tracker fail to insert the peer + if tc.occupied { + netB.identityTracker = newMockIdentityTracker(netB.identityTracker) + netB.identityTracker.(*mockIdentityTracker).setIsOccupied(true) + } + + netA.Start() + defer netA.Stop() + netB.Start() + defer netB.Stop() + + addrB, ok := netB.Address() + require.True(t, ok) + gossipB, err := netB.addrToGossipAddr(addrB) + require.NoError(t, err) + + // set addresses to just host:port to match phonebook/dns format + addrB = hostAndPort(addrB) + + if _, ok := netA.tryConnectReserveAddr(addrB); ok { + netA.wg.Add(1) + netA.tryConnect(addrB, gossipB) + // let the tryConnect go forward + time.Sleep(250 * time.Millisecond) + } + + assert.Equal(t, tc.totalInA, len(netA.GetPeers(PeersConnectedIn))) + assert.Equal(t, tc.totalOutA, len(netA.GetPeers(PeersConnectedOut))) + assert.Equal(t, tc.totalInB, len(netB.GetPeers(PeersConnectedIn))) + assert.Equal(t, tc.totalOutB, len(netB.GetPeers(PeersConnectedOut))) + } +} + type benchmarkHandler struct { returns chan uint64 } diff --git a/network/wsPeer.go b/network/wsPeer.go index d769d122b7..accd06cf9e 100644 --- a/network/wsPeer.go +++ b/network/wsPeer.go @@ -105,17 +105,18 @@ var unknownProtocolTagMessagesTotal = metrics.MakeCounter(metrics.UnknownProtoco // defaultSendMessageTags is the default list of messages which a peer would // allow to be sent without receiving any explicit request. var defaultSendMessageTags = map[protocol.Tag]bool{ - protocol.AgreementVoteTag: true, - protocol.MsgDigestSkipTag: true, - protocol.NetPrioResponseTag: true, - protocol.PingTag: true, - protocol.PingReplyTag: true, - protocol.ProposalPayloadTag: true, - protocol.TopicMsgRespTag: true, - protocol.MsgOfInterestTag: true, - protocol.TxnTag: true, - protocol.UniEnsBlockReqTag: true, - protocol.VoteBundleTag: true, + protocol.AgreementVoteTag: true, + protocol.MsgDigestSkipTag: true, + protocol.NetPrioResponseTag: true, + protocol.NetIDVerificationTag: true, + protocol.PingTag: true, + protocol.PingReplyTag: true, + protocol.ProposalPayloadTag: true, + protocol.TopicMsgRespTag: true, + protocol.MsgOfInterestTag: true, + protocol.TxnTag: true, + protocol.UniEnsBlockReqTag: true, + protocol.VoteBundleTag: true, } // interface allows substituting debug implementation for *websocket.Conn @@ -165,6 +166,8 @@ const disconnectLeastPerformingPeer disconnectReason = "LeastPerformingPeer" const disconnectCliqueResolve disconnectReason = "CliqueResolving" const disconnectRequestReceived disconnectReason = "DisconnectRequest" const disconnectStaleWrite disconnectReason = "DisconnectStaleWrite" +const disconnectDuplicateConnection disconnectReason = "DuplicateConnection" +const disconnectBadIdentityData disconnectReason = "BadIdentityData" // Response is the structure holding the response from the server type Response struct { @@ -233,6 +236,12 @@ type wsPeer struct { // is present in wn.peers. peerIndex int + // the peer's identity key which it uses for identityChallenge exchanges + identity crypto.PublicKey + identityVerified uint32 + // the identityChallenge is recorded to the peer so it may verify its identity at a later time + identityChallenge identityChallengeValue + // Challenge sent to the peer on an incoming connection prioChallenge string @@ -336,8 +345,7 @@ func (wp *wsPeer) Version() string { return wp.version } -// Unicast sends the given bytes to this specific peer. Does not wait for message to be sent. -// +// Unicast sends the given bytes to this specific peer. Does not wait for message to be sent. // (Implements UnicastPeer) func (wp *wsPeer) Unicast(ctx context.Context, msg []byte, tag protocol.Tag) error { var err error @@ -571,7 +579,7 @@ func (wp *wsPeer) readLoop() { atomic.AddUint64(&wp.ppMessageCount, 1) // the remaining valid tags: no special handling here case protocol.NetPrioResponseTag, protocol.PingTag, protocol.PingReplyTag, - protocol.StateProofSigTag, protocol.UniEnsBlockReqTag, protocol.VoteBundleTag: + protocol.StateProofSigTag, protocol.UniEnsBlockReqTag, protocol.VoteBundleTag, protocol.NetIDVerificationTag: default: // unrecognized tag unknownProtocolTagMessagesTotal.Inc(nil) atomic.AddUint64(&wp.unkMessageCount, 1) @@ -1013,6 +1021,7 @@ func (wp *wsPeer) OnClose(f func()) { wp.closers = append(wp.closers, f) } +//msgp:ignore peerFeatureFlag type peerFeatureFlag int const pfCompressedProposal peerFeatureFlag = 1 diff --git a/protocol/hash.go b/protocol/hash.go index 079333a438..26975a402f 100644 --- a/protocol/hash.go +++ b/protocol/hash.go @@ -48,6 +48,9 @@ const ( MerkleArrayNode HashID = "MA" MerkleVectorCommitmentBottomLeaf HashID = "MB" Message HashID = "MX" + NetIdentityChallenge HashID = "NIC" + NetIdentityChallengeResponse HashID = "NIR" + NetIdentityVerificationMessage HashID = "NIV" NetPrioResponse HashID = "NPR" OneTimeSigKey1 HashID = "OT1" OneTimeSigKey2 HashID = "OT2" diff --git a/protocol/tags.go b/protocol/tags.go index 876a8c868c..ef44e74acf 100644 --- a/protocol/tags.go +++ b/protocol/tags.go @@ -25,16 +25,17 @@ type Tag string // are encoded using a comma separator (see network/msgOfInterest.go). // The tags must be 2 bytes long. const ( - AgreementVoteTag Tag = "AV" - MsgOfInterestTag Tag = "MI" - MsgDigestSkipTag Tag = "MS" - NetPrioResponseTag Tag = "NP" - PingTag Tag = "pi" - PingReplyTag Tag = "pj" - ProposalPayloadTag Tag = "PP" - StateProofSigTag Tag = "SP" - TopicMsgRespTag Tag = "TS" - TxnTag Tag = "TX" + AgreementVoteTag Tag = "AV" + MsgOfInterestTag Tag = "MI" + MsgDigestSkipTag Tag = "MS" + NetPrioResponseTag Tag = "NP" + NetIDVerificationTag Tag = "NI" + PingTag Tag = "pi" + PingReplyTag Tag = "pj" + ProposalPayloadTag Tag = "PP" + StateProofSigTag Tag = "SP" + TopicMsgRespTag Tag = "TS" + TxnTag Tag = "TX" //UniCatchupReqTag Tag = "UC" was replaced by UniEnsBlockReqTag UniEnsBlockReqTag Tag = "UE" //UniEnsBlockResTag Tag = "US" was used for wsfetcherservice @@ -47,6 +48,7 @@ var TagList = []Tag{ AgreementVoteTag, MsgOfInterestTag, MsgDigestSkipTag, + NetIDVerificationTag, NetPrioResponseTag, PingTag, PingReplyTag,