Skip to content

Commit

Permalink
Merge pull request #1785 from mtrmac/fulcio-signing
Browse files Browse the repository at this point in the history
Fulcio signing implementation
  • Loading branch information
vrothberg committed Jan 12, 2023
2 parents 4592366 + cd6511f commit 5ef8a4f
Show file tree
Hide file tree
Showing 9 changed files with 433 additions and 6 deletions.
6 changes: 2 additions & 4 deletions docker/docker_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import (

"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/internal/iolimits"
"github.com/containers/image/v5/internal/useragent"
"github.com/containers/image/v5/manifest"
"github.com/containers/image/v5/pkg/docker/config"
"github.com/containers/image/v5/pkg/sysregistriesv2"
"github.com/containers/image/v5/pkg/tlsclientconfig"
"github.com/containers/image/v5/types"
"github.com/containers/image/v5/version"
"github.com/containers/storage/pkg/homedir"
"github.com/docker/distribution/registry/api/errcode"
v2 "github.com/docker/distribution/registry/api/v2"
Expand Down Expand Up @@ -68,8 +68,6 @@ var (
{path: etcDir + "/containers/certs.d", absolute: true},
{path: etcDir + "/docker/certs.d", absolute: true},
}

defaultUserAgent = "containers/" + version.Version + " (github.com/containers/image)"
)

// extensionSignature and extensionSignatureList come from github.com/openshift/origin/pkg/dockerregistry/server/signaturedispatcher.go:
Expand Down Expand Up @@ -284,7 +282,7 @@ func newDockerClient(sys *types.SystemContext, registry, reference string) (*doc
}
tlsClientConfig.InsecureSkipVerify = skipVerify

userAgent := defaultUserAgent
userAgent := useragent.DefaultUserAgent
if sys != nil && sys.DockerRegistryUserAgent != "" {
userAgent = sys.DockerRegistryUserAgent
}
Expand Down
3 changes: 2 additions & 1 deletion docker/docker_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"testing"
"time"

"github.com/containers/image/v5/internal/useragent"
"github.com/containers/image/v5/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -176,7 +177,7 @@ func TestUserAgent(t *testing.T) {
}{
// Can't both test nil and set DockerInsecureSkipTLSVerify :(
// {nil, defaultUA},
{&types.SystemContext{}, defaultUserAgent},
{&types.SystemContext{}, useragent.DefaultUserAgent},
{&types.SystemContext{DockerRegistryUserAgent: sentinelUA}, sentinelUA},
} {
// For this test against localhost, we don't care.
Expand Down
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
github.com/opencontainers/selinux v1.10.2
github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f
github.com/proglottis/gpgme v0.1.3
github.com/sigstore/fulcio v1.0.0
github.com/sigstore/rekor v1.0.1
github.com/sigstore/sigstore v1.5.0
github.com/sirupsen/logrus v1.9.0
Expand All @@ -38,6 +39,7 @@ require (
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.5.0
golang.org/x/net v0.5.0
golang.org/x/oauth2 v0.4.0
golang.org/x/sync v0.1.0
golang.org/x/term v0.4.0
)
Expand All @@ -52,6 +54,7 @@ require (
github.com/chzyer/readline v1.5.1 // indirect
github.com/containerd/cgroups v1.0.4 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.13.0 // indirect
github.com/coreos/go-oidc/v3 v3.4.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
Expand Down Expand Up @@ -98,6 +101,8 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 // indirect
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
github.com/tchap/go-patricia v2.3.0+incompatible // indirect
Expand All @@ -111,6 +116,7 @@ require (
golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
golang.org/x/tools v0.4.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 // indirect
google.golang.org/grpc v1.51.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
Expand Down
247 changes: 247 additions & 0 deletions go.sum

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions internal/signature/sigstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const (
SigstoreSignatureAnnotationKey = "dev.cosignproject.cosign/signature"
// from sigstore/cosign/pkg/oci/static.BundleAnnotationKey
SigstoreSETAnnotationKey = "dev.sigstore.cosign/bundle"
// from sigstore/cosign/pkg/oci/static.CertificateAnnotationKey
SigstoreCertificateAnnotationKey = "dev.sigstore.cosign/certificate"
// from sigstore/cosign/pkg/oci/static.ChainAnnotationKey
SigstoreIntermediateCertificateChainAnnotationKey = "dev.sigstore.cosign/chain"
)

// Sigstore is a github.com/cosign/cosign signature.
Expand Down
6 changes: 6 additions & 0 deletions internal/useragent/useragent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package useragent

import "github.com/containers/image/v5/version"

// DefaultUserAgent is a value that should be used by User-Agent headers, unless the user specifically instructs us otherwise.
var DefaultUserAgent = "containers/" + version.Version + " (github.com/containers/image)"
155 changes: 155 additions & 0 deletions signature/sigstore/fulcio/fulcio.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
10 changes: 10 additions & 0 deletions signature/sigstore/internal/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ type SigstoreSigner struct {
PrivateKey sigstoreSignature.Signer // May be nil during initialization
SigningKeyOrCert []byte // For possible Rekor upload; always initialized together with PrivateKey

// Fulcio results to include
FulcioGeneratedCertificate []byte // Or nil
FulcioGeneratedCertificateChain []byte // Or nil

// Rekor state
RekorUploader func(ctx context.Context, keyOrCertBytes []byte, signatureBytes []byte, payloadBytes []byte) ([]byte, error) // Or nil
}
Expand Down Expand Up @@ -74,6 +78,12 @@ func (s *SigstoreSigner) SignImageManifest(ctx context.Context, m []byte, docker
annotations := map[string]string{
signature.SigstoreSignatureAnnotationKey: base64Signature,
}
if s.FulcioGeneratedCertificate != nil {
annotations[signature.SigstoreCertificateAnnotationKey] = string(s.FulcioGeneratedCertificate)
}
if s.FulcioGeneratedCertificateChain != nil {
annotations[signature.SigstoreIntermediateCertificateChainAnnotationKey] = string(s.FulcioGeneratedCertificateChain)
}
if rekorSETBytes != nil {
annotations[signature.SigstoreSETAnnotationKey] = string(rekorSETBytes)
}
Expand Down
2 changes: 1 addition & 1 deletion signature/sigstore/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func NewSigner(opts ...Option) (*signer.Signer, error) {
}
}
if s.PrivateKey == nil {
return nil, fmt.Errorf("preparing to create a sigstore signature: nothing to sign with provided")
return nil, errors.New("no private key source provided (neither a private key nor Fulcio) when preparing to create sigstore signatures")
}

return internalSigner.NewSigner(&s), nil
Expand Down

0 comments on commit 5ef8a4f

Please sign in to comment.