control/noise: use key.Machine{Public,Private} as appropriate.

Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
David Anderson 2021-10-25 16:41:30 -07:00 committed by Dave Anderson
parent edb33d65c3
commit 293431aaea
8 changed files with 126 additions and 119 deletions

View File

@ -42,7 +42,7 @@
type Conn struct {
conn net.Conn
version uint16
peer key.Public
peer key.MachinePublic
handshakeHash [blake2s.Size]byte
rx rxState
tx txState
@ -83,7 +83,7 @@ func (c *Conn) HandshakeHash() [blake2s.Size]byte {
}
// Peer returns the peer's long-term public key.
func (c *Conn) Peer() key.Public {
func (c *Conn) Peer() key.MachinePublic {
return c.peer
}

View File

@ -197,8 +197,8 @@ func TestConnStd(t *testing.T) {
t.Skip("not all tests can pass on this Conn, see https://github.com/golang/go/issues/46977")
nettest.TestConn(t, func() (c1 net.Conn, c2 net.Conn, stop func(), err error) {
s1, s2 := tsnettest.NewConn("noise", 4096)
controlKey := key.NewPrivate()
machineKey := key.NewPrivate()
controlKey := key.NewMachine()
machineKey := key.NewMachine()
serverErr := make(chan error, 1)
go func() {
var err error
@ -312,8 +312,8 @@ func (s *readSink) Total() int {
func pairWithConns(t *testing.T, clientConn, serverConn net.Conn) (*Conn, *Conn) {
var (
controlKey = key.NewPrivate()
machineKey = key.NewPrivate()
controlKey = key.NewMachine()
machineKey = key.NewMachine()
server *Conn
serverErr = make(chan error, 1)
)

View File

@ -14,6 +14,7 @@
"strconv"
"time"
"go4.org/mem"
"golang.org/x/crypto/blake2s"
chp "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
@ -23,22 +24,21 @@
)
const (
// protocolName is the name of the specific instantiation of the
// Noise protocol we're using. Each field is defined in the Noise
// spec, and shouldn't be changed unless we're switching to a
// different Noise protocol instance.
// protocolName is the name of the specific instantiation of Noise
// that the control protocol uses. This string's value is fixed by
// the Noise spec, and shouldn't be changed unless we're updating
// the control protocol to use a different Noise instance.
protocolName = "Noise_IK_25519_ChaChaPoly_BLAKE2s"
// protocolVersion is the version of the Tailscale base
// protocol that Client will use when initiating a handshake.
// protocolVersion is the version of the control protocol that
// Client will use when initiating a handshake.
protocolVersion uint16 = 1
// protocolVersionPrefix is the name portion of the protocol
// name+version string that gets mixed into the Noise handshake as
// a prologue.
// name+version string that gets mixed into the handshake as a
// prologue.
//
// This mixing verifies that both clients agree that
// they're executing the Tailscale control protocol at a specific
// version that matches the advertised version in the cleartext
// packet header.
// This mixing verifies that both clients agree that they're
// executing the control protocol at a specific version that
// matches the advertised version in the cleartext packet header.
protocolVersionPrefix = "Tailscale Control Protocol v"
invalidNonce = ^uint64(0)
)
@ -49,12 +49,12 @@ func protocolVersionPrologue(version uint16) []byte {
return strconv.AppendUint(ret, uint64(version), 10)
}
// Client initiates a Noise client handshake, returning the resulting
// Noise connection.
// Client initiates a control client handshake, returning the resulting
// control connection.
//
// The context deadline, if any, covers the entire handshaking
// process. Any preexisting Conn deadline is removed.
func Client(ctx context.Context, conn net.Conn, machineKey key.Private, controlKey key.Public) (*Conn, error) {
func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, controlKey key.MachinePublic) (*Conn, error) {
if deadline, ok := ctx.Deadline(); ok {
if err := conn.SetDeadline(deadline); err != nil {
return nil, fmt.Errorf("setting conn deadline: %w", err)
@ -72,20 +72,20 @@ func Client(ctx context.Context, conn net.Conn, machineKey key.Private, controlK
// <- s
// ...
s.MixHash(controlKey[:])
s.MixHash(controlKey.UntypedBytes())
// -> e, es, s, ss
init := mkInitiationMessage()
machineEphemeral := key.NewPrivate()
machineEphemeral := key.NewMachine()
machineEphemeralPub := machineEphemeral.Public()
copy(init.EphemeralPub(), machineEphemeralPub[:])
s.MixHash(machineEphemeralPub[:])
copy(init.EphemeralPub(), machineEphemeralPub.UntypedBytes())
s.MixHash(machineEphemeralPub.UntypedBytes())
cipher, err := s.MixDH(machineEphemeral, controlKey)
if err != nil {
return nil, fmt.Errorf("computing es: %w", err)
}
machineKeyPub := machineKey.Public()
s.EncryptAndHash(cipher, init.MachinePub(), machineKeyPub[:])
s.EncryptAndHash(cipher, init.MachinePub(), machineKeyPub.UntypedBytes())
cipher, err = s.MixDH(machineKey, controlKey)
if err != nil {
return nil, fmt.Errorf("computing ss: %w", err)
@ -122,9 +122,8 @@ func Client(ctx context.Context, conn net.Conn, machineKey key.Private, controlK
}
// <- e, ee, se
var controlEphemeralPub key.Public
copy(controlEphemeralPub[:], resp.EphemeralPub())
s.MixHash(controlEphemeralPub[:])
controlEphemeralPub := key.MachinePublicFromRaw32(mem.B(resp.EphemeralPub()))
s.MixHash(controlEphemeralPub.UntypedBytes())
if _, err = s.MixDH(machineEphemeral, controlEphemeralPub); err != nil {
return nil, fmt.Errorf("computing ee: %w", err)
}
@ -156,12 +155,12 @@ func Client(ctx context.Context, conn net.Conn, machineKey key.Private, controlK
return c, nil
}
// Server initiates a Noise server handshake, returning the resulting
// Noise connection.
// Server initiates a control server handshake, returning the resulting
// control connection.
//
// The context deadline, if any, covers the entire handshaking
// process.
func Server(ctx context.Context, conn net.Conn, controlKey key.Private) (*Conn, error) {
func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate) (*Conn, error) {
if deadline, ok := ctx.Deadline(); ok {
if err := conn.SetDeadline(deadline); err != nil {
return nil, fmt.Errorf("setting conn deadline: %w", err)
@ -215,20 +214,20 @@ func Server(ctx context.Context, conn net.Conn, controlKey key.Private) (*Conn,
// <- s
// ...
controlKeyPub := controlKey.Public()
s.MixHash(controlKeyPub[:])
s.MixHash(controlKeyPub.UntypedBytes())
// -> e, es, s, ss
var machineEphemeralPub key.Public
copy(machineEphemeralPub[:], init.EphemeralPub())
s.MixHash(machineEphemeralPub[:])
machineEphemeralPub := key.MachinePublicFromRaw32(mem.B(init.EphemeralPub()))
s.MixHash(machineEphemeralPub.UntypedBytes())
cipher, err := s.MixDH(controlKey, machineEphemeralPub)
if err != nil {
return nil, fmt.Errorf("computing es: %w", err)
}
var machineKey key.Public
if err := s.DecryptAndHash(cipher, machineKey[:], init.MachinePub()); err != nil {
var machineKeyBytes [32]byte
if err := s.DecryptAndHash(cipher, machineKeyBytes[:], init.MachinePub()); err != nil {
return nil, fmt.Errorf("decrypting machine key: %w", err)
}
machineKey := key.MachinePublicFromRaw32(mem.B(machineKeyBytes[:]))
cipher, err = s.MixDH(controlKey, machineKey)
if err != nil {
return nil, fmt.Errorf("computing ss: %w", err)
@ -239,10 +238,10 @@ func Server(ctx context.Context, conn net.Conn, controlKey key.Private) (*Conn,
// <- e, ee, se
resp := mkResponseMessage()
controlEphemeral := key.NewPrivate()
controlEphemeral := key.NewMachine()
controlEphemeralPub := controlEphemeral.Public()
copy(resp.EphemeralPub(), controlEphemeralPub[:])
s.MixHash(controlEphemeralPub[:])
copy(resp.EphemeralPub(), controlEphemeralPub.UntypedBytes())
s.MixHash(controlEphemeralPub.UntypedBytes())
if _, err := s.MixDH(controlEphemeral, machineEphemeralPub); err != nil {
return nil, fmt.Errorf("computing ee: %w", err)
}
@ -276,14 +275,12 @@ func Server(ctx context.Context, conn net.Conn, controlKey key.Private) (*Conn,
return c, nil
}
// symmetricState is the SymmetricState object from the Noise protocol
// spec. It contains all the symmetric cipher state of an in-flight
// handshake. Field names match the variable names in the spec.
// symmetricState contains the state of an in-flight handshake.
type symmetricState struct {
finished bool
h [blake2s.Size]byte
ck [blake2s.Size]byte
h [blake2s.Size]byte // hash of currently-processed handshake state
ck [blake2s.Size]byte // chaining key used to construct session keys at the end of the handshake
}
func (s *symmetricState) checkFinished() {
@ -319,9 +316,9 @@ func (s *symmetricState) MixHash(data []byte) {
// reduce the risk of error in the caller (e.g. invoking X25519 with
// two private keys, or two public keys), and thus producing the wrong
// calculation.
func (s *symmetricState) MixDH(priv key.Private, pub key.Public) (*singleUseCHP, error) {
func (s *symmetricState) MixDH(priv key.MachinePrivate, pub key.MachinePublic) (*singleUseCHP, error) {
s.checkFinished()
keyData, err := curve25519.X25519(priv[:], pub[:])
keyData, err := curve25519.X25519(priv.UntypedBytes(), pub.UntypedBytes())
if err != nil {
return nil, fmt.Errorf("computing X25519: %w", err)
}

View File

@ -19,8 +19,8 @@
func TestHandshake(t *testing.T) {
var (
clientConn, serverConn = tsnettest.NewConn("noise", 128000)
serverKey = key.NewPrivate()
clientKey = key.NewPrivate()
serverKey = key.NewMachine()
clientKey = key.NewMachine()
server *Conn
serverErr = make(chan error, 1)
)
@ -71,8 +71,8 @@ func TestNoReuse(t *testing.T) {
clientBuf, serverBuf bytes.Buffer
clientConn = &readerConn{clientRaw, io.TeeReader(clientRaw, &clientBuf)}
serverConn = &readerConn{serverRaw, io.TeeReader(serverRaw, &serverBuf)}
serverKey = key.NewPrivate()
clientKey = key.NewPrivate()
serverKey = key.NewMachine()
clientKey = key.NewMachine()
server *Conn
serverErr = make(chan error, 1)
)
@ -164,8 +164,8 @@ func TestTampering(t *testing.T) {
var (
clientConn, serverRaw = tsnettest.NewConn("noise", 128000)
serverConn = &readerConn{serverRaw, &tamperReader{serverRaw, i, 0}}
serverKey = key.NewPrivate()
clientKey = key.NewPrivate()
serverKey = key.NewMachine()
clientKey = key.NewMachine()
serverErr = make(chan error, 1)
)
go func() {
@ -192,8 +192,8 @@ func TestTampering(t *testing.T) {
var (
clientRaw, serverConn = tsnettest.NewConn("noise", 128000)
clientConn = &readerConn{clientRaw, &tamperReader{clientRaw, i, 0}}
serverKey = key.NewPrivate()
clientKey = key.NewPrivate()
serverKey = key.NewMachine()
clientKey = key.NewMachine()
serverErr = make(chan error, 1)
)
go func() {
@ -217,8 +217,8 @@ func TestTampering(t *testing.T) {
var (
clientRaw, serverConn = tsnettest.NewConn("noise", 128000)
clientConn = &readerConn{clientRaw, &tamperReader{clientRaw, 53 + i, 0}}
serverKey = key.NewPrivate()
clientKey = key.NewPrivate()
serverKey = key.NewMachine()
clientKey = key.NewMachine()
serverErr = make(chan error, 1)
)
go func() {
@ -258,8 +258,8 @@ func TestTampering(t *testing.T) {
var (
clientConn, serverRaw = tsnettest.NewConn("noise", 128000)
serverConn = &readerConn{serverRaw, &tamperReader{serverRaw, 101 + i, 0}}
serverKey = key.NewPrivate()
clientKey = key.NewPrivate()
serverKey = key.NewMachine()
clientKey = key.NewMachine()
serverErr = make(chan error, 1)
)
go func() {

View File

@ -20,8 +20,8 @@
func TestInteropClient(t *testing.T) {
var (
s1, s2 = tsnettest.NewConn("noise", 128000)
controlKey = key.NewPrivate()
machineKey = key.NewPrivate()
controlKey = key.NewMachine()
machineKey = key.NewMachine()
serverErr = make(chan error, 2)
serverBytes = make(chan []byte, 1)
c2s = "client>server"
@ -68,8 +68,8 @@ func TestInteropClient(t *testing.T) {
func TestInteropServer(t *testing.T) {
var (
s1, s2 = tsnettest.NewConn("noise", 128000)
controlKey = key.NewPrivate()
machineKey = key.NewPrivate()
controlKey = key.NewMachine()
machineKey = key.NewMachine()
clientErr = make(chan error, 2)
clientBytes = make(chan []byte, 1)
c2s = "client>server"
@ -115,12 +115,13 @@ func TestInteropServer(t *testing.T) {
// noiseExplorerClient uses the Noise Explorer implementation of Noise
// IK to handshake as a Noise client on conn, transmit payload, and
// read+return a payload from the peer.
func noiseExplorerClient(conn net.Conn, controlKey key.Public, machineKey key.Private, payload []byte) ([]byte, error) {
mk := keypair{
private_key: machineKey,
public_key: machineKey.Public(),
}
session := InitSession(true, protocolVersionPrologue(protocolVersion), mk, controlKey)
func noiseExplorerClient(conn net.Conn, controlKey key.MachinePublic, machineKey key.MachinePrivate, payload []byte) ([]byte, error) {
var mk keypair
copy(mk.private_key[:], machineKey.UntypedBytes())
copy(mk.public_key[:], machineKey.Public().UntypedBytes())
var peerKey [32]byte
copy(peerKey[:], controlKey.UntypedBytes())
session := InitSession(true, protocolVersionPrologue(protocolVersion), mk, peerKey)
_, msg1 := SendMessage(&session, nil)
var hdr [headerLen]byte
@ -185,11 +186,10 @@ func noiseExplorerClient(conn net.Conn, controlKey key.Public, machineKey key.Pr
return p, nil
}
func noiseExplorerServer(conn net.Conn, controlKey key.Private, wantMachineKey key.Public, payload []byte) ([]byte, error) {
mk := keypair{
private_key: controlKey,
public_key: controlKey.Public(),
}
func noiseExplorerServer(conn net.Conn, controlKey key.MachinePrivate, wantMachineKey key.MachinePublic, payload []byte) ([]byte, error) {
var mk keypair
copy(mk.private_key[:], controlKey.UntypedBytes())
copy(mk.public_key[:], controlKey.Public().UntypedBytes())
session := InitSession(false, protocolVersionPrologue(protocolVersion), mk, [32]byte{})
var buf [1024]byte

View File

@ -1,26 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package noise
// Note that these types are deliberately separate from the types/key
// package. That package defines generic curve25519 keys, without
// consideration for how those keys are used. We don't want to
// encourage mixing machine keys, node keys, and whatever else we
// might use curve25519 for.
//
// Furthermore, the implementation in types/key does some work that is
// unnecessary for machine keys, and results in a harder to follow
// implementation. In particular, machine keys do not need to be
// clamped per the curve25519 spec because they're only used with the
// X25519 operation, and the X25519 operation defines its own clamping
// and sanity checking logic. Thus, these keys must be used only with
// this Noise protocol implementation, and the easiest way to ensure
// that is a different type.
// PrivateKey is a Tailscale machine private key.
type PrivateKey [32]byte
// PublicKey is a Tailscale machine public key.
type PublicKey [32]byte

View File

@ -6,12 +6,12 @@
import "encoding/binary"
// The transport protocol is mostly Noise messages encapsulated in a
// small header describing the payload's type and length. The one
// place we deviate from pure Noise+header is that we also support
// sending an unauthenticated plaintext error as payload, to provide
// an explanation for a connection error that happens before the
// handshake completes.
// The control protocol wire format is mostly Noise messages
// encapsulated in a small header describing the payload's type and
// length. The one place we deviate from pure Noise+header is that we
// also support sending an unauthenticated plaintext error as payload,
// to provide an explanation for a connection error that happens
// before the handshake completes.
//
// All frames in our protocol have a 5-byte header:
//
@ -31,9 +31,10 @@
// version numbers. At minimum, the version number must change
// whenever any particulars of the Noise handshake change
// (e.g. switching from Noise IK to Noise IKpsk1 or Noise XX), and
// when security-critical aspects of the "uppper" protocol within the
// Noise frames change (e.g. how further authentication data is bound
// to the underlying Noise session).
// when security-critical aspects of the "uppper" protocol (the one
// running inside the established base protocol session) change
// (e.g. how further authentication data is bound to the underlying
// session).
// headerLen is the size of the header that gets prepended to Noise
// messages.
@ -51,7 +52,7 @@
// hints only. They are not encrypted or authenticated, and so can
// be seen and tampered with on the wire.
msgTypeError = 3
// msgTypeRecord frames carry a Noise transport message (i.e. "user data").
// msgTypeRecord frames carry session data bytes.
msgTypeRecord = 4
)
@ -64,10 +65,8 @@ func hdrVersion(bs []byte) uint16 { return binary.LittleEndian.Uint16(bs[:2]) }
func hdrType(bs []byte) byte { return bs[2] }
func hdrLen(bs []byte) int { return int(binary.LittleEndian.Uint16(bs[3:5])) }
// initiationMessage is the Noise protocol message sent from a client
// machine to a control server. Aside from the message header, the
// values are as specified in the Noise specification for the IK
// handshake pattern.
// initiationMessage is the protocol message sent from a client
// machine to a control server.
//
// 5b: header (see headerLen for fields)
// 32b: client ephemeral public key (cleartext)
@ -92,10 +91,8 @@ func (m *initiationMessage) EphemeralPub() []byte { return m[headerLen : headerL
func (m *initiationMessage) MachinePub() []byte { return m[headerLen+32 : headerLen+32+48] }
func (m *initiationMessage) Tag() []byte { return m[headerLen+32+48:] }
// responseMessage is the Noise protocol message sent from a control
// server to a client machine. Aside from the message header, the
// values are as specified in the Noise specification for the IK
// handshake pattern.
// responseMessage is the protocol message sent from a control server
// to a client machine.
//
// 5b: header (see headerLen for fields)
// 32b: control ephemeral public key (cleartext)

View File

@ -77,6 +77,19 @@ func (k *MachinePrivate) UnmarshalText(b []byte) error {
return parseHex(k.k[:], mem.B(b), mem.S(machinePrivateHexPrefix))
}
// UntypedBytes returns k, encoded as an untyped 64-character hex
// string.
//
// Deprecated: this function is risky to use, because it produces
// serialized values that do not identify themselves as a
// MachinePrivate, allowing other code to potentially parse it back in
// as the wrong key type. For new uses that don't require this
// specific raw byte serialization, please use
// MarshalText/UnmarshalText.
func (k MachinePrivate) UntypedBytes() []byte {
return append([]byte(nil), k.k[:]...)
}
// SealTo wraps cleartext into a NaCl box (see
// golang.org/x/crypto/nacl) to p, authenticated from k, using a
// random nonce.
@ -112,6 +125,19 @@ type MachinePublic struct {
k [32]byte
}
// MachinePublicFromRaw32 parses a 32-byte raw value as a MachinePublic.
//
// This should be used only when deserializing a MachinePublic from a
// binary protocol.
func MachinePublicFromRaw32(raw mem.RO) MachinePublic {
if raw.Len() != 32 {
panic("input has wrong size")
}
var ret MachinePublic
raw.Copy(ret.k[:])
return ret
}
// ParseMachinePublicUntyped parses an untyped 64-character hex value
// as a MachinePublic.
//
@ -153,6 +179,19 @@ func (k MachinePublic) UntypedHexString() string {
return hex.EncodeToString(k.k[:])
}
// UntypedBytes returns k, encoded as an untyped 64-character hex
// string.
//
// Deprecated: this function is risky to use, because it produces
// serialized values that do not identify themselves as a
// MachinePublic, allowing other code to potentially parse it back in
// as the wrong key type. For new uses that don't require this
// specific raw byte serialization, please use
// MarshalText/UnmarshalText.
func (k MachinePublic) UntypedBytes() []byte {
return append([]byte(nil), k.k[:]...)
}
// String returns the output of MarshalText as a string.
func (k MachinePublic) String() string {
bs, err := k.MarshalText()