mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
Auto-update logic moved out of the UI into a dedicated updatemanager.Manager service that runs in the connection layer. The UI no longer polls or checks for updates independently. The update manager supports three modes driven by the management server's auto-update policy: No policy set by mgm: checks GitHub for the latest version and notifies the user (previous behavior, now centralized) mgm enforces update: the "About" menu triggers installation directly instead of just downloading the file — user still initiates the action mgm forces update: installation proceeds automatically without user interaction updateManager lifecycle is now owned by daemon, giving the daemon server direct control via a new TriggerUpdate RPC Introduces EngineServices struct to group external service dependencies passed to NewEngine, reducing its argument count from 11 to 4
230 lines
6.8 KiB
Go
230 lines
6.8 KiB
Go
package reposign
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
maxRevocationSignatureAge = 10 * 365 * 24 * time.Hour
|
|
defaultRevocationListExpiration = 365 * 24 * time.Hour
|
|
)
|
|
|
|
type RevocationList struct {
|
|
Revoked map[KeyID]time.Time `json:"revoked"` // KeyID -> revocation time
|
|
LastUpdated time.Time `json:"last_updated"` // When the list was last modified
|
|
ExpiresAt time.Time `json:"expires_at"` // When the list expires
|
|
}
|
|
|
|
func (rl RevocationList) MarshalJSON() ([]byte, error) {
|
|
// Convert map[KeyID]time.Time to map[string]time.Time
|
|
strMap := make(map[string]time.Time, len(rl.Revoked))
|
|
for k, v := range rl.Revoked {
|
|
strMap[k.String()] = v
|
|
}
|
|
|
|
return json.Marshal(map[string]interface{}{
|
|
"revoked": strMap,
|
|
"last_updated": rl.LastUpdated,
|
|
"expires_at": rl.ExpiresAt,
|
|
})
|
|
}
|
|
|
|
func (rl *RevocationList) UnmarshalJSON(data []byte) error {
|
|
var temp struct {
|
|
Revoked map[string]time.Time `json:"revoked"`
|
|
LastUpdated time.Time `json:"last_updated"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
Version int `json:"version"`
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &temp); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Convert map[string]time.Time back to map[KeyID]time.Time
|
|
rl.Revoked = make(map[KeyID]time.Time, len(temp.Revoked))
|
|
for k, v := range temp.Revoked {
|
|
kid, err := ParseKeyID(k)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse KeyID %q: %w", k, err)
|
|
}
|
|
rl.Revoked[kid] = v
|
|
}
|
|
|
|
rl.LastUpdated = temp.LastUpdated
|
|
rl.ExpiresAt = temp.ExpiresAt
|
|
|
|
return nil
|
|
}
|
|
|
|
func ParseRevocationList(data []byte) (*RevocationList, error) {
|
|
var rl RevocationList
|
|
if err := json.Unmarshal(data, &rl); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal revocation list: %w", err)
|
|
}
|
|
|
|
// Initialize the map if it's nil (in case of empty JSON object)
|
|
if rl.Revoked == nil {
|
|
rl.Revoked = make(map[KeyID]time.Time)
|
|
}
|
|
|
|
if rl.LastUpdated.IsZero() {
|
|
return nil, fmt.Errorf("revocation list missing last_updated timestamp")
|
|
}
|
|
|
|
if rl.ExpiresAt.IsZero() {
|
|
return nil, fmt.Errorf("revocation list missing expires_at timestamp")
|
|
}
|
|
|
|
return &rl, nil
|
|
}
|
|
|
|
func ValidateRevocationList(publicRootKeys []PublicKey, data []byte, signature Signature) (*RevocationList, error) {
|
|
revoList, err := ParseRevocationList(data)
|
|
if err != nil {
|
|
log.Debugf("failed to parse revocation list: %s", err)
|
|
return nil, err
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
|
|
// Validate signature timestamp
|
|
if signature.Timestamp.After(now.Add(maxClockSkew)) {
|
|
err := fmt.Errorf("revocation signature timestamp is in the future: %v", signature.Timestamp)
|
|
log.Debugf("revocation list signature error: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
if now.Sub(signature.Timestamp) > maxRevocationSignatureAge {
|
|
err := fmt.Errorf("revocation list signature is too old: %v (created %v)",
|
|
now.Sub(signature.Timestamp), signature.Timestamp)
|
|
log.Debugf("revocation list signature error: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Ensure LastUpdated is not in the future (with clock skew tolerance)
|
|
if revoList.LastUpdated.After(now.Add(maxClockSkew)) {
|
|
err := fmt.Errorf("revocation list LastUpdated is in the future: %v", revoList.LastUpdated)
|
|
log.Errorf("rejecting future-dated revocation list: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Check if the revocation list has expired
|
|
if now.After(revoList.ExpiresAt) {
|
|
err := fmt.Errorf("revocation list expired at %v (current time: %v)", revoList.ExpiresAt, now)
|
|
log.Errorf("rejecting expired revocation list: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Ensure ExpiresAt is not in the future by more than the expected expiration window
|
|
// (allows some clock skew but prevents maliciously long expiration times)
|
|
if revoList.ExpiresAt.After(now.Add(maxRevocationSignatureAge)) {
|
|
err := fmt.Errorf("revocation list ExpiresAt is too far in the future: %v", revoList.ExpiresAt)
|
|
log.Errorf("rejecting revocation list with invalid expiration: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Validate signature timestamp is close to LastUpdated
|
|
// (prevents signing old lists with new timestamps)
|
|
timeDiff := signature.Timestamp.Sub(revoList.LastUpdated).Abs()
|
|
if timeDiff > maxClockSkew {
|
|
err := fmt.Errorf("signature timestamp %v differs too much from list LastUpdated %v (diff: %v)",
|
|
signature.Timestamp, revoList.LastUpdated, timeDiff)
|
|
log.Errorf("timestamp mismatch in revocation list: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Reconstruct the signed message: revocation_list_data || timestamp || version
|
|
msg := make([]byte, 0, len(data)+8)
|
|
msg = append(msg, data...)
|
|
msg = binary.LittleEndian.AppendUint64(msg, uint64(signature.Timestamp.Unix()))
|
|
|
|
if !verifyAny(publicRootKeys, msg, signature.Signature) {
|
|
return nil, errors.New("revocation list verification failed")
|
|
}
|
|
return revoList, nil
|
|
}
|
|
|
|
func CreateRevocationList(privateRootKey RootKey, expiration time.Duration) ([]byte, []byte, error) {
|
|
now := time.Now()
|
|
rl := RevocationList{
|
|
Revoked: make(map[KeyID]time.Time),
|
|
LastUpdated: now.UTC(),
|
|
ExpiresAt: now.Add(expiration).UTC(),
|
|
}
|
|
|
|
signature, err := signRevocationList(privateRootKey, rl)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to sign revocation list: %w", err)
|
|
}
|
|
|
|
rlData, err := json.Marshal(&rl)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal revocation list: %w", err)
|
|
}
|
|
|
|
signData, err := json.Marshal(signature)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal signature: %w", err)
|
|
}
|
|
|
|
return rlData, signData, nil
|
|
}
|
|
|
|
func ExtendRevocationList(privateRootKey RootKey, rl RevocationList, kid KeyID, expiration time.Duration) ([]byte, []byte, error) {
|
|
now := time.Now().UTC()
|
|
|
|
rl.Revoked[kid] = now
|
|
rl.LastUpdated = now
|
|
rl.ExpiresAt = now.Add(expiration)
|
|
|
|
signature, err := signRevocationList(privateRootKey, rl)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to sign revocation list: %w", err)
|
|
}
|
|
|
|
rlData, err := json.Marshal(&rl)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal revocation list: %w", err)
|
|
}
|
|
|
|
signData, err := json.Marshal(signature)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal signature: %w", err)
|
|
}
|
|
|
|
return rlData, signData, nil
|
|
}
|
|
|
|
func signRevocationList(privateRootKey RootKey, rl RevocationList) (*Signature, error) {
|
|
data, err := json.Marshal(rl)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal revocation list for signing: %w", err)
|
|
}
|
|
|
|
timestamp := time.Now().UTC()
|
|
|
|
msg := make([]byte, 0, len(data)+8)
|
|
msg = append(msg, data...)
|
|
msg = binary.LittleEndian.AppendUint64(msg, uint64(timestamp.Unix()))
|
|
|
|
sig := ed25519.Sign(privateRootKey.Key, msg)
|
|
|
|
signature := &Signature{
|
|
Signature: sig,
|
|
Timestamp: timestamp,
|
|
KeyID: privateRootKey.Metadata.ID,
|
|
Algorithm: "ed25519",
|
|
HashAlgo: "sha512",
|
|
}
|
|
|
|
return signature, nil
|
|
}
|