Files
netbird/client/internal/updater/reposign/root_test.go
Zoltan Papp fe9b844511 [client] refactor auto update workflow (#5448)
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
2026-03-13 17:01:28 +01:00

477 lines
13 KiB
Go

package reposign
import (
"crypto/ed25519"
"crypto/rand"
"encoding/binary"
"encoding/json"
"encoding/pem"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Test RootKey.String()
func TestRootKey_String(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
createdAt := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
expiresAt := time.Date(2034, 1, 15, 10, 30, 0, 0, time.UTC)
rk := RootKey{
PrivateKey{
Key: priv,
Metadata: KeyMetadata{
ID: computeKeyID(pub),
CreatedAt: createdAt,
ExpiresAt: expiresAt,
},
},
}
str := rk.String()
assert.Contains(t, str, "RootKey")
assert.Contains(t, str, computeKeyID(pub).String())
assert.Contains(t, str, "2024-01-15")
assert.Contains(t, str, "2034-01-15")
}
func TestRootKey_String_NoExpiration(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
createdAt := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
rk := RootKey{
PrivateKey{
Key: priv,
Metadata: KeyMetadata{
ID: computeKeyID(pub),
CreatedAt: createdAt,
ExpiresAt: time.Time{}, // No expiration
},
},
}
str := rk.String()
assert.Contains(t, str, "RootKey")
assert.Contains(t, str, "0001-01-01") // Zero time format
}
// Test GenerateRootKey
func TestGenerateRootKey_Valid(t *testing.T) {
expiration := 10 * 365 * 24 * time.Hour // 10 years
rk, privPEM, pubPEM, err := GenerateRootKey(expiration)
require.NoError(t, err)
assert.NotNil(t, rk)
assert.NotEmpty(t, privPEM)
assert.NotEmpty(t, pubPEM)
// Verify the key has correct metadata
assert.False(t, rk.Metadata.CreatedAt.IsZero())
assert.False(t, rk.Metadata.ExpiresAt.IsZero())
assert.True(t, rk.Metadata.ExpiresAt.After(rk.Metadata.CreatedAt))
// Verify expiration is approximately correct
expectedExpiration := time.Now().Add(expiration)
timeDiff := rk.Metadata.ExpiresAt.Sub(expectedExpiration)
assert.True(t, timeDiff < time.Minute && timeDiff > -time.Minute)
}
func TestGenerateRootKey_ShortExpiration(t *testing.T) {
expiration := 24 * time.Hour // 1 day
rk, _, _, err := GenerateRootKey(expiration)
require.NoError(t, err)
assert.NotNil(t, rk)
// Verify expiration
expectedExpiration := time.Now().Add(expiration)
timeDiff := rk.Metadata.ExpiresAt.Sub(expectedExpiration)
assert.True(t, timeDiff < time.Minute && timeDiff > -time.Minute)
}
func TestGenerateRootKey_ZeroExpiration(t *testing.T) {
rk, _, _, err := GenerateRootKey(0)
require.NoError(t, err)
assert.NotNil(t, rk)
// With zero expiration, ExpiresAt should be equal to CreatedAt
assert.Equal(t, rk.Metadata.CreatedAt, rk.Metadata.ExpiresAt)
}
func TestGenerateRootKey_PEMFormat(t *testing.T) {
rk, privPEM, pubPEM, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
// Verify private key PEM
privBlock, _ := pem.Decode(privPEM)
require.NotNil(t, privBlock)
assert.Equal(t, tagRootPrivate, privBlock.Type)
var privKey PrivateKey
err = json.Unmarshal(privBlock.Bytes, &privKey)
require.NoError(t, err)
assert.Equal(t, rk.Key, privKey.Key)
// Verify public key PEM
pubBlock, _ := pem.Decode(pubPEM)
require.NotNil(t, pubBlock)
assert.Equal(t, tagRootPublic, pubBlock.Type)
var pubKey PublicKey
err = json.Unmarshal(pubBlock.Bytes, &pubKey)
require.NoError(t, err)
assert.Equal(t, rk.Metadata.ID, pubKey.Metadata.ID)
}
func TestGenerateRootKey_KeySize(t *testing.T) {
rk, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
// Ed25519 private key should be 64 bytes
assert.Equal(t, ed25519.PrivateKeySize, len(rk.Key))
// Ed25519 public key should be 32 bytes
pubKey := rk.Key.Public().(ed25519.PublicKey)
assert.Equal(t, ed25519.PublicKeySize, len(pubKey))
}
func TestGenerateRootKey_UniqueKeys(t *testing.T) {
rk1, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
rk2, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
// Different keys should have different IDs
assert.NotEqual(t, rk1.Metadata.ID, rk2.Metadata.ID)
assert.NotEqual(t, rk1.Key, rk2.Key)
}
// Test ParseRootKey
func TestParseRootKey_Valid(t *testing.T) {
original, privPEM, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
parsed, err := ParseRootKey(privPEM)
require.NoError(t, err)
assert.NotNil(t, parsed)
// Verify the parsed key matches the original
assert.Equal(t, original.Key, parsed.Key)
assert.Equal(t, original.Metadata.ID, parsed.Metadata.ID)
assert.Equal(t, original.Metadata.CreatedAt.Unix(), parsed.Metadata.CreatedAt.Unix())
assert.Equal(t, original.Metadata.ExpiresAt.Unix(), parsed.Metadata.ExpiresAt.Unix())
}
func TestParseRootKey_InvalidPEM(t *testing.T) {
_, err := ParseRootKey([]byte("not a valid PEM"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse")
}
func TestParseRootKey_EmptyData(t *testing.T) {
_, err := ParseRootKey([]byte{})
assert.Error(t, err)
}
func TestParseRootKey_WrongType(t *testing.T) {
// Generate an artifact key instead of root key
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
artifactKey, privPEM, _, _, err := GenerateArtifactKey(rootKey, 30*24*time.Hour)
require.NoError(t, err)
// Try to parse artifact key as root key
_, err = ParseRootKey(privPEM)
assert.Error(t, err)
assert.Contains(t, err.Error(), "PEM type")
// Just to use artifactKey to avoid unused variable warning
_ = artifactKey
}
func TestParseRootKey_CorruptedJSON(t *testing.T) {
// Create PEM with corrupted JSON
corruptedPEM := pem.EncodeToMemory(&pem.Block{
Type: tagRootPrivate,
Bytes: []byte("corrupted json data"),
})
_, err := ParseRootKey(corruptedPEM)
assert.Error(t, err)
}
func TestParseRootKey_InvalidKeySize(t *testing.T) {
// Create a key with invalid size
invalidKey := PrivateKey{
Key: []byte{0x01, 0x02, 0x03}, // Too short
Metadata: KeyMetadata{
ID: KeyID{},
CreatedAt: time.Now().UTC(),
},
}
privJSON, err := json.Marshal(invalidKey)
require.NoError(t, err)
invalidPEM := pem.EncodeToMemory(&pem.Block{
Type: tagRootPrivate,
Bytes: privJSON,
})
_, err = ParseRootKey(invalidPEM)
assert.Error(t, err)
assert.Contains(t, err.Error(), "incorrect Ed25519 private key size")
}
func TestParseRootKey_Roundtrip(t *testing.T) {
// Generate a key
original, privPEM, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
// Parse it
parsed, err := ParseRootKey(privPEM)
require.NoError(t, err)
// Generate PEM again from parsed key
privJSON2, err := json.Marshal(parsed.PrivateKey)
require.NoError(t, err)
privPEM2 := pem.EncodeToMemory(&pem.Block{
Type: tagRootPrivate,
Bytes: privJSON2,
})
// Parse again
parsed2, err := ParseRootKey(privPEM2)
require.NoError(t, err)
// Should still match original
assert.Equal(t, original.Key, parsed2.Key)
assert.Equal(t, original.Metadata.ID, parsed2.Metadata.ID)
}
// Test SignArtifactKey
func TestSignArtifactKey_Valid(t *testing.T) {
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
data := []byte("test data to sign")
sigData, err := SignArtifactKey(*rootKey, data)
require.NoError(t, err)
assert.NotEmpty(t, sigData)
// Parse and verify signature
sig, err := ParseSignature(sigData)
require.NoError(t, err)
assert.NotEmpty(t, sig.Signature)
assert.Equal(t, rootKey.Metadata.ID, sig.KeyID)
assert.Equal(t, "ed25519", sig.Algorithm)
assert.Equal(t, "sha512", sig.HashAlgo)
assert.False(t, sig.Timestamp.IsZero())
}
func TestSignArtifactKey_EmptyData(t *testing.T) {
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
sigData, err := SignArtifactKey(*rootKey, []byte{})
require.NoError(t, err)
assert.NotEmpty(t, sigData)
// Should still be able to parse
sig, err := ParseSignature(sigData)
require.NoError(t, err)
assert.NotEmpty(t, sig.Signature)
}
func TestSignArtifactKey_Verify(t *testing.T) {
rootKey, _, pubPEM, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
// Parse public key
pubKey, _, err := parsePublicKey(pubPEM, tagRootPublic)
require.NoError(t, err)
// Sign some data
data := []byte("test data for verification")
sigData, err := SignArtifactKey(*rootKey, data)
require.NoError(t, err)
// Parse signature
sig, err := ParseSignature(sigData)
require.NoError(t, err)
// Reconstruct message
msg := make([]byte, 0, len(data)+8)
msg = append(msg, data...)
msg = binary.LittleEndian.AppendUint64(msg, uint64(sig.Timestamp.Unix()))
// Verify signature
valid := ed25519.Verify(pubKey.Key, msg, sig.Signature)
assert.True(t, valid)
}
func TestSignArtifactKey_DifferentData(t *testing.T) {
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
data1 := []byte("data1")
data2 := []byte("data2")
sig1, err := SignArtifactKey(*rootKey, data1)
require.NoError(t, err)
sig2, err := SignArtifactKey(*rootKey, data2)
require.NoError(t, err)
// Different data should produce different signatures
assert.NotEqual(t, sig1, sig2)
}
func TestSignArtifactKey_MultipleSignatures(t *testing.T) {
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
data := []byte("test data")
// Sign twice with a small delay
sig1, err := SignArtifactKey(*rootKey, data)
require.NoError(t, err)
time.Sleep(10 * time.Millisecond)
sig2, err := SignArtifactKey(*rootKey, data)
require.NoError(t, err)
// Signatures should be different due to different timestamps
assert.NotEqual(t, sig1, sig2)
// Parse both signatures
parsed1, err := ParseSignature(sig1)
require.NoError(t, err)
parsed2, err := ParseSignature(sig2)
require.NoError(t, err)
// Timestamps should be different
assert.True(t, parsed2.Timestamp.After(parsed1.Timestamp))
}
func TestSignArtifactKey_LargeData(t *testing.T) {
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
// Create 1MB of data
largeData := make([]byte, 1024*1024)
for i := range largeData {
largeData[i] = byte(i % 256)
}
sigData, err := SignArtifactKey(*rootKey, largeData)
require.NoError(t, err)
assert.NotEmpty(t, sigData)
// Verify signature can be parsed
sig, err := ParseSignature(sigData)
require.NoError(t, err)
assert.NotEmpty(t, sig.Signature)
}
func TestSignArtifactKey_TimestampInSignature(t *testing.T) {
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
beforeSign := time.Now().UTC()
data := []byte("test data")
sigData, err := SignArtifactKey(*rootKey, data)
require.NoError(t, err)
afterSign := time.Now().UTC()
sig, err := ParseSignature(sigData)
require.NoError(t, err)
// Timestamp should be between before and after
assert.True(t, sig.Timestamp.After(beforeSign.Add(-time.Second)))
assert.True(t, sig.Timestamp.Before(afterSign.Add(time.Second)))
}
// Integration test
func TestRootKey_FullWorkflow(t *testing.T) {
// Step 1: Generate root key
rootKey, privPEM, pubPEM, err := GenerateRootKey(10 * 365 * 24 * time.Hour)
require.NoError(t, err)
assert.NotNil(t, rootKey)
assert.NotEmpty(t, privPEM)
assert.NotEmpty(t, pubPEM)
// Step 2: Parse the private key back
parsedRootKey, err := ParseRootKey(privPEM)
require.NoError(t, err)
assert.Equal(t, rootKey.Key, parsedRootKey.Key)
assert.Equal(t, rootKey.Metadata.ID, parsedRootKey.Metadata.ID)
// Step 3: Generate an artifact key using root key
artifactKey, _, artifactPubPEM, artifactSig, err := GenerateArtifactKey(rootKey, 30*24*time.Hour)
require.NoError(t, err)
assert.NotNil(t, artifactKey)
// Step 4: Verify the artifact key signature
pubKey, _, err := parsePublicKey(pubPEM, tagRootPublic)
require.NoError(t, err)
sig, err := ParseSignature(artifactSig)
require.NoError(t, err)
artifactPubKey, _, err := parsePublicKey(artifactPubPEM, tagArtifactPublic)
require.NoError(t, err)
// Reconstruct message - SignArtifactKey signs the PEM, not the JSON
msg := make([]byte, 0, len(artifactPubPEM)+8)
msg = append(msg, artifactPubPEM...)
msg = binary.LittleEndian.AppendUint64(msg, uint64(sig.Timestamp.Unix()))
// Verify with root public key
valid := ed25519.Verify(pubKey.Key, msg, sig.Signature)
assert.True(t, valid, "Artifact key signature should be valid")
// Step 5: Use artifact key to sign data
testData := []byte("This is test artifact data")
dataSig, err := SignData(*artifactKey, testData)
require.NoError(t, err)
assert.NotEmpty(t, dataSig)
// Step 6: Verify the artifact data signature
dataSigParsed, err := ParseSignature(dataSig)
require.NoError(t, err)
err = ValidateArtifact([]PublicKey{artifactPubKey}, testData, *dataSigParsed)
assert.NoError(t, err, "Artifact data signature should be valid")
}
func TestRootKey_ExpiredKeyWorkflow(t *testing.T) {
// Generate a root key that expires very soon
rootKey, _, _, err := GenerateRootKey(1 * time.Millisecond)
require.NoError(t, err)
// Wait for expiration
time.Sleep(10 * time.Millisecond)
// Try to generate artifact key with expired root key
_, _, _, _, err = GenerateArtifactKey(rootKey, 30*24*time.Hour)
assert.Error(t, err)
assert.Contains(t, err.Error(), "expired")
}