mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-10 23:49:09 +02:00
140 lines
4.6 KiB
Go
140 lines
4.6 KiB
Go
package certposture
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"errors"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
// Window is the default challenge window. A nonce stays valid through the window
|
|
// after the one it was issued in, so a peer re-proves possession of its key between
|
|
// once and twice per window. One window early is accepted too, for clock skew.
|
|
Window = 12 * time.Hour
|
|
|
|
// EnvWindow overrides Window, for end-to-end tests that cannot wait half a day to
|
|
// watch a renewal. Every management instance has to be given the same value: the
|
|
// window is part of the nonce, so instances that disagree reject each other's.
|
|
EnvWindow = "NB_CERT_CHALLENGE_WINDOW"
|
|
|
|
// minWindow keeps the renewal period, a third of the window, well above the
|
|
// refresher's one-second tick.
|
|
minWindow = 30 * time.Second
|
|
maxWindow = 24 * time.Hour
|
|
|
|
challengeDomain = "netbird-cert-challenge-v1"
|
|
windowLen = 8
|
|
nonceLen = windowLen + sha256.Size
|
|
|
|
// NonceSize is the length of every nonce a Challenger issues.
|
|
NonceSize = nonceLen
|
|
)
|
|
|
|
var effectiveWindow = sync.OnceValue(resolveWindow)
|
|
|
|
// EffectiveWindow returns the challenge window in force, which is Window unless
|
|
// EnvWindow overrides it. Everything timed against the window derives from this, so a
|
|
// test that shortens it shortens the renewal that goes with it.
|
|
func EffectiveWindow() time.Duration {
|
|
return effectiveWindow()
|
|
}
|
|
|
|
func resolveWindow() time.Duration {
|
|
val := os.Getenv(EnvWindow)
|
|
if val == "" {
|
|
return Window
|
|
}
|
|
|
|
window, err := time.ParseDuration(val)
|
|
if err != nil {
|
|
log.Warnf("failed to parse %s, keeping the %s certificate challenge window: %v", EnvWindow, Window, err)
|
|
return Window
|
|
}
|
|
if window < minWindow || window > maxWindow {
|
|
log.Warnf("%s of %s is outside %s..%s, keeping the %s certificate challenge window", EnvWindow, window, minWindow, maxWindow, Window)
|
|
return Window
|
|
}
|
|
if window%time.Second != 0 {
|
|
log.Warnf("%s of %s is not a whole number of seconds, keeping the %s certificate challenge window", EnvWindow, window, Window)
|
|
return Window
|
|
}
|
|
|
|
// Loud on purpose: this sets how long a device can pass the certificate check after
|
|
// its key has gone, and it has to match on every instance.
|
|
log.Warnf("certificate challenge window overridden to %s by %s", window, EnvWindow)
|
|
return window
|
|
}
|
|
|
|
var (
|
|
ErrNonceMalformed = errors.New("certificate challenge nonce is malformed")
|
|
ErrNonceExpired = errors.New("certificate challenge nonce is expired")
|
|
ErrNonceMismatch = errors.New("certificate challenge nonce was not issued to this peer")
|
|
)
|
|
|
|
// Challenger issues and verifies stateless per-peer nonces. A nonce is bound to the
|
|
// peer and to a time window, so any instance sharing the secret can verify it.
|
|
type Challenger struct {
|
|
secret []byte
|
|
window time.Duration
|
|
}
|
|
|
|
func NewChallenger(secret []byte) *Challenger {
|
|
return &Challenger{secret: secret, window: EffectiveWindow()}
|
|
}
|
|
|
|
func (c *Challenger) Nonce(peerKey []byte, now time.Time) []byte {
|
|
return c.nonceForWindow(peerKey, c.windowOf(now))
|
|
}
|
|
|
|
func (c *Challenger) verifyNonce(nonce, peerKey []byte, now time.Time) error {
|
|
if len(nonce) != nonceLen {
|
|
return ErrNonceMalformed
|
|
}
|
|
window := binary.BigEndian.Uint64(nonce[:windowLen])
|
|
if !windowAccepted(window, c.windowOf(now)) {
|
|
return ErrNonceExpired
|
|
}
|
|
if !hmac.Equal(nonce, c.nonceForWindow(peerKey, window)) {
|
|
return ErrNonceMismatch
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Challenger) windowOf(now time.Time) uint64 {
|
|
return uint64(now.Unix() / int64(c.window.Seconds()))
|
|
}
|
|
|
|
func (c *Challenger) nonceForWindow(peerKey []byte, window uint64) []byte {
|
|
nonce := make([]byte, windowLen, nonceLen)
|
|
binary.BigEndian.PutUint64(nonce, window)
|
|
|
|
mac := hmac.New(sha256.New, c.secret)
|
|
mac.Write([]byte(challengeDomain))
|
|
mac.Write(peerKey)
|
|
mac.Write(nonce[:windowLen])
|
|
return mac.Sum(nonce)
|
|
}
|
|
|
|
// NonceAcceptedAlongside reports whether a proof answering nonce is still accepted while
|
|
// management issues current to the same peer, by the same window rule verification uses.
|
|
func NonceAcceptedAlongside(nonce, current []byte) bool {
|
|
if len(nonce) != nonceLen || len(current) != nonceLen {
|
|
return false
|
|
}
|
|
window := binary.BigEndian.Uint64(nonce[:windowLen])
|
|
return windowAccepted(window, binary.BigEndian.Uint64(current[:windowLen]))
|
|
}
|
|
|
|
// windowAccepted reports whether a nonce of window is accepted in window current: the
|
|
// current window, the previous one so a nonce outlives a rollover, and the next one so an
|
|
// instance whose clock runs slightly ahead is not rejected by the others.
|
|
func windowAccepted(window, current uint64) bool {
|
|
return window == current || window+1 == current || window == current+1
|
|
}
|