mirror of
https://github.com/containers/skopeo.git
synced 2025-09-22 02:18:41 +00:00
Add support for Fulcio and Rekor, and --sign-by-sigstore=param-file
(skopeo copy) and (skopeo sync) now support --sign-by-sigstore=param-file, using the containers-sigstore-signing-params.yaml(5) file format. That notably adds support for Fulcio and Rekor signing. Signed-off-by: Miloslav Trmač <mitr@redhat.com>
This commit is contained in:
75
vendor/github.com/containers/image/v5/pkg/cli/sigstore/params/sigstore.go
generated
vendored
Normal file
75
vendor/github.com/containers/image/v5/pkg/cli/sigstore/params/sigstore.go
generated
vendored
Normal 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 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
|
||||
}
|
117
vendor/github.com/containers/image/v5/pkg/cli/sigstore/sigstore.go
generated
vendored
Normal file
117
vendor/github.com/containers/image/v5/pkg/cli/sigstore/sigstore.go
generated
vendored
Normal 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 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)
|
||||
}
|
||||
}
|
155
vendor/github.com/containers/image/v5/signature/sigstore/fulcio/fulcio.go
generated
vendored
Normal file
155
vendor/github.com/containers/image/v5/signature/sigstore/fulcio/fulcio.go
generated
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
package fulcio
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/containers/image/v5/internal/useragent"
|
||||
"github.com/containers/image/v5/signature/sigstore/internal"
|
||||
"github.com/sigstore/fulcio/pkg/api"
|
||||
"github.com/sigstore/sigstore/pkg/oauth"
|
||||
"github.com/sigstore/sigstore/pkg/oauthflow"
|
||||
sigstoreSignature "github.com/sigstore/sigstore/pkg/signature"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// setupSignerWithFulcio updates s with a certificate generated by fulcioURL based on oidcIDToken
|
||||
func setupSignerWithFulcio(s *internal.SigstoreSigner, fulcioURL *url.URL, oidcIDToken *oauthflow.OIDCIDToken) error {
|
||||
// ECDSA-P256 is the only interoperable algorithm per
|
||||
// https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#signature-schemes .
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating short-term private key: %w", err)
|
||||
}
|
||||
keyAlgorithm := "ecdsa"
|
||||
// SHA-256 is opencontainers/go-digest.Canonical, thus the algorithm to use here as well per
|
||||
// https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#hashing-algorithms
|
||||
signer, err := sigstoreSignature.LoadECDSASigner(privateKey, crypto.SHA256)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing short-term private key: %w", err)
|
||||
}
|
||||
s.PrivateKey = signer
|
||||
|
||||
logrus.Debugf("Requesting a certificate from Fulcio at %s", fulcioURL.Redacted())
|
||||
fulcioClient := api.NewClient(fulcioURL, api.WithUserAgent(useragent.DefaultUserAgent))
|
||||
// Sign the email address as part of the request
|
||||
h := sha256.Sum256([]byte(oidcIDToken.Subject))
|
||||
keyOwnershipProof, err := ecdsa.SignASN1(rand.Reader, privateKey, h[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error signing key ownership proof: %w", err)
|
||||
}
|
||||
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("converting public key to ASN.1: %w", err)
|
||||
}
|
||||
// Note that unlike most OAuth2 uses, this passes the ID token, not an access token.
|
||||
// This is only secure if every Fulcio server has an individual client ID value
|
||||
// = fulcioOIDCClientID, distinct from other Fulcio servers,
|
||||
// that is embedded into the ID token’s "aud" field.
|
||||
resp, err := fulcioClient.SigningCert(api.CertificateRequest{
|
||||
PublicKey: api.Key{
|
||||
Content: publicKeyBytes,
|
||||
Algorithm: keyAlgorithm,
|
||||
},
|
||||
SignedEmailAddress: keyOwnershipProof,
|
||||
}, oidcIDToken.RawString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("obtaining certificate from Fulcio: %w", err)
|
||||
}
|
||||
s.FulcioGeneratedCertificate = resp.CertPEM
|
||||
s.FulcioGeneratedCertificateChain = resp.ChainPEM
|
||||
// Cosign goes through an unmarshal/marshal roundtrip for Fulcio-generated certificates, let’s not do that.
|
||||
s.SigningKeyOrCert = resp.CertPEM
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithFulcioAndPreexistingOIDCIDToken sets up signing to use a short-lived key and a Fulcio-issued certificate
|
||||
// based on a caller-provided OIDC ID token.
|
||||
func WithFulcioAndPreexistingOIDCIDToken(fulcioURL *url.URL, oidcIDToken string) internal.Option {
|
||||
return func(s *internal.SigstoreSigner) error {
|
||||
if s.PrivateKey != nil {
|
||||
return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures")
|
||||
}
|
||||
|
||||
// This adds dependencies even just to parse the token. We could possibly reimplement that, and split this variant
|
||||
// into a subpackage without the OIDC dependencies… but really, is this going to be used in significantly different situations
|
||||
// than the two interactive OIDC authentication workflows?
|
||||
//
|
||||
// Are there any widely used tools to manually obtain an ID token? Why would there be?
|
||||
// For long-term usage, users provisioning a static OIDC credential might just as well provision an already-generated certificate
|
||||
// or something like that.
|
||||
logrus.Debugf("Using a statically-provided OIDC token")
|
||||
staticTokenGetter := oauthflow.StaticTokenGetter{RawToken: oidcIDToken}
|
||||
oidcIDToken, err := staticTokenGetter.GetIDToken(nil, oauth2.Config{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing OIDC token: %w", err)
|
||||
}
|
||||
|
||||
return setupSignerWithFulcio(s, fulcioURL, oidcIDToken)
|
||||
}
|
||||
}
|
||||
|
||||
// WithFulcioAndDeviceAuthorizationGrantOIDC sets up signing to use a short-lived key and a Fulcio-issued certificate
|
||||
// based on an OIDC ID token obtained using a device authorization grant (RFC 8628).
|
||||
//
|
||||
// interactiveOutput must be directly accesible to a human user in real time (i.e. not be just a log file).
|
||||
func WithFulcioAndDeviceAuthorizationGrantOIDC(fulcioURL *url.URL, oidcIssuerURL *url.URL, oidcClientID, oidcClientSecret string,
|
||||
interactiveOutput io.Writer) internal.Option {
|
||||
return func(s *internal.SigstoreSigner) error {
|
||||
if s.PrivateKey != nil {
|
||||
return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures")
|
||||
}
|
||||
|
||||
logrus.Debugf("Starting OIDC device flow for issuer %s", oidcIssuerURL.Redacted())
|
||||
tokenGetter := oauthflow.NewDeviceFlowTokenGetterForIssuer(oidcIssuerURL.String())
|
||||
tokenGetter.MessagePrinter = func(s string) {
|
||||
fmt.Fprintln(interactiveOutput, s)
|
||||
}
|
||||
oidcIDToken, err := oauthflow.OIDConnect(oidcIssuerURL.String(), oidcClientID, oidcClientSecret, "", tokenGetter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error authenticating with OIDC: %w", err)
|
||||
}
|
||||
|
||||
return setupSignerWithFulcio(s, fulcioURL, oidcIDToken)
|
||||
}
|
||||
}
|
||||
|
||||
// WithFulcioAndInterativeOIDC sets up signing to use a short-lived key and a Fulcio-issued certificate
|
||||
// based on an interactively-obtained OIDC ID token.
|
||||
// The token is obtained
|
||||
// - directly using a browser, listening on localhost, automatically opening a browser to the OIDC issuer,
|
||||
// to be redirected on localhost. (I.e. the current environment must allow launching a browser that connect back to the current process;
|
||||
// either or both may be impossible in a container or a remote VM).
|
||||
// - or by instructing the user to manually open a browser, obtain the OIDC code, and interactively input it as text.
|
||||
//
|
||||
// interactiveInput and interactiveOutput must both be directly operable by a human user in real time (i.e. not be just a log file).
|
||||
func WithFulcioAndInteractiveOIDC(fulcioURL *url.URL, oidcIssuerURL *url.URL, oidcClientID, oidcClientSecret string,
|
||||
interactiveInput io.Reader, interactiveOutput io.Writer) internal.Option {
|
||||
return func(s *internal.SigstoreSigner) error {
|
||||
if s.PrivateKey != nil {
|
||||
return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures")
|
||||
}
|
||||
|
||||
logrus.Debugf("Starting interactive OIDC authentication for issuer %s", oidcIssuerURL.Redacted())
|
||||
// This is intended to match oauthflow.DefaultIDTokenGetter, overriding only input/output
|
||||
tokenGetter := &oauthflow.InteractiveIDTokenGetter{
|
||||
HTMLPage: oauth.InteractiveSuccessHTML,
|
||||
Input: interactiveInput,
|
||||
Output: interactiveOutput,
|
||||
}
|
||||
oidcIDToken, err := oauthflow.OIDConnect(oidcIssuerURL.String(), oidcClientID, oidcClientSecret, "", tokenGetter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error authenticating with OIDC: %w", err)
|
||||
}
|
||||
|
||||
return setupSignerWithFulcio(s, fulcioURL, oidcIDToken)
|
||||
}
|
||||
}
|
52
vendor/github.com/containers/image/v5/signature/sigstore/rekor/leveled_logger.go
generated
vendored
Normal file
52
vendor/github.com/containers/image/v5/signature/sigstore/rekor/leveled_logger.go
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
package rekor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// leveledLogger adapts our use of logrus to the expected go-retryablehttp.LeveledLogger interface.
|
||||
type leveledLogger struct {
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
func leveledLoggerForLogrus(logger *logrus.Logger) retryablehttp.LeveledLogger {
|
||||
return &leveledLogger{logger: logger}
|
||||
}
|
||||
|
||||
// log is the actual conversion implementation
|
||||
func (l *leveledLogger) log(level logrus.Level, msg string, keysAndValues []interface{}) {
|
||||
fields := logrus.Fields{}
|
||||
for i := 0; i < len(keysAndValues)-1; i += 2 {
|
||||
key := keysAndValues[i]
|
||||
keyString, isString := key.(string)
|
||||
if !isString {
|
||||
// It seems attractive to panic() here, but we might already be in a failure state, so let’s not make it worse
|
||||
keyString = fmt.Sprintf("[Invalid LeveledLogger key %#v]", key)
|
||||
}
|
||||
fields[keyString] = keysAndValues[i+1]
|
||||
}
|
||||
l.logger.WithFields(fields).Log(level, msg)
|
||||
}
|
||||
|
||||
// Debug implements retryablehttp.LeveledLogger
|
||||
func (l *leveledLogger) Debug(msg string, keysAndValues ...interface{}) {
|
||||
l.log(logrus.DebugLevel, msg, keysAndValues)
|
||||
}
|
||||
|
||||
// Error implements retryablehttp.LeveledLogger
|
||||
func (l *leveledLogger) Error(msg string, keysAndValues ...interface{}) {
|
||||
l.log(logrus.ErrorLevel, msg, keysAndValues)
|
||||
}
|
||||
|
||||
// Info implements retryablehttp.LeveledLogger
|
||||
func (l *leveledLogger) Info(msg string, keysAndValues ...interface{}) {
|
||||
l.log(logrus.InfoLevel, msg, keysAndValues)
|
||||
}
|
||||
|
||||
// Warn implements retryablehttp.LeveledLogger
|
||||
func (l *leveledLogger) Warn(msg string, keysAndValues ...interface{}) {
|
||||
l.log(logrus.WarnLevel, msg, keysAndValues)
|
||||
}
|
160
vendor/github.com/containers/image/v5/signature/sigstore/rekor/rekor.go
generated
vendored
Normal file
160
vendor/github.com/containers/image/v5/signature/sigstore/rekor/rekor.go
generated
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
package rekor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/signature/internal"
|
||||
signerInternal "github.com/containers/image/v5/signature/sigstore/internal"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/swag"
|
||||
rekor "github.com/sigstore/rekor/pkg/client"
|
||||
"github.com/sigstore/rekor/pkg/generated/client"
|
||||
"github.com/sigstore/rekor/pkg/generated/client/entries"
|
||||
"github.com/sigstore/rekor/pkg/generated/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// WithRekor asks the generated signature to be uploaded to the specified Rekor server,
|
||||
// and to include a log inclusion proof in the signature.
|
||||
func WithRekor(rekorURL *url.URL) signerInternal.Option {
|
||||
return func(s *signerInternal.SigstoreSigner) error {
|
||||
logrus.Debugf("Using Rekor server at %s", rekorURL.Redacted())
|
||||
client, err := rekor.GetRekorClient(rekorURL.String(),
|
||||
rekor.WithLogger(leveledLoggerForLogrus(logrus.StandardLogger())))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating Rekor client: %w", err)
|
||||
}
|
||||
u := uploader{
|
||||
client: client,
|
||||
}
|
||||
s.RekorUploader = u.uploadKeyOrCert
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// uploader wraps a Rekor client, basically so that we can set RekorUploader to a method instead of an one-off closure.
|
||||
type uploader struct {
|
||||
client *client.Rekor
|
||||
}
|
||||
|
||||
// rekorEntryToSET converts a Rekor log entry into a sigstore “signed entry timestamp”.
|
||||
func rekorEntryToSET(entry *models.LogEntryAnon) (internal.UntrustedRekorSET, error) {
|
||||
// We could plausibly call entry.Validate() here; that mostly just uses unnecessary reflection instead of direct == nil checks.
|
||||
// Right now the only extra validation .Validate() does is *entry.LogIndex >= 0 and a regex check on *entry.LogID;
|
||||
// we don’t particularly care about either of these (notably signature verification only uses the Body value).
|
||||
if entry.Verification == nil || entry.IntegratedTime == nil || entry.LogIndex == nil || entry.LogID == nil {
|
||||
return internal.UntrustedRekorSET{}, fmt.Errorf("invalid Rekor entry (missing data): %#v", *entry)
|
||||
}
|
||||
bodyBase64, ok := entry.Body.(string)
|
||||
if !ok {
|
||||
return internal.UntrustedRekorSET{}, fmt.Errorf("unexpected Rekor entry body type: %#v", entry.Body)
|
||||
}
|
||||
body, err := base64.StdEncoding.DecodeString(bodyBase64)
|
||||
if err != nil {
|
||||
return internal.UntrustedRekorSET{}, fmt.Errorf("error parsing Rekor entry body: %w", err)
|
||||
}
|
||||
payloadJSON, err := internal.UntrustedRekorPayload{
|
||||
Body: body,
|
||||
IntegratedTime: *entry.IntegratedTime,
|
||||
LogIndex: *entry.LogIndex,
|
||||
LogID: *entry.LogID,
|
||||
}.MarshalJSON()
|
||||
if err != nil {
|
||||
return internal.UntrustedRekorSET{}, err
|
||||
}
|
||||
|
||||
return internal.UntrustedRekorSET{
|
||||
UntrustedSignedEntryTimestamp: entry.Verification.SignedEntryTimestamp,
|
||||
UntrustedPayload: payloadJSON,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// uploadEntry ensures proposedEntry exists in Rekor (usually uploading it), and returns the resulting log entry.
|
||||
func (u *uploader) uploadEntry(ctx context.Context, proposedEntry models.ProposedEntry) (models.LogEntry, error) {
|
||||
params := entries.NewCreateLogEntryParamsWithContext(ctx)
|
||||
params.SetProposedEntry(proposedEntry)
|
||||
logrus.Debugf("Calling Rekor's CreateLogEntry")
|
||||
resp, err := u.client.Entries.CreateLogEntry(params)
|
||||
if err != nil {
|
||||
// In ordinary operation, we should not get duplicate entries, because our payload contains a timestamp,
|
||||
// so it is supposed to be unique; and the default key format, ECDSA p256, also contains a nonce.
|
||||
// But conflicts can fairly easily happen during debugging and experimentation, so it pays to handle this.
|
||||
var conflictErr *entries.CreateLogEntryConflict
|
||||
if errors.As(err, &conflictErr) && conflictErr.Location != "" {
|
||||
location := conflictErr.Location.String()
|
||||
logrus.Debugf("CreateLogEntry reported a conflict, location = %s", location)
|
||||
// We might be able to just GET the returned Location, but let’s use the generated API client.
|
||||
// OTOH that requires us to hard-code the URI structure…
|
||||
uuidDelimiter := strings.LastIndexByte(location, '/')
|
||||
if uuidDelimiter != -1 { // Otherwise the URI is unexpected, and fall through to the bottom
|
||||
uuid := location[uuidDelimiter+1:]
|
||||
logrus.Debugf("Calling Rekor's NewGetLogEntryByUUIDParamsWithContext")
|
||||
params2 := entries.NewGetLogEntryByUUIDParamsWithContext(ctx)
|
||||
params2.SetEntryUUID(uuid)
|
||||
resp2, err := u.client.Entries.GetLogEntryByUUID(params2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error re-loading previously-created log entry with UUID %s: %w", uuid, err)
|
||||
}
|
||||
return resp2.GetPayload(), nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Error uploading a log entry: %w", err)
|
||||
}
|
||||
return resp.GetPayload(), nil
|
||||
}
|
||||
|
||||
// uploadKeyOrCert integrates this code into sigstore/internal.Signer.
|
||||
// Given components of the created signature, it returns a SET that should be added to the signature.
|
||||
func (u *uploader) uploadKeyOrCert(ctx context.Context, keyOrCertBytes []byte, signatureBytes []byte, payloadBytes []byte) ([]byte, error) {
|
||||
payloadHash := sha256.Sum256(payloadBytes) // HashedRecord only accepts SHA-256
|
||||
proposedEntry := models.Hashedrekord{
|
||||
APIVersion: swag.String(internal.HashedRekordV001APIVersion),
|
||||
Spec: models.HashedrekordV001Schema{
|
||||
Data: &models.HashedrekordV001SchemaData{
|
||||
Hash: &models.HashedrekordV001SchemaDataHash{
|
||||
Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256),
|
||||
Value: swag.String(hex.EncodeToString(payloadHash[:])),
|
||||
},
|
||||
},
|
||||
Signature: &models.HashedrekordV001SchemaSignature{
|
||||
Content: strfmt.Base64(signatureBytes),
|
||||
PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{
|
||||
Content: strfmt.Base64(keyOrCertBytes),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
uploadedPayload, err := u.uploadEntry(ctx, &proposedEntry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(uploadedPayload) != 1 {
|
||||
return nil, fmt.Errorf("expected 1 Rekor entry, got %d", len(uploadedPayload))
|
||||
}
|
||||
var storedEntry *models.LogEntryAnon
|
||||
// This “loop” extracts the single value from the uploadedPayload map.
|
||||
for _, p := range uploadedPayload {
|
||||
storedEntry = &p
|
||||
break
|
||||
}
|
||||
|
||||
rekorBundle, err := rekorEntryToSET(storedEntry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rekorSETBytes, err := json.Marshal(rekorBundle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rekorSETBytes, nil
|
||||
}
|
Reference in New Issue
Block a user