notify/peer_attach_auth.go

518 lines
17 KiB
Go
Raw Permalink Normal View History

package notify
import (
"bytes"
"crypto/hmac"
cryptorand "crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"net"
"sync"
"sync/atomic"
"time"
)
const (
peerAttachFeatureExplicitAuth uint64 = 1 << iota
peerAttachFeatureChannelBinding
peerAttachFeatureForwardSecrecy
)
const (
peerAttachNonceSize = 16
peerAttachReplayTTL = 30 * time.Second
)
var (
errPeerAttachAuthInvalid = errors.New("peer attach auth invalid")
errPeerAttachReplayRejected = errors.New("peer attach replay rejected")
errPeerAttachReplayWindowFull = errors.New("peer attach replay window full")
errPeerAttachExplicitAuthRequired = errors.New("peer attach explicit auth required")
errPeerAttachChannelBindingRequired = errors.New("peer attach channel binding required")
errPeerAttachChannelBindingUnavailable = errors.New("peer attach channel binding unavailable")
errPeerAttachForwardSecrecyRequired = errors.New("peer attach forward secrecy required")
)
type peerAttachAuthResult struct {
explicit bool
fallback bool
clientNonce []byte
serverNonce []byte
channelBinding []byte
clientECDHEPublicKey []byte
}
type peerAttachReplayCache struct {
mu sync.Mutex
entries map[string]time.Time
rejects atomic.Int64
overflowRejects atomic.Int64
}
func newPeerAttachNonce() ([]byte, error) {
buf := make([]byte, peerAttachNonceSize)
if _, err := cryptorand.Read(buf); err != nil {
return nil, err
}
return buf, nil
}
func appendPeerAttachAuthBytes(dst []byte, data []byte) []byte {
dst = binary.BigEndian.AppendUint32(dst, uint32(len(data)))
return append(dst, data...)
}
func appendPeerAttachAuthString(dst []byte, value string) []byte {
return appendPeerAttachAuthBytes(dst, []byte(value))
}
func appendPeerAttachAuthBool(dst []byte, value bool) []byte {
if value {
return append(dst, 1)
}
return append(dst, 0)
}
func peerAttachRequestAuthPayload(req peerAttachRequest, channelBinding []byte) []byte {
buf := make([]byte, 0, 96+len(req.PeerID)+len(channelBinding))
buf = appendPeerAttachAuthString(buf, "notify/peer-attach/request-auth/v1")
buf = binary.BigEndian.AppendUint64(buf, req.Features)
buf = appendPeerAttachAuthString(buf, req.PeerID)
buf = appendPeerAttachAuthBytes(buf, req.ClientNonce)
if supportsPeerAttachChannelBinding(req.Features) {
buf = appendPeerAttachAuthBytes(buf, channelBinding)
}
return buf
}
func peerAttachResponseAuthPayload(req peerAttachRequest, resp peerAttachResponse, channelBinding []byte) []byte {
buf := make([]byte, 0, 160+len(req.PeerID)+len(resp.PeerID)+len(resp.Error)+len(channelBinding))
buf = appendPeerAttachAuthString(buf, "notify/peer-attach/response-auth/v1")
buf = binary.BigEndian.AppendUint64(buf, req.Features)
buf = appendPeerAttachAuthString(buf, req.PeerID)
buf = appendPeerAttachAuthBytes(buf, req.ClientNonce)
buf = binary.BigEndian.AppendUint64(buf, resp.Features)
buf = appendPeerAttachAuthString(buf, resp.PeerID)
buf = appendPeerAttachAuthBool(buf, resp.Accepted)
buf = appendPeerAttachAuthBool(buf, resp.Reused)
buf = appendPeerAttachAuthString(buf, resp.Error)
buf = appendPeerAttachAuthBytes(buf, resp.ServerNonce)
if supportsPeerAttachChannelBinding(resp.Features) {
buf = appendPeerAttachAuthBytes(buf, channelBinding)
}
return buf
}
func signPeerAttachPayload(secretKey []byte, payload []byte) []byte {
if len(secretKey) == 0 {
return nil
}
mac := hmac.New(sha256.New, secretKey)
_, _ = mac.Write(payload)
return mac.Sum(nil)
}
func computePeerAttachRequestAuthTag(secretKey []byte, req peerAttachRequest, channelBinding []byte) []byte {
return signPeerAttachPayload(secretKey, peerAttachRequestAuthPayload(req, channelBinding))
}
func computePeerAttachResponseAuthTag(secretKey []byte, req peerAttachRequest, resp peerAttachResponse, channelBinding []byte) []byte {
return signPeerAttachPayload(secretKey, peerAttachResponseAuthPayload(req, resp, channelBinding))
}
func supportsExplicitPeerAttachAuth(features uint64) bool {
return features&peerAttachFeatureExplicitAuth != 0
}
func supportsPeerAttachChannelBinding(features uint64) bool {
return features&peerAttachFeatureChannelBinding != 0
}
func supportsPeerAttachForwardSecrecy(features uint64) bool {
return features&peerAttachFeatureForwardSecrecy != 0
}
func classifyPeerAttachRejectCounter(s *ServerCommon, err error) {
if s == nil || err == nil {
return
}
switch {
case errors.Is(err, errPeerAttachReplayRejected):
s.peerAttachReplay.rejects.Add(1)
case errors.Is(err, errPeerAttachReplayWindowFull):
s.peerAttachReplay.overflowRejects.Add(1)
case errors.Is(err, errPeerAttachExplicitAuthRequired), errors.Is(err, errPeerAttachChannelBindingRequired), errors.Is(err, errPeerAttachForwardSecrecyRequired):
s.peerAttachDowngradeRejectCount.Add(1)
case errors.Is(err, errPeerAttachChannelBindingUnavailable):
s.peerAttachBindingRejectCount.Add(1)
default:
s.peerAttachAuthRejectCount.Add(1)
}
}
func (c *ClientCommon) shouldUseExplicitPeerAttachAuth() bool {
if c == nil || !c.securityConfigured {
return false
}
return c.securityAuthMode == AuthPSK && len(c.securityBootstrap.secretKey) != 0
}
func resolvePeerAttachChannelBinding(provider PeerAttachChannelBindingProvider, role PeerAttachChannelBindingRole, peerID string, conn net.Conn) ([]byte, error) {
if provider == nil {
return nil, nil
}
if conn == nil {
return nil, errPeerAttachChannelBindingUnavailable
}
binding, err := provider(PeerAttachChannelBindingContext{
Role: role,
PeerID: peerID,
Conn: conn,
})
if err != nil || len(binding) == 0 {
return nil, errPeerAttachChannelBindingUnavailable
}
return bytes.Clone(binding), nil
}
func (c *ClientCommon) buildPeerAttachRequest(peerID string) (peerAttachRequest, peerAttachRequestState, error) {
cfg := c.peerAttachSecuritySnapshot()
req := peerAttachRequest{
PeerID: stringsTrimSpaceNoAlloc(peerID),
}
if !c.shouldUseExplicitPeerAttachAuth() {
if c.clientRequiresForwardSecrecy() {
return peerAttachRequest{}, peerAttachRequestState{}, errPeerAttachForwardSecrecyRequired
}
if cfg.requireExplicitAuth {
return peerAttachRequest{}, peerAttachRequestState{}, errPeerAttachExplicitAuthRequired
}
return req, peerAttachRequestState{}, nil
}
nonce, err := newPeerAttachNonce()
if err != nil {
return peerAttachRequest{}, peerAttachRequestState{}, err
}
req.Features = peerAttachFeatureExplicitAuth
requestState := peerAttachRequestState{}
var channelBinding []byte
if cfg.channelBinding != nil {
channelBinding, err = resolvePeerAttachChannelBinding(cfg.channelBinding, PeerAttachChannelBindingRoleClient, req.PeerID, c.clientTransportConnSnapshot())
if err != nil {
return peerAttachRequest{}, peerAttachRequestState{}, err
}
}
if len(channelBinding) != 0 {
req.Features |= peerAttachFeatureChannelBinding
}
if cfg.requireChannelBinding && !supportsPeerAttachChannelBinding(req.Features) {
return peerAttachRequest{}, peerAttachRequestState{}, errPeerAttachChannelBindingRequired
}
if c.clientSupportsForwardSecrecy() {
requestState.forwardSecrecy, err = newPeerAttachForwardSecrecyClientState()
if err != nil {
return peerAttachRequest{}, peerAttachRequestState{}, err
}
req.Features |= peerAttachFeatureForwardSecrecy
req.ClientECDHEPublicKey = bytes.Clone(requestState.forwardSecrecy.publicKey)
}
req.ClientNonce = nonce
req.AuthTag = computePeerAttachRequestAuthTag(c.securityBootstrap.secretKey, req, channelBinding)
return req, requestState, nil
}
func (c *ClientCommon) verifyPeerAttachResponse(req peerAttachRequest, resp peerAttachResponse, requestState peerAttachRequestState) (peerAttachResponseVerifyResult, error) {
cfg := c.peerAttachSecuritySnapshot()
baseSteady := transportProtectionProfile{}
if c != nil {
baseSteady = c.securitySteady.clone().withForwardSecrecyFallback(false)
}
result := peerAttachResponseVerifyResult{steadyProfile: baseSteady}
if c == nil || !c.shouldUseExplicitPeerAttachAuth() {
if c.clientRequiresForwardSecrecy() {
return peerAttachResponseVerifyResult{}, errPeerAttachForwardSecrecyRequired
}
if cfg.requireExplicitAuth {
return peerAttachResponseVerifyResult{}, errPeerAttachExplicitAuthRequired
}
return result, nil
}
if !supportsExplicitPeerAttachAuth(resp.Features) {
if c.clientRequiresForwardSecrecy() {
return peerAttachResponseVerifyResult{}, errPeerAttachForwardSecrecyRequired
}
if cfg.requireExplicitAuth {
return peerAttachResponseVerifyResult{}, errPeerAttachExplicitAuthRequired
}
if c.clientSupportsForwardSecrecy() {
result.steadyProfile = result.steadyProfile.withForwardSecrecyFallback(true)
}
result.authFallback = true
return result, nil
}
var channelBinding []byte
if supportsPeerAttachChannelBinding(req.Features) {
if cfg.channelBinding == nil {
return peerAttachResponseVerifyResult{}, errPeerAttachChannelBindingUnavailable
}
if !supportsPeerAttachChannelBinding(resp.Features) {
return peerAttachResponseVerifyResult{}, errPeerAttachChannelBindingRequired
}
var err error
channelBinding, err = resolvePeerAttachChannelBinding(cfg.channelBinding, PeerAttachChannelBindingRoleClient, req.PeerID, c.clientTransportConnSnapshot())
if err != nil {
return peerAttachResponseVerifyResult{}, err
}
} else if cfg.requireChannelBinding {
return peerAttachResponseVerifyResult{}, errPeerAttachChannelBindingRequired
}
if len(resp.ServerNonce) != peerAttachNonceSize {
return peerAttachResponseVerifyResult{}, errPeerAttachAuthInvalid
}
expected := computePeerAttachResponseAuthTag(c.securityBootstrap.secretKey, req, peerAttachResponse{
PeerID: resp.PeerID,
Accepted: resp.Accepted,
Reused: resp.Reused,
Error: resp.Error,
Features: resp.Features,
ServerNonce: resp.ServerNonce,
}, channelBinding)
if !hmac.Equal(resp.AuthTag, expected) {
return peerAttachResponseVerifyResult{}, errPeerAttachAuthInvalid
}
if requestState.forwardSecrecy == nil || !supportsPeerAttachForwardSecrecy(req.Features) {
return result, nil
}
if !supportsPeerAttachForwardSecrecy(resp.Features) {
if c.clientRequiresForwardSecrecy() {
return peerAttachResponseVerifyResult{}, errPeerAttachForwardSecrecyRequired
}
result.steadyProfile = result.steadyProfile.withForwardSecrecyFallback(true)
return result, nil
}
if resp.KeyMode != "" && resp.KeyMode != peerAttachKeyModeECDHE {
return peerAttachResponseVerifyResult{}, errPeerAttachForwardSecrecyInvalid
}
profile, err := derivePeerAttachForwardSecrecyTransportProfile(c.securitySteady, c.securityBootstrap.secretKey, requestState.forwardSecrecy.privateKey, resp.ServerECDHEPublicKey, req, resp)
if err != nil {
return peerAttachResponseVerifyResult{}, err
}
result.steadyProfile = profile
return result, nil
}
func (s *ServerCommon) validatePeerAttachRequestAuth(logical *LogicalConn, transport net.Conn, req peerAttachRequest) (peerAttachAuthResult, error) {
cfg := s.peerAttachSecuritySnapshot()
if !supportsExplicitPeerAttachAuth(req.Features) {
if s.serverRequiresForwardSecrecy() {
return peerAttachAuthResult{}, errPeerAttachForwardSecrecyRequired
}
if cfg.requireExplicitAuth {
return peerAttachAuthResult{}, errPeerAttachExplicitAuthRequired
}
return peerAttachAuthResult{fallback: true}, nil
}
if logical == nil {
return peerAttachAuthResult{}, errPeerAttachAuthInvalid
}
var channelBinding []byte
if supportsPeerAttachChannelBinding(req.Features) {
if cfg.channelBinding == nil {
return peerAttachAuthResult{}, errPeerAttachChannelBindingUnavailable
}
var err error
channelBinding, err = resolvePeerAttachChannelBinding(cfg.channelBinding, PeerAttachChannelBindingRoleServer, req.PeerID, transport)
if err != nil {
return peerAttachAuthResult{}, err
}
} else if cfg.requireChannelBinding {
return peerAttachAuthResult{}, errPeerAttachChannelBindingRequired
}
secretKey := logical.secretKeySnapshot()
if len(secretKey) == 0 {
return peerAttachAuthResult{}, errPeerAttachAuthInvalid
}
if len(req.ClientNonce) != peerAttachNonceSize {
return peerAttachAuthResult{}, errPeerAttachAuthInvalid
}
expected := computePeerAttachRequestAuthTag(secretKey, peerAttachRequest{
PeerID: req.PeerID,
Features: req.Features,
ClientNonce: req.ClientNonce,
}, channelBinding)
if !hmac.Equal(req.AuthTag, expected) {
return peerAttachAuthResult{}, errPeerAttachAuthInvalid
}
if supportsPeerAttachForwardSecrecy(req.Features) {
if len(req.ClientECDHEPublicKey) != peerAttachECDHEPublicKeySize {
return peerAttachAuthResult{}, errPeerAttachForwardSecrecyInvalid
}
}
if err := s.acceptPeerAttachReplay(req.PeerID, req.ClientNonce, time.Now(), cfg.replayWindow, cfg.replayCapacity); err != nil {
return peerAttachAuthResult{}, err
}
serverNonce, err := newPeerAttachNonce()
if err != nil {
return peerAttachAuthResult{}, err
}
return peerAttachAuthResult{
explicit: true,
clientNonce: bytes.Clone(req.ClientNonce),
serverNonce: serverNonce,
channelBinding: channelBinding,
clientECDHEPublicKey: bytes.Clone(req.ClientECDHEPublicKey),
}, nil
}
func (s *ServerCommon) signPeerAttachResponse(logical *LogicalConn, req peerAttachRequest, resp *peerAttachResponse, auth peerAttachAuthResult) {
if s == nil || logical == nil || resp == nil || !auth.explicit {
return
}
secretKey := logical.secretKeySnapshot()
if len(secretKey) == 0 {
return
}
resp.Features |= peerAttachFeatureExplicitAuth
if len(auth.channelBinding) != 0 {
resp.Features |= peerAttachFeatureChannelBinding
}
resp.ServerNonce = bytes.Clone(auth.serverNonce)
resp.AuthTag = computePeerAttachResponseAuthTag(secretKey, req, *resp, auth.channelBinding)
}
func (s *ServerCommon) preparePeerAttachSteadyTransportProfile(logical *LogicalConn, req peerAttachRequest, resp *peerAttachResponse, auth peerAttachAuthResult) (transportProtectionProfile, error) {
if s == nil {
return transportProtectionProfile{}, nil
}
profile := s.securitySteady.clone().withForwardSecrecyFallback(false)
if resp != nil && auth.explicit {
resp.Features |= peerAttachFeatureExplicitAuth
if len(auth.channelBinding) != 0 {
resp.Features |= peerAttachFeatureChannelBinding
}
resp.ServerNonce = bytes.Clone(auth.serverNonce)
}
if resp != nil && resp.KeyMode == "" {
resp.KeyMode = profile.keyMode
}
if !s.serverSupportsForwardSecrecy() {
return profile, nil
}
if !auth.explicit || !supportsPeerAttachForwardSecrecy(req.Features) {
if s.serverRequiresForwardSecrecy() {
return transportProtectionProfile{}, errPeerAttachForwardSecrecyRequired
}
return profile.withForwardSecrecyFallback(true), nil
}
fsState, err := newPeerAttachForwardSecrecyClientState()
if err != nil {
return transportProtectionProfile{}, err
}
if resp != nil {
resp.Features |= peerAttachFeatureForwardSecrecy
resp.KeyMode = peerAttachKeyModeECDHE
resp.ServerECDHEPublicKey = bytes.Clone(fsState.publicKey)
}
return derivePeerAttachForwardSecrecyTransportProfile(s.securitySteady, logical.secretKeySnapshot(), fsState.privateKey, auth.clientECDHEPublicKey, req, *resp)
}
func (s *ServerCommon) acceptPeerAttachReplay(peerID string, nonce []byte, now time.Time, window time.Duration, capacity int) error {
if s == nil || len(nonce) == 0 {
return nil
}
cache := &s.peerAttachReplay
key := peerID + "\x00" + string(nonce)
expireBefore := now.Add(-window)
cache.mu.Lock()
defer cache.mu.Unlock()
if cache.entries == nil {
cache.entries = make(map[string]time.Time)
}
for replayKey, seenAt := range cache.entries {
if seenAt.Before(expireBefore) {
delete(cache.entries, replayKey)
}
}
if seenAt, ok := cache.entries[key]; ok && !seenAt.Before(expireBefore) {
return errPeerAttachReplayRejected
}
if capacity > 0 && len(cache.entries) >= capacity {
return errPeerAttachReplayWindowFull
}
cache.entries[key] = now
return nil
}
func (s *ServerCommon) peerAttachReplayRejectCountSnapshot() int64 {
if s == nil {
return 0
}
return s.peerAttachReplay.rejects.Load()
}
func (s *ServerCommon) peerAttachReplayOverflowRejectCountSnapshot() int64 {
if s == nil {
return 0
}
return s.peerAttachReplay.overflowRejects.Load()
}
func (c *ClientCommon) markClientPeerAttachAuthenticated(fallback bool, at time.Time) {
if c == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
c.peerAttachAuthenticated = true
c.peerAttachAuthFallback = fallback
c.peerAttachAt = at.UnixNano()
}
func (c *ClientCommon) resetClientPeerAttachAuth() {
if c == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
c.peerAttachAuthenticated = false
c.peerAttachAuthFallback = false
c.peerAttachAt = 0
}
func (c *ClientCommon) clientPeerAttachAuthSnapshot() (bool, bool, time.Time) {
if c == nil {
return false, false, time.Time{}
}
c.mu.Lock()
defer c.mu.Unlock()
if c.peerAttachAt == 0 {
return c.peerAttachAuthenticated, c.peerAttachAuthFallback, time.Time{}
}
return c.peerAttachAuthenticated, c.peerAttachAuthFallback, time.Unix(0, c.peerAttachAt)
}
func stringsTrimSpaceNoAlloc(value string) string {
start := 0
for start < len(value) {
switch value[start] {
case ' ', '\t', '\n', '\r':
start++
default:
goto endStart
}
}
return ""
endStart:
end := len(value)
for end > start {
switch value[end-1] {
case ' ', '\t', '\n', '\r':
end--
default:
return value[start:end]
}
}
return value[start:end]
}