From 8b20c2efc156d4a98478d7cb5e5a313b87b50bfa Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 29 May 2026 22:11:30 +0200 Subject: [PATCH] Test bootstrap signing flag validation Covers the validation matrix of the new --gpg-* / --ssh-signing-* surface: mutual exclusion (across GPG/SSH groups and within the SSH group between --ssh-signing-key-file and --ssh-signing-reuse-private- key), alias resolution between --ssh-signing-password and --ssh-signing-passphrase, the dependency checks (--ssh-signing- password requires --ssh-signing-key-file; --ssh-signing-reuse- private-key requires --private-key-file), and pre-flight key-parse failures (malformed PEM, encrypted SSH key without passphrase, GPG ring with wrong passphrase). Test keys are checked in so the test does not depend on local ssh-keygen or gpg invocations at run time. Signed-off-by: Hidde Beydals --- cmd/flux/bootstrap_test.go | 155 ++++++++++++++++++ .../bootstrap/ed25519-encrypted.private | 8 + cmd/flux/testdata/bootstrap/ed25519.private | 7 + cmd/flux/testdata/bootstrap/gpg-encrypted.pgp | Bin 0 -> 313 bytes cmd/flux/testdata/bootstrap/gpg.pgp | Bin 0 -> 261 bytes cmd/flux/testdata/bootstrap/malformed.private | 1 + 6 files changed, 171 insertions(+) create mode 100644 cmd/flux/bootstrap_test.go create mode 100644 cmd/flux/testdata/bootstrap/ed25519-encrypted.private create mode 100644 cmd/flux/testdata/bootstrap/ed25519.private create mode 100644 cmd/flux/testdata/bootstrap/gpg-encrypted.pgp create mode 100644 cmd/flux/testdata/bootstrap/gpg.pgp create mode 100644 cmd/flux/testdata/bootstrap/malformed.private diff --git a/cmd/flux/bootstrap_test.go b/cmd/flux/bootstrap_test.go new file mode 100644 index 00000000..8686d3e8 --- /dev/null +++ b/cmd/flux/bootstrap_test.go @@ -0,0 +1,155 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "strings" + "testing" +) + +func TestBootstrapValidate_signingFlags(t *testing.T) { + tests := []struct { + name string + gpgRing string + gpgPass string + sshKey string + sshPass string + sshPassp string + privateKey string + reuse bool + wantErr string + }{ + {name: "no signing flags is valid"}, + {name: "GPG only is valid", gpgRing: "./testdata/bootstrap/gpg.pgp"}, + {name: "SSH only is valid", sshKey: "./testdata/bootstrap/ed25519.private"}, + { + name: "Reuse-private-key with private-key-file is valid", + privateKey: "./testdata/bootstrap/ed25519.private", + reuse: true, + }, + { + name: "GPG + SSH errors", + gpgRing: "./testdata/bootstrap/gpg.pgp", + sshKey: "./testdata/bootstrap/ed25519.private", + wantErr: "--gpg-* and --ssh-signing-* are mutually exclusive", + }, + { + name: "GPG + reuse errors", + gpgRing: "./testdata/bootstrap/gpg.pgp", + privateKey: "./testdata/bootstrap/ed25519.private", + reuse: true, + wantErr: "--gpg-* and --ssh-signing-* are mutually exclusive", + }, + { + name: "SSH key-file + reuse errors", + sshKey: "./testdata/bootstrap/ed25519.private", + privateKey: "./testdata/bootstrap/ed25519.private", + reuse: true, + wantErr: "--ssh-signing-key-file and --ssh-signing-reuse-private-key are mutually exclusive", + }, + { + name: "Reuse without private-key-file errors", + reuse: true, + wantErr: "--ssh-signing-reuse-private-key requires --private-key-file", + }, + { + name: "SSH password without key errors", + sshPass: "secret", + wantErr: "--ssh-signing-password requires --ssh-signing-key-file", + }, + { + name: "SSH passphrase alias alone applies", + sshKey: "./testdata/bootstrap/ed25519-encrypted.private", + sshPassp: "abcde12345", + }, + { + name: "SSH password and passphrase with same value passes", + sshKey: "./testdata/bootstrap/ed25519-encrypted.private", + sshPass: "abcde12345", + sshPassp: "abcde12345", + }, + { + name: "SSH password and passphrase with different values errors", + sshKey: "./testdata/bootstrap/ed25519-encrypted.private", + sshPass: "right", + sshPassp: "wrong", + wantErr: "are aliases; do not pass both", + }, + { + name: "SSH malformed key fails pre-flight", + sshKey: "./testdata/bootstrap/malformed.private", + wantErr: "invalid SSH signing key", + }, + { + name: "SSH encrypted key without password fails pre-flight", + sshKey: "./testdata/bootstrap/ed25519-encrypted.private", + wantErr: "passphrase required", + }, + // The GPG fixture used here is encrypted (passphrase: "right") so that + // passing the wrong passphrase exercises the Decrypt error path. + // An unencrypted key would make Decrypt a no-op regardless of the + // passphrase supplied. + { + name: "GPG with wrong passphrase fails pre-flight", + gpgRing: "./testdata/bootstrap/gpg-encrypted.pgp", + gpgPass: "wrong", + wantErr: "invalid GPG signing key", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savedGpgRing := bootstrapArgs.gpgKeyRingPath + savedGpgPass := bootstrapArgs.gpgPassphrase + savedSshKey := bootstrapArgs.sshSigningKeyFile + savedSshPass := bootstrapArgs.sshSigningPassword + savedSshPassp := bootstrapArgs.sshSigningPassphrase + savedPrivKey := bootstrapArgs.privateKeyFile + savedReuse := bootstrapArgs.sshSigningReusePrivateKey + defer func() { + bootstrapArgs.gpgKeyRingPath = savedGpgRing + bootstrapArgs.gpgPassphrase = savedGpgPass + bootstrapArgs.sshSigningKeyFile = savedSshKey + bootstrapArgs.sshSigningPassword = savedSshPass + bootstrapArgs.sshSigningPassphrase = savedSshPassp + bootstrapArgs.privateKeyFile = savedPrivKey + bootstrapArgs.sshSigningReusePrivateKey = savedReuse + }() + + bootstrapArgs.gpgKeyRingPath = tt.gpgRing + bootstrapArgs.gpgPassphrase = tt.gpgPass + bootstrapArgs.sshSigningKeyFile = tt.sshKey + bootstrapArgs.sshSigningPassword = tt.sshPass + bootstrapArgs.sshSigningPassphrase = tt.sshPassp + bootstrapArgs.privateKeyFile = tt.privateKey + bootstrapArgs.sshSigningReusePrivateKey = tt.reuse + + err := bootstrapValidate() + if tt.wantErr == "" { + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + return + } + if err == nil { + t.Fatalf("expected error containing %q, got nil", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got: %v", tt.wantErr, err) + } + }) + } +} diff --git a/cmd/flux/testdata/bootstrap/ed25519-encrypted.private b/cmd/flux/testdata/bootstrap/ed25519-encrypted.private new file mode 100644 index 00000000..6a540db3 --- /dev/null +++ b/cmd/flux/testdata/bootstrap/ed25519-encrypted.private @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDuUiEMA0 +eUvKlmOsur2w9FAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIDF/w86ZQb5qmZtv +m1GvyLojiJdhmPtI9hJ9XPcP7HBoAAAAkG2cIOuSVdWInSC0P81ExiorUpiAGOjxxpgvKW +VYERfU1zU72Z/c9n1+z/IH5cJOhZ1vlqBO0rubl4s0KQFvY/LKcsc4N0x0uzpqrvcJP4tO +9VW8LrMnrPp7b6KVJPsbeSW1SBcUM24aCMzF4/wV03mN/Uqz30s+YgS9SU4Lz8AOkX58xX +yAV0gkmndIzZl+Og== +-----END OPENSSH PRIVATE KEY----- diff --git a/cmd/flux/testdata/bootstrap/ed25519.private b/cmd/flux/testdata/bootstrap/ed25519.private new file mode 100644 index 00000000..2cc37a05 --- /dev/null +++ b/cmd/flux/testdata/bootstrap/ed25519.private @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAWDldtCFdSMXIV1vLwXvRwk4eEmSoDCpxNkcbNph3dCAAAAIjjSDmx40g5 +sQAAAAtzc2gtZWQyNTUxOQAAACAWDldtCFdSMXIV1vLwXvRwk4eEmSoDCpxNkcbNph3dCA +AAAEAGpzSFuLkCNDD49+tysxSFFwdOsRnDj67vDT9bfwoSDhYOV20IV1IxchXW8vBe9HCT +h4SZKgMKnE2Rxs2mHd0IAAAABHRlc3QB +-----END OPENSSH PRIVATE KEY----- diff --git a/cmd/flux/testdata/bootstrap/gpg-encrypted.pgp b/cmd/flux/testdata/bootstrap/gpg-encrypted.pgp new file mode 100644 index 0000000000000000000000000000000000000000..aff2f539fe5a10b75af1df672054ae6b8a1ff36d GIT binary patch literal 313 zcmbQz#*!sDd5IXOHX9=g<1Kf7Mn-mrkJ{<^QyY7>N~YeN+Kmi-L5 G<^=$RCw-~_ literal 0 HcmV?d00001 diff --git a/cmd/flux/testdata/bootstrap/gpg.pgp b/cmd/flux/testdata/bootstrap/gpg.pgp new file mode 100644 index 0000000000000000000000000000000000000000..e435898c06c3027d499c650255c8d22c63c9768b GIT binary patch literal 261 zcmbOd!IC98xlfE!n~jl$@s>M3BO|-Rvbn3j%DVi`YyaWDIAGTy8|GUHpN_3~^Q3U; z*MxAs+C&D1|8711%`e&Tg|DcX;%{>EVY>zAyW74G4O{H~_-$CuX?9NF^cL}w)Z!8a z8xZM`T9KGrkdvyHoS$pgF@Z%`jEliSOp!&a^`L!nQeOS5jeiwBf1Uear)UYmr>B^pqGmTYxMPt Hh1=Kx1KnrV literal 0 HcmV?d00001 diff --git a/cmd/flux/testdata/bootstrap/malformed.private b/cmd/flux/testdata/bootstrap/malformed.private new file mode 100644 index 00000000..850bf46f --- /dev/null +++ b/cmd/flux/testdata/bootstrap/malformed.private @@ -0,0 +1 @@ +not a real ssh key