Add pkg/cli/sigstore

Introduce a "sigstore signing parameter file" that can carry all the required
configuration, so that we don't need to add 9 different CLI options.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
This commit is contained in:
Miloslav Trmač 2023-01-05 05:41:48 +01:00
parent 6320cfe1f5
commit 9e3177f44c
4 changed files with 310 additions and 1 deletions

View File

@ -0,0 +1,117 @@
% CONTAINERS-SIGSTORE-SIGNING-PARAMS.YAML 5 sigstore signing parameters Man Page
% Miloslav Trmač
% January 2023
# NAME
containers-sigstore-signing-params.yaml - syntax for the sigstore signing parameter file
# DESCRIPTION
Sigstore signing parameter files are used to store options that may be required to create sigstore signatures.
There is no default location for these files; they are user-managed, and used as inputs to a container image signing operation,
e.g. `skopeo copy --sign-by-sigstore=`_param-file_`.yaml` or `podman push --sign-by-sigstore=`_param-file_`.yaml` .
## FORMAT
Sigstore signing parameter files use YAML.
Many parameters are optional, but the file must specify enough to create a signature;
in particular either a private key, or Fulcio.
### Signing with Private Keys
- `privateKeyFile:` _path_
Create a signature using a private key at _path_.
Existence of this field triggers the use of a private key.
- `privateKeyPassphraseFile:` _passphrasePath_
Read the passphrase required to use `privateKeyFile` from _passphrasePath_.
Optional: if this is not set, the user must provide the passphrase interactively.
### Signing with Fulcio-generated Certificates
Instead of a static private key, the signing process generates a short-lived key pair
and requests a Fulcio server to issue a certificate for that key pair,
based on the user authenticating to an OpenID Connect provider.
To specify Fulcio, include a `fulcio` sub-object with one or more of the following keys.
In addition, a Rekor server must be specified as well.
- `fulcioURL:` _URL_
Required. URL of the Fulcio server to use.
- `oidcMode:` `interactive` | `deviceGrant` | `staticToken`
Required. Specifies how to obtain the necessary OpenID Connect credential.
`interactive` opens a web browser on the same machine, or if that is not possible,
asks the user to open a browser manually and to type in the provided code.
It requires the user to be able to directly interact with the signing process.
`deviceGrant` uses a device authorization grant flow (RFC 8628).
It requires the user to be able to read text printed by the signing process, and to act on it reasonably promptly.
`staticToken` provides a pre-existing OpenID Connect “ID token”, which must have been obtained separately.
- `oidcIssuerURL:` _URL_
Required for `oidcMode:` `interactive` or `deviceGrant`. URL of an OpenID Connect issuer server to authenticate with.
- `oidcClientID:` _client ID_
Used for `oidcMode:` `interactive` or `deviceGrant` to identify the client when contacting the issuer.
Optional but likely to be necessary in those cases.
- `oidcClientSecret:` _client secret_
Used for `oidcMode:` `interactive` or `deviceGrant` to authenticate the client when contacting the issuer.
Optional.
- `oidcIDToken:` _token_
Required for `oidcMode: staticToken`.
An OpenID Connect ID token that identifies the user (and authorizes certificate issuance).
### Recording the Signature to a Rekor Transparency Server
This can be combined with either a private key or Fulcio.
It is, pratically speaking, required for Fulcio; it is optional when a static private key is used, but necessary for
interoperability with the default configuration of `cosign`.
- `rekorURL`: _URL_
URL of the Rekor server to use.
# EXAMPLES
### Sign Using a Pre-existing Private Key
Uses the ”community infrastructure” Rekor server.
```yaml
privateKeyFile: "/home/user/sigstore/private-key.key"
privateKeyPassphraseFile: "/mnt/user/sigstore-private-key"
rekorURL: "https://rekor.sigstore.dev"
```
### Sign Using a Fulcio-Issued Certificate
Uses the ”community infrastructure” Fulcio and Rekor server,
and the Dex OIDC issuer which delegates to other major issuers like Google and GitHub.
Other configurations will very likely need to also provide an OIDC client secret.
```yaml
fulcio:
fulcioURL: "https://fulcio.sigstore.dev"
oidcMode: "interactive"
oidcIssuerURL: "https://oauth2.sigstore.dev/auth"
oidcClientID: "sigstore"
rekorURL: "https://rekor.sigstore.dev"
```
# SEE ALSO
skopeo(1), podman(1)

2
go.mod
View File

@ -42,6 +42,7 @@ require (
golang.org/x/oauth2 v0.4.0
golang.org/x/sync v0.1.0
golang.org/x/term v0.4.0
gopkg.in/yaml.v3 v3.0.1
)
require (
@ -122,5 +123,4 @@ require (
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -0,0 +1,75 @@
package params
import (
"bytes"
"fmt"
"os"
"gopkg.in/yaml.v3"
)
// SigningParameterFile collects parameters used for creating sigstore signatures.
//
// To consume such a file, most callers should use c/image/pkg/cli/sigstore instead
// of dealing with this type explicitly using ParseFile.
//
// This type is exported primarily to allow creating parameter files programmatically
// (and eventually this subpackage should provide an API to convert this type into
// the appropriate file contents, so that callers dont need to do that manually).
type SigningParameterFile struct {
// Keep this in sync with docs/containers-sigstore-signing-params.yaml.5.md !
PrivateKeyFile string `yaml:"privateKeyFile,omitempty"` // If set, sign using a private key stored in this file.
PrivateKeyPassphraseFile string `yaml:"privateKeyPassphraseFile,omitempty"` // A file that contains the passprase required for PrivateKeyFile.
Fulcio *SigningParameterFileFulcio `yaml:"fulcio,omitempty"` // If set, sign using a short-lived key and a Fulcio-issued certificate.
RekorURL string `yaml:"rekorURL,omitempty"` // If set, upload the signature to the specified Rekor server, and include a log inclusion proof in the signature.
}
// SigningParameterFileFulcio is a subset of SigningParameterFile dedicated to Fulcio parameters.
type SigningParameterFileFulcio struct {
// Keep this in sync with docs/containers-sigstore-signing-params.yaml.5.md !
FulcioURL string `yaml:"fulcioURL,omitempty"` // URL of the Fulcio server. Required.
// How to obtain the OIDC ID token required by Fulcio. Required.
OIDCMode OIDCMode `yaml:"oidcMode,omitempty"`
// oidcMode = staticToken
OIDCIDToken string `yaml:"oidcIDToken,omitempty"`
// oidcMode = deviceGrant || interactive
OIDCIssuerURL string `yaml:"oidcIssuerURL,omitempty"` //
OIDCClientID string `yaml:"oidcClientID,omitempty"`
OIDCClientSecret string `yaml:"oidcClientSecret,omitempty"`
}
type OIDCMode string
const (
// OIDCModeStaticToken means the parameter file contains an user-provided OIDC ID token value.
OIDCModeStaticToken OIDCMode = "staticToken"
// OIDCModeDeviceGrant specifies the OIDC ID token should be obtained using a device authorization grant (RFC 8628).
OIDCModeDeviceGrant OIDCMode = "deviceGrant"
// OIDCModeInteractive specifies the OIDC ID token should be obtained interactively (automatically opening a browser,
// or interactively prompting the user.)
OIDCModeInteractive OIDCMode = "interactive"
)
// ParseFile parses a SigningParameterFile at the specified path.
//
// Most consumers of the parameter file should use c/image/pkg/cli/sigstore to obtain a *signer.Signer instead.
func ParseFile(path string) (*SigningParameterFile, error) {
var res SigningParameterFile
source, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading %q: %w", path, err)
}
dec := yaml.NewDecoder(bytes.NewReader(source))
dec.KnownFields(true)
if err = dec.Decode(&res); err != nil {
return nil, fmt.Errorf("parsing %q: %w", path, err)
}
return &res, nil
}

View File

@ -0,0 +1,117 @@
package sigstore
import (
"errors"
"fmt"
"io"
"net/url"
"github.com/containers/image/v5/pkg/cli"
"github.com/containers/image/v5/pkg/cli/sigstore/params"
"github.com/containers/image/v5/signature/signer"
"github.com/containers/image/v5/signature/sigstore"
"github.com/containers/image/v5/signature/sigstore/fulcio"
"github.com/containers/image/v5/signature/sigstore/rekor"
)
// Options collects data that the caller should provide to NewSignerFromParameterFile.
// The caller should set all fields unless documented otherwise.
type Options struct {
PrivateKeyPassphrasePrompt func(keyFile string) (string, error) // A function to call to interactively prompt for a passphrase
Stdin io.Reader
Stdout io.Writer
}
// NewSignerFromParameterFile returns a signature.Signer which creates sigstore signatures based a parameter file at the specified path.
//
// The caller must call Close() on the returned Signer.
func NewSignerFromParameterFile(path string, options *Options) (*signer.Signer, error) {
params, err := params.ParseFile(path)
if err != nil {
return nil, fmt.Errorf("setting up signing using parameter file %q: %w", path, err)
}
return newSignerFromParameterData(params, options)
}
// newSignerFromParameterData returns a signature.Signer which creates sigstore signatures based on parameter file contents.
//
// The caller must call Close() on the returned Signer.
func newSignerFromParameterData(params *params.SigningParameterFile, options *Options) (*signer.Signer, error) {
opts := []sigstore.Option{}
if params.PrivateKeyFile != "" {
var getPassphrase func(keyFile string) (string, error)
switch {
case params.PrivateKeyPassphraseFile != "":
getPassphrase = func(_ string) (string, error) {
return cli.ReadPassphraseFile(params.PrivateKeyPassphraseFile)
}
case options.PrivateKeyPassphrasePrompt != nil:
getPassphrase = options.PrivateKeyPassphrasePrompt
default: // This shouldnt happen, the caller is expected to set options.PrivateKeyPassphrasePrompt
return nil, fmt.Errorf("private key %s specified, but no way to get a passphrase", params.PrivateKeyFile)
}
passphrase, err := getPassphrase(params.PrivateKeyFile)
if err != nil {
return nil, err
}
opts = append(opts, sigstore.WithPrivateKeyFile(params.PrivateKeyFile, []byte(passphrase)))
}
if params.Fulcio != nil {
fulcioOpt, err := fulcioOption(params.Fulcio, options)
if err != nil {
return nil, err
}
opts = append(opts, fulcioOpt)
}
if params.RekorURL != "" {
rekorURL, err := url.Parse(params.RekorURL)
if err != nil {
return nil, fmt.Errorf("parsing rekorURL %q: %w", params.RekorURL, err)
}
opts = append(opts, rekor.WithRekor(rekorURL))
}
return sigstore.NewSigner(opts...)
}
// fulcioOption returns a sigstore.Option for Fulcio use based on f.
func fulcioOption(f *params.SigningParameterFileFulcio, options *Options) (sigstore.Option, error) {
if f.FulcioURL == "" {
return nil, errors.New("missing fulcioURL")
}
fulcioURL, err := url.Parse(f.FulcioURL)
if err != nil {
return nil, fmt.Errorf("parsing fulcioURL %q: %w", f.FulcioURL, err)
}
if f.OIDCMode == params.OIDCModeStaticToken {
if f.OIDCIDToken == "" {
return nil, errors.New("missing oidcToken")
}
return fulcio.WithFulcioAndPreexistingOIDCIDToken(fulcioURL, f.OIDCIDToken), nil
}
if f.OIDCIssuerURL == "" {
return nil, errors.New("missing oidcIssuerURL")
}
oidcIssuerURL, err := url.Parse(f.OIDCIssuerURL)
if err != nil {
return nil, fmt.Errorf("parsing oidcIssuerURL %q: %w", f.OIDCIssuerURL, err)
}
switch f.OIDCMode {
case params.OIDCModeDeviceGrant:
return fulcio.WithFulcioAndDeviceAuthorizationGrantOIDC(fulcioURL, oidcIssuerURL, f.OIDCClientID, f.OIDCClientSecret,
options.Stdout), nil
case params.OIDCModeInteractive:
return fulcio.WithFulcioAndInteractiveOIDC(fulcioURL, oidcIssuerURL, f.OIDCClientID, f.OIDCClientSecret,
options.Stdin, options.Stdout), nil
case "":
return nil, errors.New("missing oidcMode")
case params.OIDCModeStaticToken:
return nil, errors.New("internal inconsistency: SigningParameterFileOIDCModeStaticToken was supposed to already be handled")
default:
return nil, fmt.Errorf("unknown oidcMode value %q", f.OIDCMode)
}
}