mirror of https://github.com/containers/image.git
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:
parent
6320cfe1f5
commit
9e3177f44c
|
|
@ -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
2
go.mod
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 don’t 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
|
||||
}
|
||||
|
|
@ -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 shouldn’t 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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue