blob: 65bec0b23bf1d535ab0e82d2f50368c5856b20e0 [file] [log] [blame]
// Copyright 2015 The Vanadium 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 vif
import (
"crypto/rand"
"io"
"golang.org/x/crypto/nacl/box"
rpcversion "v.io/v23/rpc/version"
"v.io/v23/security"
"v.io/v23/verror"
"v.io/x/ref/profiles/internal/lib/iobuf"
"v.io/x/ref/profiles/internal/rpc/stream"
"v.io/x/ref/profiles/internal/rpc/stream/crypto"
"v.io/x/ref/profiles/internal/rpc/stream/message"
"v.io/x/ref/profiles/internal/rpc/stream/vc"
"v.io/x/ref/profiles/internal/rpc/version"
)
var (
// These errors are intended to be used as arguments to higher
// level errors and hence {1}{2} is omitted from their format
// strings to avoid repeating these n-times in the final error
// message visible to the user.
errAuthFailed = reg(".errAuthFailed", "authentication failed{:3}")
errUnsupportedEncryptVersion = reg(".errUnsupportedEncryptVersion", "unsupported encryption version {4} < {5}")
errNaclBoxVersionNegotiationFailed = reg(".errNaclBoxVersionNegotiationFailed", "nacl box encryption version negotiation failed")
errVersionNegotiationFailed = reg(".errVersionNegotiationFailed", "encryption version negotiation failed")
nullCipher crypto.NullControlCipher
)
// privateData includes secret data we need for encryption.
type privateData struct {
naclBoxPrivateKey crypto.BoxKey
}
// AuthenticateAsClient sends a Setup message if possible. If so, it chooses
// encryption based on the max supported version.
//
// The sequence is initiated by the client.
//
// - The client sends a Setup message to the server, containing the client's
// supported versions, and the client's crypto options. The Setup message
// is sent in the clear.
//
// - When the server receives the Setup message, it calls
// AuthenticateAsServer, which constructs a response Setup containing
// the server's version range, and any crypto options.
//
// - The client and server use the public/private key pairs
// generated for the Setup messages to create an encrypted stream
// of SetupStream messages for the remainder of the authentication
// setup. The encyrption uses NewControlCipherRPC6, which is based
// on code.google.com/p/go.crypto/nacl/box.
//
// - Once the encrypted SetupStream channel is setup, the client and
// server authenticate using the vc.AuthenticateAs{Client,Server} protocol.
//
// Note that the Setup messages are sent in the clear, so they are subject to
// modification by a man-in-the-middle, which can currently force a downgrade by
// modifying the acceptable version ranges downward. This can be addressed by
// including a hash of the Setup message in the encrypted stream. It is
// likely that this will be addressed in subsequent protocol versions.
func AuthenticateAsClient(writer io.Writer, reader *iobuf.Reader, versions *version.Range, params security.CallParams, auth *vc.ServerAuthorizer) (crypto.ControlCipher, error) {
if versions == nil {
versions = version.SupportedRange
}
// Send the client's public data.
pvt, pub, err := makeSetup(versions, params.LocalPrincipal != nil)
if err != nil {
return nil, verror.New(stream.ErrSecurity, nil, err)
}
errch := make(chan error, 1)
go func() {
errch <- message.WriteTo(writer, pub, nullCipher)
}()
pmsg, err := message.ReadFrom(reader, nullCipher)
if err != nil {
return nil, verror.New(stream.ErrNetwork, nil, err)
}
ppub, ok := pmsg.(*message.Setup)
if !ok {
return nil, verror.New(stream.ErrSecurity, nil, verror.New(errVersionNegotiationFailed, nil))
}
// Wait for the write to succeed.
if err := <-errch; err != nil {
return nil, verror.New(stream.ErrNetwork, nil, err)
}
// Choose the max version in the intersection.
vrange, err := pub.Versions.Intersect(&ppub.Versions)
if err != nil {
return nil, verror.New(stream.ErrNetwork, nil, err)
}
v := vrange.Max
if params.LocalPrincipal == nil {
return nullCipher, nil
}
// Perform the authentication.
return authenticateAsClient(writer, reader, params, auth, pvt, pub, ppub, v)
}
func authenticateAsClient(writer io.Writer, reader *iobuf.Reader, params security.CallParams, auth *vc.ServerAuthorizer,
pvt *privateData, pub, ppub *message.Setup, version rpcversion.RPCVersion) (crypto.ControlCipher, error) {
pbox := ppub.NaclBox()
if pbox == nil {
return nil, verror.New(errNaclBoxVersionNegotiationFailed, nil)
}
c := crypto.NewControlCipherRPC6(&pbox.PublicKey, &pvt.naclBoxPrivateKey, false)
sconn := newSetupConn(writer, reader, c)
// TODO(jyh): act upon the authentication results.
_, _, _, err := vc.AuthenticateAsClient(sconn, crypto.NewNullCrypter(), params, auth, version)
if err != nil {
return nil, err
}
return c, nil
}
// AuthenticateAsServer handles a Setup message, choosing authentication
// based on the max common version.
//
// See AuthenticateAsClient for a description of the negotiation.
func AuthenticateAsServer(writer io.Writer, reader *iobuf.Reader, versions *version.Range, principal security.Principal, lBlessings security.Blessings,
dc vc.DischargeClient) (crypto.ControlCipher, error) {
var err error
if versions == nil {
versions = version.SupportedRange
}
// Send server's public data.
pvt, pub, err := makeSetup(versions, principal != nil)
if err != nil {
return nil, err
}
errch := make(chan error, 1)
readch := make(chan struct{})
go func() {
// TODO(mattr,ribrdb): In the case of the agent, which is
// currently the only user of insecure connections, we need to
// wait for the client to initiate the communication. The agent
// sends an extra first byte to clients, which clients read before
// dialing their side of the vif. If we send this message before
// the magic byte has been sent the client will use the first
// byte of this message instead rendering the remainder of the
// stream uninterpretable.
if principal == nil {
<-readch
}
err := message.WriteTo(writer, pub, nullCipher)
errch <- err
}()
// Read client's public data.
pmsg, err := message.ReadFrom(reader, nullCipher)
close(readch) // Note: we need to close this whether we get an error or not.
if err != nil {
return nil, verror.New(stream.ErrNetwork, nil, err)
}
ppub, ok := pmsg.(*message.Setup)
if !ok {
return nil, verror.New(stream.ErrSecurity, nil, verror.New(errVersionNegotiationFailed, nil))
}
// Wait for the write to succeed.
if err := <-errch; err != nil {
return nil, err
}
// Choose the max version in the intersection.
vrange, err := versions.Intersect(&ppub.Versions)
if err != nil {
return nil, verror.New(stream.ErrNetwork, nil, err)
}
v := vrange.Max
if principal == nil {
return nullCipher, nil
}
// Perform authentication.
return authenticateAsServerRPC6(writer, reader, principal, lBlessings, dc, pvt, pub, ppub, v)
}
func authenticateAsServerRPC6(writer io.Writer, reader *iobuf.Reader, principal security.Principal, lBlessings security.Blessings, dc vc.DischargeClient,
pvt *privateData, pub, ppub *message.Setup, version rpcversion.RPCVersion) (crypto.ControlCipher, error) {
box := ppub.NaclBox()
if box == nil {
return nil, verror.New(errNaclBoxVersionNegotiationFailed, nil)
}
c := crypto.NewControlCipherRPC6(&box.PublicKey, &pvt.naclBoxPrivateKey, true)
sconn := newSetupConn(writer, reader, c)
// TODO(jyh): act upon authentication results.
_, _, err := vc.AuthenticateAsServer(sconn, principal, lBlessings, dc, crypto.NewNullCrypter(), version)
if err != nil {
return nil, verror.New(errAuthFailed, nil, err)
}
return c, nil
}
// getDischargeClient returns the dischargeClient needed to fetch server discharges for this call.
// TODO(suharshs): Perhaps we should pass dischargeClient explicitly?
func getDischargeClient(lopts []stream.ListenerOpt) vc.DischargeClient {
for _, o := range lopts {
switch v := o.(type) {
case vc.DischargeClient:
return v
}
}
return nil
}
// makeSetup constructs the options that this process can support.
func makeSetup(versions *version.Range, secure bool) (*privateData, *message.Setup, error) {
var options []message.SetupOption
var pvt *privateData
if secure {
pubKey, pvtKey, err := box.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
options = []message.SetupOption{&message.NaclBox{PublicKey: *pubKey}}
pvt = &privateData{
naclBoxPrivateKey: *pvtKey,
}
}
pub := &message.Setup{
Versions: *versions,
Options: options,
}
return pvt, pub, nil
}