Description
Cosign command types such as verify.VerifyBlobCmd can be called from Go programs, but callers cannot redirect their UI output for a single invocation. Successful blob verification calls ui.Infof(ctx, "Verified OK"); without a Cosign UI environment in the context, that function writes to os.Stderr.
The default is appropriate for the cosign executable. In an application that calls VerifyBlobCmd.Exec, however, the message bypasses the application's logger, formatting, log-level controls, and configured destination.
Applications may use the command layer to stay aligned with Cosign's option validation and verification behavior:
cmd := &verify.VerifyBlobCmd{
// Cosign verification options
}
if err := cmd.Exec(ctx, blobPath); err != nil {
return err
}
A successful verification can therefore introduce an unformatted line among the application's own logs:
2026-08-04 10:10:05 INF processing artifact
Verified OK
2026-08-04 10:10:09 INF processing complete
Cosign already supports per-context streams internally through ui.Env and ui.WithEnv. Go prevents external consumers from importing internal/ui, and Cobra's SetErr does not affect output written through that package.
Could Cosign provide a public, supported way for command-layer consumers to supply stdin and stderr streams for one invocation?
Possible solutions
-
Expose a narrow public context helper over the existing UI environment. This appears to be the smallest and most consistent option because command execution already passes a context through the UI calls. The public API could accept streams without exposing the rest of internal/ui:
type IOStreams struct {
Stdin io.Reader
Stderr io.Writer
}
func WithIOStreams(ctx context.Context, streams IOStreams) context.Context
A caller could then configure the invocation before calling Exec:
ctx = cli.WithIOStreams(ctx, cli.IOStreams{
Stdin: input,
Stderr: logWriter,
})
err := cmd.Exec(ctx, blobPath)
The public function could continue delegating to internal/ui.WithEnv, leaving the internal UI implementation private.
-
Add shared streams to command options. An exported IOStreams value could be embedded in command structs or common command options. VerifyBlobCmd.Exec would use those streams when creating its internal UI context. This is more discoverable on the command itself, but each command that emits UI output would need to adopt the option.
Promoting all of internal/ui to a public package would also make Env and WithEnv usable, but it would expose prompt and logging implementation details that callers do not need. A narrow stream API would provide the required control with a smaller public compatibility surface.
In either design, unset streams should retain the current os.Stdin and os.Stderr defaults. Streams must remain scoped to one invocation so concurrent commands can use different destinations without replacing process-global file handles. Supplying streams must not change verification behavior or return values.
Relevant implementation:
VerifyBlobCmd new-bundle success output:
|
_, err = cosign.VerifyNewBundle(ctx, co, artifactPolicyOption, bundle) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
ui.Infof(ctx, "Verified OK") |
|
return nil |
VerifyBlobCmd legacy success output:
|
if _, err = cosign.VerifyBlobSignature(ctx, signature, co); err != nil { |
|
return err |
|
} |
|
|
|
ui.Infof(ctx, "Verified OK") |
|
return nil |
- Internal UI environment and context functions:
|
type Env struct { |
|
Stderr io.Writer |
|
Stdin io.Reader |
|
} |
|
|
|
// defaultEnv returns the default environment (writing to os.Stderr and |
|
// reading from os.Stdin). |
|
func defaultEnv() *Env { |
|
return &Env{ |
|
Stderr: os.Stderr, |
|
Stdin: os.Stdin, |
|
} |
|
} |
|
|
|
type ctxKey struct{} |
|
|
|
func (c ctxKey) String() string { |
|
return "cosign/ui:env" |
|
} |
|
|
|
var ctxKeyEnv = ctxKey{} |
|
|
|
// getEnv gets the environment from ctx. |
|
// |
|
// If ctx does not contain an environment, getEnv returns the default |
|
// environment (see defaultEnvironment). |
|
func getEnv(ctx context.Context) *Env { |
|
e, ok := ctx.Value(ctxKeyEnv).(*Env) |
|
if !ok { |
|
return defaultEnv() |
|
} |
|
return e |
|
} |
|
|
|
// WithEnv adds the environment to the context. |
|
func WithEnv(ctx context.Context, e *Env) context.Context { |
|
return context.WithValue(ctx, ctxKeyEnv, e) |
|
} |
Description
Cosign command types such as
verify.VerifyBlobCmdcan be called from Go programs, but callers cannot redirect their UI output for a single invocation. Successful blob verification callsui.Infof(ctx, "Verified OK"); without a Cosign UI environment in the context, that function writes toos.Stderr.The default is appropriate for the
cosignexecutable. In an application that callsVerifyBlobCmd.Exec, however, the message bypasses the application's logger, formatting, log-level controls, and configured destination.Applications may use the command layer to stay aligned with Cosign's option validation and verification behavior:
A successful verification can therefore introduce an unformatted line among the application's own logs:
Cosign already supports per-context streams internally through
ui.Envandui.WithEnv. Go prevents external consumers from importinginternal/ui, and Cobra'sSetErrdoes not affect output written through that package.Could Cosign provide a public, supported way for command-layer consumers to supply stdin and stderr streams for one invocation?
Possible solutions
Expose a narrow public context helper over the existing UI environment. This appears to be the smallest and most consistent option because command execution already passes a context through the UI calls. The public API could accept streams without exposing the rest of
internal/ui:A caller could then configure the invocation before calling
Exec:The public function could continue delegating to
internal/ui.WithEnv, leaving the internal UI implementation private.Add shared streams to command options. An exported
IOStreamsvalue could be embedded in command structs or common command options.VerifyBlobCmd.Execwould use those streams when creating its internal UI context. This is more discoverable on the command itself, but each command that emits UI output would need to adopt the option.Promoting all of
internal/uito a public package would also makeEnvandWithEnvusable, but it would expose prompt and logging implementation details that callers do not need. A narrow stream API would provide the required control with a smaller public compatibility surface.In either design, unset streams should retain the current
os.Stdinandos.Stderrdefaults. Streams must remain scoped to one invocation so concurrent commands can use different destinations without replacing process-global file handles. Supplying streams must not change verification behavior or return values.Relevant implementation:
VerifyBlobCmdnew-bundle success output:cosign/cmd/cosign/cli/verify/verify_blob.go
Lines 160 to 166 in 83d9ec8
VerifyBlobCmdlegacy success output:cosign/cmd/cosign/cli/verify/verify_blob.go
Lines 289 to 294 in 83d9ec8
cosign/internal/ui/env.go
Lines 32 to 69 in 83d9ec8