Skip to content

Commit

Permalink
Add (skopeo generate-sigstore-key)
Browse files Browse the repository at this point in the history
Signed-off-by: Miloslav Trmač <[email protected]>
  • Loading branch information
mtrmac committed Jan 23, 2023
1 parent 981c2fc commit 552e376
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ Please read the [contribution guide](CONTRIBUTING.md) if you want to collaborate
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------|
| [skopeo-copy(1)](/docs/skopeo-copy.1.md) | Copy an image (manifest, filesystem layers, signatures) from one location to another. |
| [skopeo-delete(1)](/docs/skopeo-delete.1.md) | Mark the image-name for later deletion by the registry's garbage collector. |
| [skopeo-generate-sigstore-key(1)](/docs/skopeo-generate-sigstore-key.1.md) | Generate a sigstore public/private key pair. |
| [skopeo-inspect(1)](/docs/skopeo-inspect.1.md) | Return low-level information about image-name in a registry. |
| [skopeo-list-tags(1)](/docs/skopeo-list-tags.1.md) | Return a list of tags for the transport-specific image repository. |
| [skopeo-login(1)](/docs/skopeo-login.1.md) | Login to a container registry. |
Expand Down
90 changes: 90 additions & 0 deletions cmd/skopeo/generate_sigstore_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"errors"
"fmt"
"io"
"io/fs"
"os"

"github.com/containers/image/v5/pkg/cli"
"github.com/containers/image/v5/signature/sigstore"
"github.com/spf13/cobra"
)

type generateSigstoreKeyOptions struct {
outputPrefix string
passphraseFile string
}

func generateSigstoreKeyCmd() *cobra.Command {
var opts generateSigstoreKeyOptions
cmd := &cobra.Command{
Use: "generate-sigstore-key --output-prefix PREFIX",
Short: "Generate a sigstore public/private key pair",
RunE: commandAction(opts.run),
Example: "skopeo generate-sigstore-key --output-prefix my-key",
}
adjustUsage(cmd)
flags := cmd.Flags()
flags.StringVar(&opts.outputPrefix, "output-prefix", "", "Write the keys to `PREFIX`.pub and `PREFIX`.private")
flags.StringVar(&opts.passphraseFile, "passphrase-file", "", "Read a passphrase for the private key from `PATH`")
return cmd
}

// ensurePathDoesNotExist verifies that path does not refer to an existing file,
// and returns an error if so.
func ensurePathDoesNotExist(path string) error {
switch _, err := os.Stat(path); {
case err == nil:
return fmt.Errorf("Refusing to overwrite existing %q", path)
case errors.Is(err, fs.ErrNotExist):
return nil
default:
return fmt.Errorf("Error checking existence of %q: %w", path, err)
}
}

func (opts *generateSigstoreKeyOptions) run(args []string, stdout io.Writer) error {
if len(args) != 0 || opts.outputPrefix == "" {
return errors.New("Usage: generate-sigstore-key --output-prefix PREFIX")
}

pubKeyPath := opts.outputPrefix + ".pub"
privateKeyPath := opts.outputPrefix + ".private"
if err := ensurePathDoesNotExist(pubKeyPath); err != nil {
return err
}
if err := ensurePathDoesNotExist(privateKeyPath); err != nil {
return err
}

var passphrase string
if opts.passphraseFile != "" {
p, err := cli.ReadPassphraseFile(opts.passphraseFile)
if err != nil {
return err
}
passphrase = p
} else {
p, err := promptForPassphrase(privateKeyPath, os.Stdin, os.Stdout)
if err != nil {
return err
}
passphrase = p
}

keys, err := sigstore.GenerateKeyPair([]byte(passphrase))
if err != nil {
return fmt.Errorf("Error generating key pair: %w", err)
}

if err := os.WriteFile(privateKeyPath, keys.PrivateKey, 0600); err != nil {
return fmt.Errorf("Error writing private key to %q: %w", privateKeyPath, err)
}
if err := os.WriteFile(pubKeyPath, keys.PublicKey, 0644); err != nil {
return fmt.Errorf("Error writing private key to %q: %w", pubKeyPath, err)
}
fmt.Fprintf(stdout, "Key written to %q and %q", privateKeyPath, pubKeyPath)
return nil
}
102 changes: 102 additions & 0 deletions cmd/skopeo/generate_sigstore_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGenerateSigstoreKey(t *testing.T) {
// Invalid command-line arguments
for _, args := range [][]string{
{},
{"--output-prefix", "foo", "a1"},
} {
out, err := runSkopeo(append([]string{"generate-sigstore-key"}, args...)...)
assertTestFailed(t, out, err, "Usage")
}

// One of the destination files already exists
outputSuffixes := []string{".pub", ".private"}
for _, suffix := range outputSuffixes {
dir := t.TempDir()
prefix := filepath.Join(dir, "prefix")
err := os.WriteFile(prefix+suffix, []byte{}, 0600)
require.NoError(t, err)
out, err := runSkopeo("generate-sigstore-key",
"--output-prefix", prefix, "--passphrase-file", "/dev/null",
)
assertTestFailed(t, out, err, "Refusing to overwrite")
}

// One of the destinations is inaccessible (simulate by a symlink to an inaccessible
// directory)
for _, suffix := range outputSuffixes {
dir := t.TempDir()
unaccessible := filepath.Join(dir, "unaccessible")
err := os.Mkdir(unaccessible, 0000)
require.NoError(t, err)
t.Cleanup(func() {
err := os.Chmod(unaccessible, 0700)
require.NoError(t, err)
err = os.Remove(unaccessible)
require.NoError(t, err)
})
prefix := filepath.Join(dir, "prefix")
err = os.Symlink(filepath.Join(unaccessible, "unaccessible"), prefix+suffix)
require.NoError(t, err)
out, err := runSkopeo("generate-sigstore-key",
"--output-prefix", prefix, "--passphrase-file", "/dev/null",
)
assertTestFailed(t, out, err, prefix+suffix) // + an OS-specific error message
}
destDir := t.TempDir()
// Error reading passphrase
out, err := runSkopeo("generate-sigstore-key",
"--output-prefix", filepath.Join(destDir, "prefix"),
"--passphrase-file", filepath.Join(destDir, "this-does-not-exist"),
)
assertTestFailed(t, out, err, "this-does-not-exist")

// (The interactive passphrase prompting is not yet tested)

// Error writing one of the outputs: an unmodifiable directory
for _, suffix := range outputSuffixes {
dir := t.TempDir()
unwriteable := filepath.Join(dir, "subdir")
err := os.Mkdir(unwriteable, 0500)
require.NoError(t, err)
t.Cleanup(func() {
err := os.Chmod(unwriteable, 0700)
require.NoError(t, err)
err = os.Remove(unwriteable)
require.NoError(t, err)
})
prefix := filepath.Join(dir, "prefix")
err = os.Symlink(filepath.Join(unwriteable, "unwriteable"), prefix+suffix)
require.NoError(t, err)
out, err := runSkopeo("generate-sigstore-key",
"--output-prefix", prefix, "--passphrase-file", "/dev/null",
)
assertTestFailed(t, out, err, "Error writing")
}

// Success
// Just a smoke-test, useability of the keys is tested in the generate implementation.
dir := t.TempDir()
prefix := filepath.Join(dir, "prefix")
passphraseFile := filepath.Join(dir, "passphrase")
err = os.WriteFile(passphraseFile, []byte("some passphrase"), 0600)
require.NoError(t, err)
out, err = runSkopeo("generate-sigstore-key",
"--output-prefix", prefix, "--passphrase-file", passphraseFile,
)
assert.NoError(t, err)
for _, suffix := range outputSuffixes {
assert.Contains(t, out, prefix+suffix)
}

}
1 change: 1 addition & 0 deletions cmd/skopeo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func createApp() (*cobra.Command, *globalOptions) {
rootCommand.AddCommand(
copyCmd(&opts),
deleteCmd(&opts),
generateSigstoreKeyCmd(),
inspectCmd(&opts),
layersCmd(&opts),
loginCmd(&opts),
Expand Down
43 changes: 43 additions & 0 deletions docs/skopeo-generate-sigstore-key.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
% skopeo-generate-sigstore-key(1)

## NAME
skopeo\-generate-sigstore-key - Generate a sigstore public/private key pair.

## SYNOPSIS
**skopeo generate-sigstore-key** [*options*] **--output-prefix** _prefix_

## DESCRIPTION

Generates a public/private key pair suitable for creating sigstore image signatures.
The private key is encrypted with a passphrase;
if one is not provided using an option, this command prompts for it interactively.

The private key is written to _prefix_**.private** .
The private key is written to _prefix_**.pub** .

## OPTIONS

**--output=prefix** _prefix_

Mandatory.
Path prefix for the output keys (_prefix_**.private** and _prefix_**.pub**).

**--passphrase-file** _path_

The passphare to use to encrypt the private key.
Only the first line will be read.
A passphrase stored in a file is of questionable security if other users can read this file.
Do not use this option if at all avoidable.

## EXAMPLES

```sh
$ skopeo generate-sigstore-key --output-prefix mykey
```

# SEE ALSO
skopeo(1), skopeo-copy(1), containers-policy.json(5)

## AUTHORS

Miloslav Trmač <[email protected]>
1 change: 1 addition & 0 deletions docs/skopeo.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Print the version number
| ----------------------------------------- | ------------------------------------------------------------------------------ |
| [skopeo-copy(1)](skopeo-copy.1.md) | Copy an image (manifest, filesystem layers, signatures) from one location to another. |
| [skopeo-delete(1)](skopeo-delete.1.md) | Mark the _image-name_ for later deletion by the registry's garbage collector. |
| [skopeo-generate-sigstore-key(1)](skopeo-generate-sigstore-key.1.md) | Generate a sigstore public/private key pair. |
| [skopeo-inspect(1)](skopeo-inspect.1.md) | Return low-level information about _image-name_ in a registry. |
| [skopeo-list-tags(1)](skopeo-list-tags.1.md) | List image names in a transport-specific collection of images.|
| [skopeo-login(1)](skopeo-login.1.md) | Login to a container registry. |
Expand Down

0 comments on commit 552e376

Please sign in to comment.