mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-05 00:44:10 -04: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
303 lines
8.6 KiB
Go
303 lines
8.6 KiB
Go
package reposign
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/crypto/blake2s"
|
|
)
|
|
|
|
const (
|
|
tagArtifactPrivate = "ARTIFACT PRIVATE KEY"
|
|
tagArtifactPublic = "ARTIFACT PUBLIC KEY"
|
|
|
|
maxArtifactKeySignatureAge = 10 * 365 * 24 * time.Hour
|
|
maxArtifactSignatureAge = 10 * 365 * 24 * time.Hour
|
|
)
|
|
|
|
// ArtifactHash wraps a hash.Hash and counts bytes written
|
|
type ArtifactHash struct {
|
|
hash.Hash
|
|
}
|
|
|
|
// NewArtifactHash returns an initialized ArtifactHash using BLAKE2s
|
|
func NewArtifactHash() *ArtifactHash {
|
|
h, err := blake2s.New256(nil)
|
|
if err != nil {
|
|
panic(err) // Should never happen with nil Key
|
|
}
|
|
return &ArtifactHash{Hash: h}
|
|
}
|
|
|
|
func (ah *ArtifactHash) Write(b []byte) (int, error) {
|
|
return ah.Hash.Write(b)
|
|
}
|
|
|
|
// ArtifactKey is a signing Key used to sign artifacts
|
|
type ArtifactKey struct {
|
|
PrivateKey
|
|
}
|
|
|
|
func (k ArtifactKey) String() string {
|
|
return fmt.Sprintf(
|
|
"ArtifactKey[ID=%s, CreatedAt=%s, ExpiresAt=%s]",
|
|
k.Metadata.ID,
|
|
k.Metadata.CreatedAt.Format(time.RFC3339),
|
|
k.Metadata.ExpiresAt.Format(time.RFC3339),
|
|
)
|
|
}
|
|
|
|
func GenerateArtifactKey(rootKey *RootKey, expiration time.Duration) (*ArtifactKey, []byte, []byte, []byte, error) {
|
|
// Verify root key is still valid
|
|
if !rootKey.Metadata.ExpiresAt.IsZero() && time.Now().After(rootKey.Metadata.ExpiresAt) {
|
|
return nil, nil, nil, nil, fmt.Errorf("root key has expired on %s", rootKey.Metadata.ExpiresAt.Format(time.RFC3339))
|
|
}
|
|
|
|
now := time.Now()
|
|
expirationTime := now.Add(expiration)
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, fmt.Errorf("generate ed25519 key: %w", err)
|
|
}
|
|
|
|
metadata := KeyMetadata{
|
|
ID: computeKeyID(pub),
|
|
CreatedAt: now.UTC(),
|
|
ExpiresAt: expirationTime.UTC(),
|
|
}
|
|
|
|
ak := &ArtifactKey{
|
|
PrivateKey{
|
|
Key: priv,
|
|
Metadata: metadata,
|
|
},
|
|
}
|
|
|
|
// Marshal PrivateKey struct to JSON
|
|
privJSON, err := json.Marshal(ak.PrivateKey)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, fmt.Errorf("failed to marshal private key: %w", err)
|
|
}
|
|
|
|
// Marshal PublicKey struct to JSON
|
|
pubKey := PublicKey{
|
|
Key: pub,
|
|
Metadata: metadata,
|
|
}
|
|
pubJSON, err := json.Marshal(pubKey)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, fmt.Errorf("failed to marshal public key: %w", err)
|
|
}
|
|
|
|
// Encode to PEM with metadata embedded in bytes
|
|
privPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: tagArtifactPrivate,
|
|
Bytes: privJSON,
|
|
})
|
|
|
|
pubPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: tagArtifactPublic,
|
|
Bytes: pubJSON,
|
|
})
|
|
|
|
// Sign the public key with the root key
|
|
signature, err := SignArtifactKey(*rootKey, pubPEM)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, fmt.Errorf("failed to sign artifact key: %w", err)
|
|
}
|
|
|
|
return ak, privPEM, pubPEM, signature, nil
|
|
}
|
|
|
|
func ParseArtifactKey(privKeyPEM []byte) (ArtifactKey, error) {
|
|
pk, err := parsePrivateKey(privKeyPEM, tagArtifactPrivate)
|
|
if err != nil {
|
|
return ArtifactKey{}, fmt.Errorf("failed to parse artifact Key: %w", err)
|
|
}
|
|
return ArtifactKey{pk}, nil
|
|
}
|
|
|
|
func ParseArtifactPubKey(data []byte) (PublicKey, error) {
|
|
pk, _, err := parsePublicKey(data, tagArtifactPublic)
|
|
return pk, err
|
|
}
|
|
|
|
func BundleArtifactKeys(rootKey *RootKey, keys []PublicKey) ([]byte, []byte, error) {
|
|
if len(keys) == 0 {
|
|
return nil, nil, errors.New("no keys to bundle")
|
|
}
|
|
|
|
// Create bundle by concatenating PEM-encoded keys
|
|
var pubBundle []byte
|
|
|
|
for _, pk := range keys {
|
|
// Marshal PublicKey struct to JSON
|
|
pubJSON, err := json.Marshal(pk)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal public key: %w", err)
|
|
}
|
|
|
|
// Encode to PEM
|
|
pubPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: tagArtifactPublic,
|
|
Bytes: pubJSON,
|
|
})
|
|
|
|
pubBundle = append(pubBundle, pubPEM...)
|
|
}
|
|
|
|
// Sign the entire bundle with the root key
|
|
signature, err := SignArtifactKey(*rootKey, pubBundle)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to sign artifact key bundle: %w", err)
|
|
}
|
|
|
|
return pubBundle, signature, nil
|
|
}
|
|
|
|
func ValidateArtifactKeys(publicRootKeys []PublicKey, data []byte, signature Signature, revocationList *RevocationList) ([]PublicKey, error) {
|
|
now := time.Now().UTC()
|
|
if signature.Timestamp.After(now.Add(maxClockSkew)) {
|
|
err := fmt.Errorf("signature timestamp is in the future: %v", signature.Timestamp)
|
|
log.Debugf("artifact signature error: %v", err)
|
|
return nil, err
|
|
}
|
|
if now.Sub(signature.Timestamp) > maxArtifactKeySignatureAge {
|
|
err := fmt.Errorf("signature is too old: %v (created %v)", now.Sub(signature.Timestamp), signature.Timestamp)
|
|
log.Debugf("artifact signature error: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Reconstruct the signed message: artifact_key_data || timestamp
|
|
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("failed to verify signature of artifact keys")
|
|
}
|
|
|
|
pubKeys, err := parsePublicKeyBundle(data, tagArtifactPublic)
|
|
if err != nil {
|
|
log.Debugf("failed to parse public keys: %s", err)
|
|
return nil, err
|
|
}
|
|
|
|
validKeys := make([]PublicKey, 0, len(pubKeys))
|
|
for _, pubKey := range pubKeys {
|
|
// Filter out expired keys
|
|
if !pubKey.Metadata.ExpiresAt.IsZero() && now.After(pubKey.Metadata.ExpiresAt) {
|
|
log.Debugf("Key %s is expired at %v (current time %v)",
|
|
pubKey.Metadata.ID, pubKey.Metadata.ExpiresAt, now)
|
|
continue
|
|
}
|
|
|
|
if revocationList != nil {
|
|
if revTime, revoked := revocationList.Revoked[pubKey.Metadata.ID]; revoked {
|
|
log.Debugf("Key %s is revoked as of %v (created %v)",
|
|
pubKey.Metadata.ID, revTime, pubKey.Metadata.CreatedAt)
|
|
continue
|
|
}
|
|
}
|
|
validKeys = append(validKeys, pubKey)
|
|
}
|
|
|
|
if len(validKeys) == 0 {
|
|
log.Debugf("no valid public keys found for artifact keys")
|
|
return nil, fmt.Errorf("all %d artifact keys are revoked", len(pubKeys))
|
|
}
|
|
|
|
return validKeys, nil
|
|
}
|
|
|
|
func ValidateArtifact(artifactPubKeys []PublicKey, data []byte, signature Signature) error {
|
|
// Validate signature timestamp
|
|
now := time.Now().UTC()
|
|
if signature.Timestamp.After(now.Add(maxClockSkew)) {
|
|
err := fmt.Errorf("artifact signature timestamp is in the future: %v", signature.Timestamp)
|
|
log.Debugf("failed to verify signature of artifact: %s", err)
|
|
return err
|
|
}
|
|
if now.Sub(signature.Timestamp) > maxArtifactSignatureAge {
|
|
return fmt.Errorf("artifact signature is too old: %v (created %v)",
|
|
now.Sub(signature.Timestamp), signature.Timestamp)
|
|
}
|
|
|
|
h := NewArtifactHash()
|
|
if _, err := h.Write(data); err != nil {
|
|
return fmt.Errorf("failed to hash artifact: %w", err)
|
|
}
|
|
hash := h.Sum(nil)
|
|
|
|
// Reconstruct the signed message: hash || length || timestamp
|
|
msg := make([]byte, 0, len(hash)+8+8)
|
|
msg = append(msg, hash...)
|
|
msg = binary.LittleEndian.AppendUint64(msg, uint64(len(data)))
|
|
msg = binary.LittleEndian.AppendUint64(msg, uint64(signature.Timestamp.Unix()))
|
|
|
|
// Find matching Key and verify
|
|
for _, keyInfo := range artifactPubKeys {
|
|
if keyInfo.Metadata.ID == signature.KeyID {
|
|
// Check Key expiration
|
|
if !keyInfo.Metadata.ExpiresAt.IsZero() &&
|
|
signature.Timestamp.After(keyInfo.Metadata.ExpiresAt) {
|
|
return fmt.Errorf("signing Key %s expired at %v, signature from %v",
|
|
signature.KeyID, keyInfo.Metadata.ExpiresAt, signature.Timestamp)
|
|
}
|
|
|
|
if ed25519.Verify(keyInfo.Key, msg, signature.Signature) {
|
|
log.Debugf("artifact verified successfully with Key: %s", signature.KeyID)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("signature verification failed for Key %s", signature.KeyID)
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("no signing Key found with ID %s", signature.KeyID)
|
|
}
|
|
|
|
func SignData(artifactKey ArtifactKey, data []byte) ([]byte, error) {
|
|
if len(data) == 0 { // Check happens too late
|
|
return nil, fmt.Errorf("artifact length must be positive, got %d", len(data))
|
|
}
|
|
|
|
h := NewArtifactHash()
|
|
if _, err := h.Write(data); err != nil {
|
|
return nil, fmt.Errorf("failed to write artifact hash: %w", err)
|
|
}
|
|
|
|
timestamp := time.Now().UTC()
|
|
|
|
if !artifactKey.Metadata.ExpiresAt.IsZero() && timestamp.After(artifactKey.Metadata.ExpiresAt) {
|
|
return nil, fmt.Errorf("artifact key expired at %v", artifactKey.Metadata.ExpiresAt)
|
|
}
|
|
|
|
hash := h.Sum(nil)
|
|
|
|
// Create message: hash || length || timestamp
|
|
msg := make([]byte, 0, len(hash)+8+8)
|
|
msg = append(msg, hash...)
|
|
msg = binary.LittleEndian.AppendUint64(msg, uint64(len(data)))
|
|
msg = binary.LittleEndian.AppendUint64(msg, uint64(timestamp.Unix()))
|
|
|
|
sig := ed25519.Sign(artifactKey.Key, msg)
|
|
|
|
bundle := Signature{
|
|
Signature: sig,
|
|
Timestamp: timestamp,
|
|
KeyID: artifactKey.Metadata.ID,
|
|
Algorithm: "ed25519",
|
|
HashAlgo: "blake2s",
|
|
}
|
|
|
|
return json.Marshal(bundle)
|
|
}
|