storagenode/contact: send noise key and settings as contact info

Change-Id: I1e7a83de36d5cf16eed8874091b15af1e0b73df7
This commit is contained in:
JT Olio 2022-12-21 17:19:05 -05:00 committed by Storj Robot
parent b6bcb32ecf
commit 382af95499
5 changed files with 243 additions and 10 deletions

View File

@ -4,9 +4,16 @@
package server
import (
"context"
"crypto/subtle"
"encoding/binary"
"time"
"github.com/flynn/noise"
"storj.io/common/identity"
"storj.io/common/pb"
"storj.io/common/signing"
)
// NoiseHeader is the drpcmigrate.Header prefix for DRPC over Noise.
@ -26,3 +33,99 @@ func defaultNoiseConfig() noise.Config {
Pattern: noise.HandshakeIK,
}
}
func signableNoisePublicKey(ts time.Time, key []byte) []byte {
var buf [8]byte
tsnano := ts.UnixNano()
if tsnano < 0 {
tsnano = 0
}
binary.BigEndian.PutUint64(buf[:], uint64(tsnano))
return append(buf[:], key...)
}
// GenerateNoiseKeyAttestation will sign a given Noise public key using the
// Node's leaf key and certificate chain, generating a pb.NoiseKeyAttestation.
func GenerateNoiseKeyAttestation(ctx context.Context, ident *identity.FullIdentity, info *pb.NoiseInfo) (_ *pb.NoiseKeyAttestation, err error) {
defer mon.Task()(&ctx)(&err)
ts := time.Now()
signature, err := signing.SignerFromFullIdentity(ident).HashAndSign(ctx,
append([]byte("noise-key-attestation-v1:"), signableNoisePublicKey(ts, info.PublicKey)...))
if err != nil {
return nil, Error.Wrap(err)
}
return &pb.NoiseKeyAttestation{
NodeId: ident.ID,
NodeCertchain: identity.EncodePeerIdentity(ident.PeerIdentity()),
NoiseProto: info.Proto,
NoisePublicKey: info.PublicKey,
Timestamp: ts,
Signature: signature,
}, nil
}
// ValidateNoiseKeyAttestation will confirm that a provided
// *pb.NoiseKeyAttestation was signed correctly.
func ValidateNoiseKeyAttestation(ctx context.Context, attestation *pb.NoiseKeyAttestation) (err error) {
defer mon.Task()(&ctx)(&err)
peer, err := identity.DecodePeerIdentity(ctx, attestation.NodeCertchain)
if err != nil {
return Error.Wrap(err)
}
err = peer.Leaf.CheckSignatureFrom(peer.CA)
if err != nil {
return Error.New("certificate chain invalid: %w", err)
}
if subtle.ConstantTimeCompare(peer.ID.Bytes(), attestation.NodeId.Bytes()) != 1 {
return Error.New("node id mismatch")
}
signee := signing.SigneeFromPeerIdentity(peer)
unsigned := signableNoisePublicKey(attestation.Timestamp, attestation.NoisePublicKey)
err = signee.HashAndVerifySignature(ctx,
append([]byte("noise-key-attestation-v1:"), unsigned...),
attestation.Signature)
return Error.Wrap(err)
}
// GenerateNoiseSessionAttestation will sign a given Noise session handshake
// hash using the Node's leaf key and certificate chain, generating a
// pb.NoiseSessionAttestation.
func GenerateNoiseSessionAttestation(ctx context.Context, ident *identity.FullIdentity, handshakeHash []byte) (_ *pb.NoiseSessionAttestation, err error) {
defer mon.Task()(&ctx)(&err)
signature, err := signing.SignerFromFullIdentity(ident).HashAndSign(ctx,
append([]byte("noise-session-attestation-v1:"), handshakeHash...))
if err != nil {
return nil, Error.Wrap(err)
}
return &pb.NoiseSessionAttestation{
NodeId: ident.ID,
NodeCertchain: identity.EncodePeerIdentity(ident.PeerIdentity()),
NoiseHandshakeHash: handshakeHash,
Signature: signature,
}, nil
}
// ValidateNoiseSessionAttestation will confirm that a provided
// *pb.NoiseSessionAttestation was signed correctly.
func ValidateNoiseSessionAttestation(ctx context.Context, attestation *pb.NoiseSessionAttestation) (err error) {
defer mon.Task()(&ctx)(&err)
peer, err := identity.DecodePeerIdentity(ctx, attestation.NodeCertchain)
if err != nil {
return Error.Wrap(err)
}
err = peer.Leaf.CheckSignatureFrom(peer.CA)
if err != nil {
return Error.New("certificate chain invalid: %w", err)
}
if subtle.ConstantTimeCompare(peer.ID.Bytes(), attestation.NodeId.Bytes()) != 1 {
return Error.New("node id mismatch")
}
signee := signing.SigneeFromPeerIdentity(peer)
err = signee.HashAndVerifySignature(ctx,
append([]byte("noise-session-attestation-v1:"), attestation.NoiseHandshakeHash...),
attestation.Signature)
return Error.Wrap(err)
}

View File

@ -0,0 +1,117 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
package server
import (
"crypto/rand"
"testing"
"time"
"github.com/stretchr/testify/require"
"storj.io/common/identity"
"storj.io/common/pb"
"storj.io/common/storj"
"storj.io/common/testcontext"
)
func TestNoiseKeyAttestation(t *testing.T) {
ctx := testcontext.New(t)
ident, err := identity.NewFullIdentity(ctx, identity.NewCAOptions{})
require.NoError(t, err)
ident2, err := identity.NewFullIdentity(ctx, identity.NewCAOptions{})
require.NoError(t, err)
ident3, err := identity.NewFullIdentity(ctx, identity.NewCAOptions{})
require.NoError(t, err)
noiseCfg, err := generateNoiseConf(ident)
require.NoError(t, err)
attestation, err := GenerateNoiseKeyAttestation(ctx, ident, &pb.NoiseInfo{
Proto: defaultNoiseProto,
PublicKey: noiseCfg.StaticKeypair.Public,
})
require.NoError(t, err)
require.NoError(t, ValidateNoiseKeyAttestation(ctx, attestation))
badAttestation1 := *attestation
badAttestation1.NodeId, err = storj.NodeIDFromString("121RTSDpyNZVcEU84Ticf2L1ntiuUimbWgfATz21tuvgk3vzoA6")
require.NoError(t, err)
err = ValidateNoiseKeyAttestation(ctx, &badAttestation1)
require.Error(t, err)
require.Contains(t, err.Error(), "node id mismatch")
badAttestation2 := *attestation
badAttestation2.NoisePublicKey = badAttestation2.NoisePublicKey[:len(badAttestation2.NoisePublicKey)-1]
err = ValidateNoiseKeyAttestation(ctx, &badAttestation2)
require.Error(t, err)
require.Contains(t, err.Error(), "signature is not valid")
badAttestation3 := *attestation
badAttestation3.Timestamp = time.Now()
err = ValidateNoiseKeyAttestation(ctx, &badAttestation3)
require.Error(t, err)
require.Contains(t, err.Error(), "signature is not valid")
ident2.CA = ident.CA
badAttestation4 := *attestation
badAttestation4.NodeCertchain = identity.EncodePeerIdentity(ident2.PeerIdentity())
err = ValidateNoiseKeyAttestation(ctx, &badAttestation4)
require.Error(t, err)
require.Contains(t, err.Error(), "certificate chain invalid")
ident3.Leaf = ident.Leaf
badAttestation5 := *attestation
badAttestation5.NodeCertchain = identity.EncodePeerIdentity(ident3.PeerIdentity())
err = ValidateNoiseKeyAttestation(ctx, &badAttestation5)
require.Error(t, err)
require.Contains(t, err.Error(), "certificate chain invalid")
}
func TestNoiseSessionAttestation(t *testing.T) {
ctx := testcontext.New(t)
ident, err := identity.NewFullIdentity(ctx, identity.NewCAOptions{})
require.NoError(t, err)
ident2, err := identity.NewFullIdentity(ctx, identity.NewCAOptions{})
require.NoError(t, err)
ident3, err := identity.NewFullIdentity(ctx, identity.NewCAOptions{})
require.NoError(t, err)
var hash [32]byte
_, err = rand.Read(hash[:])
require.NoError(t, err)
attestation, err := GenerateNoiseSessionAttestation(ctx, ident, hash[:])
require.NoError(t, err)
require.NoError(t, ValidateNoiseSessionAttestation(ctx, attestation))
badAttestation1 := *attestation
badAttestation1.NodeId, err = storj.NodeIDFromString("121RTSDpyNZVcEU84Ticf2L1ntiuUimbWgfATz21tuvgk3vzoA6")
require.NoError(t, err)
err = ValidateNoiseSessionAttestation(ctx, &badAttestation1)
require.Error(t, err)
require.Contains(t, err.Error(), "node id mismatch")
badAttestation2 := *attestation
badAttestation2.NoiseHandshakeHash = badAttestation2.NoiseHandshakeHash[:len(badAttestation2.NoiseHandshakeHash)-1]
err = ValidateNoiseSessionAttestation(ctx, &badAttestation2)
require.Error(t, err)
require.Contains(t, err.Error(), "signature is not valid")
ident2.CA = ident.CA
badAttestation3 := *attestation
badAttestation3.NodeCertchain = identity.EncodePeerIdentity(ident2.PeerIdentity())
err = ValidateNoiseSessionAttestation(ctx, &badAttestation3)
require.Error(t, err)
require.Contains(t, err.Error(), "certificate chain invalid")
ident3.Leaf = ident.Leaf
badAttestation4 := *attestation
badAttestation4.NodeCertchain = identity.EncodePeerIdentity(ident3.PeerIdentity())
err = ValidateNoiseSessionAttestation(ctx, &badAttestation4)
require.Error(t, err)
require.Contains(t, err.Error(), "certificate chain invalid")
}

View File

@ -231,6 +231,12 @@ func (p *Server) NoiseInfo() *pb.NoiseInfo {
}
}
// NoiseKeyAttestation returns the noise key attestation for this server.
func (p *Server) NoiseKeyAttestation(ctx context.Context) (_ *pb.NoiseKeyAttestation, err error) {
defer mon.Task()(&ctx)(&err)
return GenerateNoiseKeyAttestation(ctx, p.tlsOptions.Ident, p.NoiseInfo())
}
// Close shuts down the server.
func (p *Server) Close() error {
p.mu.Lock()

View File

@ -47,6 +47,7 @@ type NodeInfo struct {
Version pb.NodeVersion
Capacity pb.NodeCapacity
Operator pb.NodeOperator
NoiseKeyAttestation *pb.NoiseKeyAttestation
}
// Service is the contact service between storage nodes and satellites.
@ -133,6 +134,7 @@ func (service *Service) pingSatelliteOnce(ctx context.Context, id storj.NodeID)
Version: &self.Version,
Capacity: &self.Capacity,
Operator: &self.Operator,
NoiseKeyAttestation: self.NoiseKeyAttestation,
})
service.quicStats.SetStatus(false)
if err != nil {

View File

@ -420,6 +420,10 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB, revocationDB exten
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
noiseKeyAttestation, err := peer.Server.NoiseKeyAttestation(context.Background())
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
self := contact.NodeInfo{
ID: peer.ID(),
Address: c.ExternalAddress,
@ -429,6 +433,7 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB, revocationDB exten
WalletFeatures: config.Operator.WalletFeatures,
},
Version: *pbVersion,
NoiseKeyAttestation: noiseKeyAttestation,
}
peer.Contact.PingStats = new(contact.PingStats)
peer.Contact.QUICStats = contact.NewQUICStats(peer.Server.IsQUICEnabled())