Files
netbird/client/internal/updater/reposign/verify_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

529 lines
16 KiB
Go

package reposign
import (
"context"
"crypto/ed25519"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Test ArtifactVerify construction
func TestArtifactVerify_Construction(t *testing.T) {
// Generate test root key
rootKey, _, rootPubPEM, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
rootPubKey, _, err := parsePublicKey(rootPubPEM, tagRootPublic)
require.NoError(t, err)
keysBaseURL := "http://localhost:8080/artifact-signatures"
av, err := newArtifactVerify(keysBaseURL, []PublicKey{rootPubKey})
require.NoError(t, err)
assert.NotNil(t, av)
assert.NotEmpty(t, av.rootKeys)
assert.Equal(t, keysBaseURL, av.keysBaseURL.String())
// Verify root key structure
assert.NotEmpty(t, av.rootKeys[0].Key)
assert.Equal(t, rootKey.Metadata.ID, av.rootKeys[0].Metadata.ID)
assert.False(t, av.rootKeys[0].Metadata.CreatedAt.IsZero())
}
func TestArtifactVerify_MultipleRootKeys(t *testing.T) {
// Generate multiple test root keys
rootKey1, _, rootPubPEM1, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
rootPubKey1, _, err := parsePublicKey(rootPubPEM1, tagRootPublic)
require.NoError(t, err)
rootKey2, _, rootPubPEM2, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
rootPubKey2, _, err := parsePublicKey(rootPubPEM2, tagRootPublic)
require.NoError(t, err)
keysBaseURL := "http://localhost:8080/artifact-signatures"
av, err := newArtifactVerify(keysBaseURL, []PublicKey{rootPubKey1, rootPubKey2})
assert.NoError(t, err)
assert.Len(t, av.rootKeys, 2)
assert.NotEqual(t, rootKey1.Metadata.ID, rootKey2.Metadata.ID)
}
// Test Verify workflow with mock HTTP server
func TestArtifactVerify_FullWorkflow(t *testing.T) {
// Create temporary test directory
tempDir := t.TempDir()
// Step 1: Generate root key
rootKey, _, _, err := GenerateRootKey(10 * 365 * 24 * time.Hour)
require.NoError(t, err)
// Step 2: Generate artifact key
artifactKey, _, artifactPubPEM, _, err := GenerateArtifactKey(rootKey, 30*24*time.Hour)
require.NoError(t, err)
artifactPubKey, err := ParseArtifactPubKey(artifactPubPEM)
require.NoError(t, err)
// Step 3: Create revocation list
revocationData, revocationSig, err := CreateRevocationList(*rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
// Step 4: Bundle artifact keys
artifactKeysBundle, artifactKeysSig, err := BundleArtifactKeys(rootKey, []PublicKey{artifactPubKey})
require.NoError(t, err)
// Step 5: Create test artifact
artifactPath := filepath.Join(tempDir, "test-artifact.bin")
artifactData := []byte("This is test artifact data for verification")
err = os.WriteFile(artifactPath, artifactData, 0644)
require.NoError(t, err)
// Step 6: Sign artifact
artifactSigData, err := SignData(*artifactKey, artifactData)
require.NoError(t, err)
// Step 7: Setup mock HTTP server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/artifact-signatures/keys/" + revocationFileName:
_, _ = w.Write(revocationData)
case "/artifact-signatures/keys/" + revocationSignFileName:
_, _ = w.Write(revocationSig)
case "/artifact-signatures/keys/" + artifactPubKeysFileName:
_, _ = w.Write(artifactKeysBundle)
case "/artifact-signatures/keys/" + artifactPubKeysSigFileName:
_, _ = w.Write(artifactKeysSig)
case "/artifacts/v1.0.0/test-artifact.bin":
_, _ = w.Write(artifactData)
case "/artifact-signatures/tag/v1.0.0/test-artifact.bin.sig":
_, _ = w.Write(artifactSigData)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
// Step 8: Create ArtifactVerify with test root key
rootPubKey := PublicKey{
Key: rootKey.Key.Public().(ed25519.PublicKey),
Metadata: rootKey.Metadata,
}
av, err := newArtifactVerify(server.URL+"/artifact-signatures", []PublicKey{rootPubKey})
require.NoError(t, err)
// Step 9: Verify artifact
ctx := context.Background()
err = av.Verify(ctx, "1.0.0", artifactPath)
assert.NoError(t, err)
}
func TestArtifactVerify_InvalidRevocationList(t *testing.T) {
tempDir := t.TempDir()
artifactPath := filepath.Join(tempDir, "test.bin")
err := os.WriteFile(artifactPath, []byte("test"), 0644)
require.NoError(t, err)
// Setup server with invalid revocation list
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/artifact-signatures/keys/" + revocationFileName:
_, _ = w.Write([]byte("invalid data"))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
rootPubKey := PublicKey{
Key: rootKey.Key.Public().(ed25519.PublicKey),
Metadata: rootKey.Metadata,
}
av, err := newArtifactVerify(server.URL+"/artifact-signatures", []PublicKey{rootPubKey})
require.NoError(t, err)
ctx := context.Background()
err = av.Verify(ctx, "1.0.0", artifactPath)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to load revocation list")
}
func TestArtifactVerify_MissingArtifactFile(t *testing.T) {
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
rootPubKey := PublicKey{
Key: rootKey.Key.Public().(ed25519.PublicKey),
Metadata: rootKey.Metadata,
}
// Create revocation list
revocationData, revocationSig, err := CreateRevocationList(*rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
artifactKey, _, artifactPubPEM, _, err := GenerateArtifactKey(rootKey, 30*24*time.Hour)
require.NoError(t, err)
artifactPubKey, err := ParseArtifactPubKey(artifactPubPEM)
require.NoError(t, err)
artifactKeysBundle, artifactKeysSig, err := BundleArtifactKeys(rootKey, []PublicKey{artifactPubKey})
require.NoError(t, err)
// Create signature for non-existent file
testData := []byte("test")
artifactSigData, err := SignData(*artifactKey, testData)
require.NoError(t, err)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/artifact-signatures/keys/" + revocationFileName:
_, _ = w.Write(revocationData)
case "/artifact-signatures/keys/" + revocationSignFileName:
_, _ = w.Write(revocationSig)
case "/artifact-signatures/keys/" + artifactPubKeysFileName:
_, _ = w.Write(artifactKeysBundle)
case "/artifact-signatures/keys/" + artifactPubKeysSigFileName:
_, _ = w.Write(artifactKeysSig)
case "/artifact-signatures/tag/v1.0.0/missing.bin.sig":
_, _ = w.Write(artifactSigData)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
av, err := newArtifactVerify(server.URL+"/artifact-signatures", []PublicKey{rootPubKey})
require.NoError(t, err)
ctx := context.Background()
err = av.Verify(ctx, "1.0.0", "file.bin")
assert.Error(t, err)
}
func TestArtifactVerify_ServerUnavailable(t *testing.T) {
tempDir := t.TempDir()
artifactPath := filepath.Join(tempDir, "test.bin")
err := os.WriteFile(artifactPath, []byte("test"), 0644)
require.NoError(t, err)
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
rootPubKey := PublicKey{
Key: rootKey.Key.Public().(ed25519.PublicKey),
Metadata: rootKey.Metadata,
}
// Use URL that doesn't exist
av, err := newArtifactVerify("http://localhost:19999/keys", []PublicKey{rootPubKey})
require.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
err = av.Verify(ctx, "1.0.0", artifactPath)
assert.Error(t, err)
}
func TestArtifactVerify_ContextCancellation(t *testing.T) {
tempDir := t.TempDir()
artifactPath := filepath.Join(tempDir, "test.bin")
err := os.WriteFile(artifactPath, []byte("test"), 0644)
require.NoError(t, err)
// Create a server that delays response
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(500 * time.Millisecond)
_, _ = w.Write([]byte("data"))
}))
defer server.Close()
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
require.NoError(t, err)
rootPubKey := PublicKey{
Key: rootKey.Key.Public().(ed25519.PublicKey),
Metadata: rootKey.Metadata,
}
av, err := newArtifactVerify(server.URL, []PublicKey{rootPubKey})
require.NoError(t, err)
// Create context that cancels quickly
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
err = av.Verify(ctx, "1.0.0", artifactPath)
assert.Error(t, err)
}
func TestArtifactVerify_WithRevocation(t *testing.T) {
tempDir := t.TempDir()
// Generate root key
rootKey, _, _, err := GenerateRootKey(10 * 365 * 24 * time.Hour)
require.NoError(t, err)
// Generate two artifact keys
artifactKey1, _, artifactPubPEM1, _, err := GenerateArtifactKey(rootKey, 30*24*time.Hour)
require.NoError(t, err)
artifactPubKey1, err := ParseArtifactPubKey(artifactPubPEM1)
require.NoError(t, err)
_, _, artifactPubPEM2, _, err := GenerateArtifactKey(rootKey, 30*24*time.Hour)
require.NoError(t, err)
artifactPubKey2, err := ParseArtifactPubKey(artifactPubPEM2)
require.NoError(t, err)
// Create revocation list with first key revoked
emptyRevocation, _, err := CreateRevocationList(*rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
parsedRevocation, err := ParseRevocationList(emptyRevocation)
require.NoError(t, err)
revocationData, revocationSig, err := ExtendRevocationList(*rootKey, *parsedRevocation, artifactPubKey1.Metadata.ID, defaultRevocationListExpiration)
require.NoError(t, err)
// Bundle both keys
artifactKeysBundle, artifactKeysSig, err := BundleArtifactKeys(rootKey, []PublicKey{artifactPubKey1, artifactPubKey2})
require.NoError(t, err)
// Create artifact signed by revoked key
artifactPath := filepath.Join(tempDir, "test.bin")
artifactData := []byte("test data")
err = os.WriteFile(artifactPath, artifactData, 0644)
require.NoError(t, err)
artifactSigData, err := SignData(*artifactKey1, artifactData)
require.NoError(t, err)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/artifact-signatures/keys/" + revocationFileName:
_, _ = w.Write(revocationData)
case "/artifact-signatures/keys/" + revocationSignFileName:
_, _ = w.Write(revocationSig)
case "/artifact-signatures/keys/" + artifactPubKeysFileName:
_, _ = w.Write(artifactKeysBundle)
case "/artifact-signatures/keys/" + artifactPubKeysSigFileName:
_, _ = w.Write(artifactKeysSig)
case "/artifact-signatures/tag/v1.0.0/test.bin.sig":
_, _ = w.Write(artifactSigData)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
rootPubKey := PublicKey{
Key: rootKey.Key.Public().(ed25519.PublicKey),
Metadata: rootKey.Metadata,
}
av, err := newArtifactVerify(server.URL+"/artifact-signatures", []PublicKey{rootPubKey})
require.NoError(t, err)
ctx := context.Background()
err = av.Verify(ctx, "1.0.0", artifactPath)
// Should fail because the signing key is revoked
assert.Error(t, err)
assert.Contains(t, err.Error(), "no signing Key found")
}
func TestArtifactVerify_ValidWithSecondKey(t *testing.T) {
tempDir := t.TempDir()
// Generate root key
rootKey, _, _, err := GenerateRootKey(10 * 365 * 24 * time.Hour)
require.NoError(t, err)
// Generate two artifact keys
_, _, artifactPubPEM1, _, err := GenerateArtifactKey(rootKey, 30*24*time.Hour)
require.NoError(t, err)
artifactPubKey1, err := ParseArtifactPubKey(artifactPubPEM1)
require.NoError(t, err)
artifactKey2, _, artifactPubPEM2, _, err := GenerateArtifactKey(rootKey, 30*24*time.Hour)
require.NoError(t, err)
artifactPubKey2, err := ParseArtifactPubKey(artifactPubPEM2)
require.NoError(t, err)
// Create revocation list with first key revoked
emptyRevocation, _, err := CreateRevocationList(*rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
parsedRevocation, err := ParseRevocationList(emptyRevocation)
require.NoError(t, err)
revocationData, revocationSig, err := ExtendRevocationList(*rootKey, *parsedRevocation, artifactPubKey1.Metadata.ID, defaultRevocationListExpiration)
require.NoError(t, err)
// Bundle both keys
artifactKeysBundle, artifactKeysSig, err := BundleArtifactKeys(rootKey, []PublicKey{artifactPubKey1, artifactPubKey2})
require.NoError(t, err)
// Create artifact signed by second key (not revoked)
artifactPath := filepath.Join(tempDir, "test.bin")
artifactData := []byte("test data")
err = os.WriteFile(artifactPath, artifactData, 0644)
require.NoError(t, err)
artifactSigData, err := SignData(*artifactKey2, artifactData)
require.NoError(t, err)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/artifact-signatures/keys/" + revocationFileName:
_, _ = w.Write(revocationData)
case "/artifact-signatures/keys/" + revocationSignFileName:
_, _ = w.Write(revocationSig)
case "/artifact-signatures/keys/" + artifactPubKeysFileName:
_, _ = w.Write(artifactKeysBundle)
case "/artifact-signatures/keys/" + artifactPubKeysSigFileName:
_, _ = w.Write(artifactKeysSig)
case "/artifact-signatures/tag/v1.0.0/test.bin.sig":
_, _ = w.Write(artifactSigData)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
rootPubKey := PublicKey{
Key: rootKey.Key.Public().(ed25519.PublicKey),
Metadata: rootKey.Metadata,
}
av, err := newArtifactVerify(server.URL+"/artifact-signatures", []PublicKey{rootPubKey})
require.NoError(t, err)
ctx := context.Background()
err = av.Verify(ctx, "1.0.0", artifactPath)
// Should succeed because second key is not revoked
assert.NoError(t, err)
}
func TestArtifactVerify_TamperedArtifact(t *testing.T) {
tempDir := t.TempDir()
// Generate root key and artifact key
rootKey, _, _, err := GenerateRootKey(10 * 365 * 24 * time.Hour)
require.NoError(t, err)
artifactKey, _, artifactPubPEM, _, err := GenerateArtifactKey(rootKey, 30*24*time.Hour)
require.NoError(t, err)
artifactPubKey, err := ParseArtifactPubKey(artifactPubPEM)
require.NoError(t, err)
// Create revocation list
revocationData, revocationSig, err := CreateRevocationList(*rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
// Bundle keys
artifactKeysBundle, artifactKeysSig, err := BundleArtifactKeys(rootKey, []PublicKey{artifactPubKey})
require.NoError(t, err)
// Sign original data
originalData := []byte("original data")
artifactSigData, err := SignData(*artifactKey, originalData)
require.NoError(t, err)
// Write tampered data to file
artifactPath := filepath.Join(tempDir, "test.bin")
tamperedData := []byte("tampered data")
err = os.WriteFile(artifactPath, tamperedData, 0644)
require.NoError(t, err)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/artifact-signatures/keys/" + revocationFileName:
_, _ = w.Write(revocationData)
case "/artifact-signatures/keys/" + revocationSignFileName:
_, _ = w.Write(revocationSig)
case "/artifact-signatures/keys/" + artifactPubKeysFileName:
_, _ = w.Write(artifactKeysBundle)
case "/artifact-signatures/keys/" + artifactPubKeysSigFileName:
_, _ = w.Write(artifactKeysSig)
case "/artifact-signatures/tag/v1.0.0/test.bin.sig":
_, _ = w.Write(artifactSigData)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
rootPubKey := PublicKey{
Key: rootKey.Key.Public().(ed25519.PublicKey),
Metadata: rootKey.Metadata,
}
av, err := newArtifactVerify(server.URL+"/artifact-signatures", []PublicKey{rootPubKey})
require.NoError(t, err)
ctx := context.Background()
err = av.Verify(ctx, "1.0.0", artifactPath)
// Should fail because artifact was tampered
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to validate artifact")
}
// Test URL validation
func TestArtifactVerify_URLParsing(t *testing.T) {
tests := []struct {
name string
keysBaseURL string
expectError bool
}{
{
name: "Valid HTTP URL",
keysBaseURL: "http://example.com/artifact-signatures",
expectError: false,
},
{
name: "Valid HTTPS URL",
keysBaseURL: "https://example.com/artifact-signatures",
expectError: false,
},
{
name: "URL with port",
keysBaseURL: "http://localhost:8080/artifact-signatures",
expectError: false,
},
{
name: "Invalid URL",
keysBaseURL: "://invalid",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := newArtifactVerify(tt.keysBaseURL, nil)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}