mirror of
https://github.com/containers/image.git
synced 2025-04-18 19:44:05 +03:00
162 lines
7.2 KiB
Go
162 lines
7.2 KiB
Go
//go:build !containers_image_fulcio_stub
|
||
|
||
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 accessible 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 (incl. the update in init()), overriding only input/output
|
||
htmlPage, err := oauth.GetInteractiveSuccessHTML(false, 10)
|
||
if err != nil {
|
||
return fmt.Errorf("formatting HTML content: %w", err)
|
||
}
|
||
tokenGetter := &oauthflow.InteractiveIDTokenGetter{
|
||
HTMLPage: htmlPage,
|
||
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)
|
||
}
|
||
}
|