defenseunicorns / defenseunicorns/uds-cli

`uds create --signing-key` cannot sign unattended when the cosign key has an empty passphrase

Open
#1,434 0 comments 0 reactions 0 assignees View on GitHub
possible-bug
Dominant language
Go
Stars
54
Forks
21
Avg merge
2d 15h
Merged PRs (30d)
45

Description

### Environment

Device and OS:
App version: `uds` v0.34.3 (behaviour also present in earlier releases)
Kubernetes distro being used: N/A — reproduces at build time, no cluster involved
Other: cosign key pair generated with an empty passphrase (`cosign generate-key-pair` + empty prompt). Running non-interactively from a semantic-release `prepareCmd` in CI.

### Steps to reproduce

1. Generate a cosign key pair with an **empty** passphrase:
```bash
COSIGN_PASSWORD='' cosign generate-key-pair
```
2. From a directory containing a valid `uds-bundle.yaml`, run a fully non-interactive create:
```bash
uds create . \
--signing-key ./cosign.key \
--signing-key-password '' \
--confirm --no-color --no-progress
```
3. Repeat with each of the following, all of which fail the same way:
- `COSIGN_PASSWORD='' uds create . --signing-key ./cosign.key --confirm`
- omitting `--signing-key-password` entirely
- piping input: `uds create . --signing-key ./cosign.key --confirm < /dev/null`

### Expected result

With `--confirm` set (and/or an explicitly-provided empty `--signing-key-password`, and/or `COSIGN_PASSWORD` exported), `uds create` signs the bundle without prompting, and exits non-zero on failure rather than blocking. Bundles signed with an empty-passphrase key should be producible in an unattended pipeline.

### Actual Result

`uds create` blocks on an interactive prompt:

```
? Private key password (empty for no password):
```

On a runner with a TTY this hangs until the job times out. Without a TTY, `survey` errors out instead. Either way there is **no** flag, env var, or config value that lets you get past it, because an empty passphrase is not expressible.

### Visual Proof (screenshots, videos, text, etc)

The relevant code is in [`[src/pkg/bundle/create.go#L75-L86](https://github.com/defenseunicorns/uds-cli/blob/v0.34.3/src/pkg/bundle/create.go#L75-L86)`](https://github.com/defenseunicorns/uds-cli/blob/v0.34.3/src/pkg/bundle/create.go#L75-L86):

```go
getSigCreatePassword := func(_ bool) ([]byte, error) {
if b.cfg.CreateOpts.SigningKeyPassword != "" {
return []byte(b.cfg.CreateOpts.SigningKeyPassword), nil
}
return interactive.PromptSigPassword()
}

// sign the bundle
signBlobOptions := signing.DefaultSignBlobOptions()
signBlobOptions.OutputSignature = filepath.Join(b.tmp, config.BundleYAMLSignature)
signBlobOptions.PassFunc = getSigCreatePassword
signBlobOptions.Key = b.cfg.CreateOpts.SigningKeyPath
```

Three separate consequences, all from these lines:

1. **An empty passphrase is unrepresentable.** `SigningKeyPassword` is a plain `string` and the guard is `!= ""`, so "the user explicitly passed an empty password" and "the user passed nothing" collapse into the same case. An empty passphrase is valid for cosign, so this is a genuine gap rather than a missing feature.
2. **`COSIGN_PASSWORD` is ignored.** Assigning `signBlobOptions.PassFunc` overrides cosign's own env-aware pass function, which would otherwise honour `COSIGN_PASSWORD` (cosign uses `os.LookupEnv`, so it handles the empty-string case correctly).
3. **`--confirm` is not consulted here.** [`[interactive.PromptSigPassword()](https://github.com/defenseunicorns/uds-cli/blob/v0.34.3/src/pkg/interactive/interactive.go#L12-L23)`](https://github.com/defenseunicorns/uds-cli/blob/v0.34.3/src/pkg/interactive/interactive.go#L12-L23) uses `survey.Password`, which requires a TTY. Zarf's equivalent path returns early when confirm is set; UDS CLI does not, so `--confirm` does not imply non-interactive here even though that is what it implies everywhere else in the command.

For comparison, Zarf exposes `zarf package sign ... --signing-key-pass`, so a package can be signed after the fact. UDS CLI has no `uds sign` command, and signing happens mid-`create` (the signature is written to the temp dir and pushed as an OCI layer by the bundler), so there is no supported way to sign a bundle out of band either.

### Severity/Priority

Medium. It does not affect deployed bundles, but it blocks automated signed-bundle releases for anyone using an empty-passphrase key, and the failure mode in CI is a hang rather than a clear error. There is a workaround (re-key with a non-empty passphrase), but it is not discoverable from the CLI output or the docs — the prompt explicitly advertises "empty for no password", which is exactly the case that cannot be automated.

### Additional Context

Possible fixes, roughly in increasing order of completeness:

- **Honour `--confirm`.** Return `nil, nil` (or `[]byte{}, nil`) instead of prompting when `config.CommonOptions.Confirm` is set. Smallest change; makes `--confirm` mean the same thing here as elsewhere.
- **Fall back to cosign's own pass function.** Leave `signBlobOptions.PassFunc` unset when no password flag was supplied, so `COSIGN_PASSWORD` works as users coming from cosign would expect.
- **Make "empty" expressible.** Use `cmd.Flags().Changed("signing-key-password")` rather than a `!= ""` comparison, so an explicit `--signing-key-password ''` is respected.
- **Add `--signing-key-password-file`.** Avoids passphrases in process args / CI logs, and sidesteps the empty-string ambiguity entirely. Useful independent of this bug.
- **Optionally, add a `uds sign` command** mirroring `zarf package sign`, for the post-creation signing case.

Happy to open a PR for whichever direction maintainers prefer.

Contributor guide

Open the contributing guide

Research direction

Start in src/pkg/bundle/create.go at the signing password callback and compare its behavior with src/pkg/interactive/interactive.go and the documented --confirm path. Verify how an explicitly empty password and COSIGN_PASSWORD are distinguished, then exercise unattended uds create signing with an empty-passphrase key; done means it does not prompt and exits non-zero on signing failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
cli, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.