Files
kubesolo-os/update/pkg/signing/signing_test.go
Adolfo Delorenzo 49a37e30e8 feat: add production hardening — Ed25519 signing, Portainer Edge, SSH extension (Phase 4)
Image signing:
- Ed25519 sign/verify package (pure Go stdlib, zero deps)
- genkey and sign CLI subcommands for build system
- Optional --pubkey flag for verifying updates on apply
- Signature URLs in update metadata (latest.json)

Portainer Edge Agent:
- cloud-init portainer.go module writes K8s manifest
- Auto-deploys Edge Agent when portainer.edge-agent.enabled
- Full RBAC (ServiceAccount, ClusterRoleBinding, Deployment)
- 5 Portainer tests in portainer_test.go

Production tooling:
- SSH debug extension builder (hack/build-ssh-extension.sh)
- Boot performance benchmark (test/benchmark/bench-boot.sh)
- Resource usage benchmark (test/benchmark/bench-resources.sh)
- Deployment guide (docs/deployment-guide.md)

Test results: 50 update agent tests + 22 cloud-init tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 11:26:23 -06:00

335 lines
7.3 KiB
Go

package signing
import (
"crypto/ed25519"
"encoding/hex"
"os"
"path/filepath"
"testing"
)
func generateTestKeyPair(t *testing.T) (string, string) {
t.Helper()
pub, priv, err := GenerateKeyPair()
if err != nil {
t.Fatal(err)
}
return pub, priv
}
func TestGenerateKeyPair(t *testing.T) {
pub, priv, err := GenerateKeyPair()
if err != nil {
t.Fatal(err)
}
// Public key should be 32 bytes = 64 hex chars
if len(pub) != 64 {
t.Errorf("expected 64 hex chars for public key, got %d", len(pub))
}
// Private key should be 64 bytes = 128 hex chars
if len(priv) != 128 {
t.Errorf("expected 128 hex chars for private key, got %d", len(priv))
}
// Keys should be valid hex
if _, err := hex.DecodeString(pub); err != nil {
t.Errorf("public key is not valid hex: %v", err)
}
if _, err := hex.DecodeString(priv); err != nil {
t.Errorf("private key is not valid hex: %v", err)
}
}
func TestNewVerifier(t *testing.T) {
pub, _ := generateTestKeyPair(t)
v, err := NewVerifier(pub)
if err != nil {
t.Fatal(err)
}
if v == nil {
t.Fatal("verifier should not be nil")
}
}
func TestNewVerifierInvalid(t *testing.T) {
// Invalid hex
_, err := NewVerifier("not-hex")
if err == nil {
t.Fatal("expected error for invalid hex")
}
// Wrong length
_, err = NewVerifier("abcd")
if err == nil {
t.Fatal("expected error for wrong key length")
}
}
func TestNewSigner(t *testing.T) {
_, priv := generateTestKeyPair(t)
s, err := NewSigner(priv)
if err != nil {
t.Fatal(err)
}
if s == nil {
t.Fatal("signer should not be nil")
}
}
func TestSignAndVerifyBytes(t *testing.T) {
pub, priv := generateTestKeyPair(t)
signer, err := NewSigner(priv)
if err != nil {
t.Fatal(err)
}
verifier, err := NewVerifier(pub)
if err != nil {
t.Fatal(err)
}
message := []byte("KubeSolo OS update v1.0.0")
sig := signer.SignBytes(message)
// Verify should succeed
if err := verifier.VerifyBytes(message, sig); err != nil {
t.Errorf("verification should succeed: %v", err)
}
// Tampered message should fail
tampered := []byte("KubeSolo OS update v1.0.1")
if err := verifier.VerifyBytes(tampered, sig); err == nil {
t.Error("verification should fail for tampered message")
}
// Tampered signature should fail
badSig := make([]byte, len(sig))
copy(badSig, sig)
badSig[0] ^= 0xff
if err := verifier.VerifyBytes(message, badSig); err == nil {
t.Error("verification should fail for tampered signature")
}
}
func TestSignAndVerifyFile(t *testing.T) {
pub, priv := generateTestKeyPair(t)
dir := t.TempDir()
signer, err := NewSigner(priv)
if err != nil {
t.Fatal(err)
}
verifier, err := NewVerifier(pub)
if err != nil {
t.Fatal(err)
}
// Create a test file
filePath := filepath.Join(dir, "test-image.gz")
content := []byte("fake OS image content for signing test")
if err := os.WriteFile(filePath, content, 0o644); err != nil {
t.Fatal(err)
}
// Sign
sigPath := filePath + ".sig"
if err := signer.SignFile(filePath, sigPath); err != nil {
t.Fatal(err)
}
// Verify signature file was created
if _, err := os.Stat(sigPath); err != nil {
t.Fatalf("signature file not created: %v", err)
}
// Verify
if err := verifier.VerifyFile(filePath, sigPath); err != nil {
t.Errorf("verification should succeed: %v", err)
}
}
func TestVerifyFileTampered(t *testing.T) {
pub, priv := generateTestKeyPair(t)
dir := t.TempDir()
signer, err := NewSigner(priv)
if err != nil {
t.Fatal(err)
}
verifier, err := NewVerifier(pub)
if err != nil {
t.Fatal(err)
}
// Create and sign a file
filePath := filepath.Join(dir, "test-image.gz")
if err := os.WriteFile(filePath, []byte("original content"), 0o644); err != nil {
t.Fatal(err)
}
sigPath := filePath + ".sig"
if err := signer.SignFile(filePath, sigPath); err != nil {
t.Fatal(err)
}
// Tamper with the file
if err := os.WriteFile(filePath, []byte("tampered content"), 0o644); err != nil {
t.Fatal(err)
}
// Verification should fail
if err := verifier.VerifyFile(filePath, sigPath); err == nil {
t.Error("verification should fail for tampered file")
}
}
func TestVerifyFileWrongKey(t *testing.T) {
_, priv := generateTestKeyPair(t)
otherPub, _ := generateTestKeyPair(t) // different key pair
dir := t.TempDir()
signer, err := NewSigner(priv)
if err != nil {
t.Fatal(err)
}
wrongVerifier, err := NewVerifier(otherPub)
if err != nil {
t.Fatal(err)
}
// Create and sign
filePath := filepath.Join(dir, "test.gz")
if err := os.WriteFile(filePath, []byte("test content"), 0o644); err != nil {
t.Fatal(err)
}
sigPath := filePath + ".sig"
if err := signer.SignFile(filePath, sigPath); err != nil {
t.Fatal(err)
}
// Verify with wrong key should fail
if err := wrongVerifier.VerifyFile(filePath, sigPath); err == nil {
t.Error("verification should fail with wrong public key")
}
}
func TestNewVerifierFromFile(t *testing.T) {
pub, _ := generateTestKeyPair(t)
dir := t.TempDir()
keyFile := filepath.Join(dir, "pubkey.hex")
if err := os.WriteFile(keyFile, []byte(pub+"\n"), 0o644); err != nil {
t.Fatal(err)
}
v, err := NewVerifierFromFile(keyFile)
if err != nil {
t.Fatal(err)
}
if v == nil {
t.Fatal("verifier should not be nil")
}
}
func TestNewSignerFromFile(t *testing.T) {
_, priv := generateTestKeyPair(t)
dir := t.TempDir()
keyFile := filepath.Join(dir, "privkey.hex")
if err := os.WriteFile(keyFile, []byte(priv+"\n"), 0o644); err != nil {
t.Fatal(err)
}
s, err := NewSignerFromFile(keyFile)
if err != nil {
t.Fatal(err)
}
if s == nil {
t.Fatal("signer should not be nil")
}
}
func TestSignerPublicKeyHex(t *testing.T) {
pub, priv := generateTestKeyPair(t)
signer, err := NewSigner(priv)
if err != nil {
t.Fatal(err)
}
got := signer.PublicKeyHex()
if got != pub {
t.Errorf("public key mismatch: got %s, want %s", got, pub)
}
}
func TestDecodeSignature(t *testing.T) {
// Create a valid signature
_, priv, _ := ed25519.GenerateKey(nil)
message := []byte("test")
rawSig := ed25519.Sign(priv, message)
hexSig := hex.EncodeToString(rawSig)
// Raw signature (64 bytes)
decoded, err := decodeSignature(rawSig)
if err != nil {
t.Fatalf("raw sig decode failed: %v", err)
}
if len(decoded) != ed25519.SignatureSize {
t.Errorf("expected %d bytes, got %d", ed25519.SignatureSize, len(decoded))
}
// Hex-encoded signature
decoded, err = decodeSignature([]byte(hexSig))
if err != nil {
t.Fatalf("hex sig decode failed: %v", err)
}
if len(decoded) != ed25519.SignatureSize {
t.Errorf("expected %d bytes, got %d", ed25519.SignatureSize, len(decoded))
}
// Hex with trailing newline
decoded, err = decodeSignature([]byte(hexSig + "\n"))
if err != nil {
t.Fatalf("hex sig with newline decode failed: %v", err)
}
if len(decoded) != ed25519.SignatureSize {
t.Errorf("expected %d bytes, got %d", ed25519.SignatureSize, len(decoded))
}
// Invalid data
_, err = decodeSignature([]byte("not valid"))
if err == nil {
t.Error("expected error for invalid signature data")
}
}
func TestTrimWhitespace(t *testing.T) {
tests := []struct {
input string
expect string
}{
{"hello", "hello"},
{" hello ", "hello"},
{"hello\n", "hello"},
{"\thello\r\n", "hello"},
{" he llo ", "hello"},
}
for _, tt := range tests {
got := trimWhitespace(tt.input)
if got != tt.expect {
t.Errorf("trimWhitespace(%q) = %q, want %q", tt.input, got, tt.expect)
}
}
}