213 lines
6.6 KiB
Go
213 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const testAPIToken = "ely_test_token"
|
|
|
|
type fakeSecretTerminal struct {
|
|
interactive bool
|
|
answers []string
|
|
prompts []string
|
|
}
|
|
|
|
func (terminal *fakeSecretTerminal) Interactive() bool {
|
|
return terminal.interactive
|
|
}
|
|
|
|
func (terminal *fakeSecretTerminal) ReadHidden(prompt string) (string, error) {
|
|
terminal.prompts = append(terminal.prompts, prompt)
|
|
if len(terminal.answers) == 0 {
|
|
return "", os.ErrInvalid
|
|
}
|
|
answer := terminal.answers[0]
|
|
terminal.answers = terminal.answers[1:]
|
|
return answer, nil
|
|
}
|
|
|
|
func testPrivateKey() string {
|
|
return base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{7}, 32))
|
|
}
|
|
|
|
func writeSecretFile(t *testing.T, path, value string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(value), 0600); err != nil {
|
|
t.Fatalf("write secret file: %v", err)
|
|
}
|
|
if runtime.GOOS != "windows" {
|
|
if err := os.Chmod(path, 0600); err != nil {
|
|
t.Fatalf("restrict secret file: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveInstallSecretsUsesHiddenPrompts(t *testing.T) {
|
|
terminal := &fakeSecretTerminal{
|
|
interactive: true,
|
|
answers: []string{testPrivateKey(), testAPIToken},
|
|
}
|
|
secrets, err := resolveInstallSecrets(installSecretSources{}, terminal)
|
|
if err != nil {
|
|
t.Fatalf("resolve install secrets: %v", err)
|
|
}
|
|
if secrets.privateKey != testPrivateKey() || secrets.token != testAPIToken {
|
|
t.Fatalf("resolved secrets have unexpected values")
|
|
}
|
|
wantPrompts := []string{"Private key: ", "API token (optional): "}
|
|
if strings.Join(terminal.prompts, "|") != strings.Join(wantPrompts, "|") {
|
|
t.Fatalf("prompts = %q, want %q", terminal.prompts, wantPrompts)
|
|
}
|
|
}
|
|
|
|
func TestResolveInstallSecretsRequiresFileWhenNoninteractive(t *testing.T) {
|
|
terminal := &fakeSecretTerminal{interactive: false}
|
|
_, err := resolveInstallSecrets(installSecretSources{}, terminal)
|
|
if err == nil || !strings.Contains(err.Error(), "--private-key-file <path>") {
|
|
t.Fatalf("resolve install secrets error = %v", err)
|
|
}
|
|
if len(terminal.prompts) != 0 {
|
|
t.Fatalf("unexpected prompts: %q", terminal.prompts)
|
|
}
|
|
}
|
|
|
|
func TestResolveInstallSecretsReadsCredentialFiles(t *testing.T) {
|
|
directory := t.TempDir()
|
|
privateKeyPath := filepath.Join(directory, "private.key")
|
|
tokenPath := filepath.Join(directory, "token")
|
|
writeSecretFile(t, privateKeyPath, testPrivateKey()+"\r\n")
|
|
writeSecretFile(t, tokenPath, testAPIToken+"\n")
|
|
|
|
secrets, err := resolveInstallSecrets(installSecretSources{
|
|
privateKeyFile: privateKeyPath,
|
|
tokenFile: tokenPath,
|
|
}, &fakeSecretTerminal{})
|
|
if err != nil {
|
|
t.Fatalf("resolve install secrets: %v", err)
|
|
}
|
|
if secrets.privateKey != testPrivateKey() || secrets.token != testAPIToken {
|
|
t.Fatalf("resolved secrets have unexpected values")
|
|
}
|
|
}
|
|
|
|
func TestReadSecretFileRejectsInvalidContent(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
pattern string
|
|
}{
|
|
{name: "empty", content: "", pattern: "is empty"},
|
|
{name: "multiple lines", content: testPrivateKey() + "\nsecond\n", pattern: "one line"},
|
|
{name: "nul", content: "key\x00value", pattern: "one line"},
|
|
{name: "invalid UTF-8", content: string([]byte{0xff}), pattern: "UTF-8"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "private.key")
|
|
writeSecretFile(t, path, test.content)
|
|
_, err := readSecretFile(path, "private key")
|
|
if err == nil || !strings.Contains(err.Error(), test.pattern) {
|
|
t.Fatalf("read secret file error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReadSecretFileRejectsOversizedContent(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "private.key")
|
|
writeSecretFile(t, path, strings.Repeat("a", maxSecretFileBytes+1))
|
|
_, err := readSecretFile(path, "private key")
|
|
if err == nil || !strings.Contains(err.Error(), "exceeds") {
|
|
t.Fatalf("read secret file error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReadSecretFileRejectsSymbolicLink(t *testing.T) {
|
|
directory := t.TempDir()
|
|
target := filepath.Join(directory, "target.key")
|
|
link := filepath.Join(directory, "linked.key")
|
|
writeSecretFile(t, target, testPrivateKey())
|
|
if err := os.Symlink(target, link); err != nil {
|
|
t.Skipf("file symbolic links unavailable: %v", err)
|
|
}
|
|
_, err := readSecretFile(link, "private key")
|
|
if err == nil || !strings.Contains(err.Error(), "regular file") {
|
|
t.Fatalf("read secret file error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReadSecretFileRequiresOwnerOnlyPermissions(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("POSIX permission contract")
|
|
}
|
|
path := filepath.Join(t.TempDir(), "private.key")
|
|
writeSecretFile(t, path, testPrivateKey())
|
|
if err := os.Chmod(path, 0640); err != nil {
|
|
t.Fatalf("widen secret file permissions: %v", err)
|
|
}
|
|
_, err := readSecretFile(path, "private key")
|
|
if err == nil || !strings.Contains(err.Error(), "only by its owner") {
|
|
t.Fatalf("read secret file error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReadSecretFileDetectsReplacementWhileOpening(t *testing.T) {
|
|
directory := t.TempDir()
|
|
path := filepath.Join(directory, "private.key")
|
|
replacement := filepath.Join(directory, "replacement.key")
|
|
writeSecretFile(t, path, testPrivateKey())
|
|
writeSecretFile(t, replacement, testPrivateKey())
|
|
|
|
_, err := readSecretFileWithOpen(path, "private key", func(name string) (*os.File, error) {
|
|
if removeErr := os.Remove(path); removeErr != nil {
|
|
return nil, removeErr
|
|
}
|
|
if renameErr := os.Rename(replacement, path); renameErr != nil {
|
|
return nil, renameErr
|
|
}
|
|
return os.Open(name)
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "changed while opening") {
|
|
t.Fatalf("read secret file error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidatePrivateKeyRejectsInvalidSeeds(t *testing.T) {
|
|
tests := []string{"invalid!", base64.RawURLEncoding.EncodeToString([]byte("short"))}
|
|
for _, privateKey := range tests {
|
|
if err := validatePrivateKey(privateKey); err == nil {
|
|
t.Fatalf("validatePrivateKey(%q) succeeded", privateKey)
|
|
}
|
|
}
|
|
if err := validatePrivateKey(testPrivateKey()); err != nil {
|
|
t.Fatalf("validate private key: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRejectLegacySecretArgumentsRedactsValues(t *testing.T) {
|
|
tests := [][]string{
|
|
{"--private-key", "must-not-appear"},
|
|
{"--private-key=must-not-appear"},
|
|
{"--token", "must-not-appear"},
|
|
{"--token=must-not-appear"},
|
|
}
|
|
for _, arguments := range tests {
|
|
err := rejectLegacySecretArguments(arguments)
|
|
if err == nil {
|
|
t.Fatalf("rejectLegacySecretArguments(%q) succeeded", arguments)
|
|
}
|
|
if strings.Contains(err.Error(), "must-not-appear") {
|
|
t.Fatalf("legacy secret value leaked in error: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "-file") {
|
|
t.Fatalf("replacement option missing from error: %v", err)
|
|
}
|
|
}
|
|
}
|