Jonathan A. Sternberg b35a0f4718
protobuf: remove gogoproto
Removes gogo/protobuf from buildx and updates to a version of
moby/buildkit where gogo is removed.

This also changes how the proto files are generated. This is because
newer versions of protobuf are more strict about name conflicts. If two
files have the same name (even if they are relative paths) and are used
in different protoc commands, they'll conflict in the registry.

Since protobuf file generation doesn't work very well with
`paths=source_relative`, this removes the `go:generate` expression and
just relies on the dockerfile to perform the generation.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
2024-10-02 15:51:59 -05:00

305 lines
8.1 KiB
Go

package authprovider
import (
"context"
"crypto/ed25519"
"crypto/hmac"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"runtime"
"strconv"
"strings"
"sync"
"time"
authutil "github.com/containerd/containerd/remotes/docker/auth"
remoteserrors "github.com/containerd/containerd/remotes/errors"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
http "github.com/hashicorp/go-cleanhttp"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth"
"github.com/moby/buildkit/util/progress/progresswriter"
"github.com/pkg/errors"
"golang.org/x/crypto/nacl/sign"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
defaultExpiration = 60
dockerHubConfigfileKey = "https://index.docker.io/v1/"
dockerHubRegistryHost = "registry-1.docker.io"
)
func NewDockerAuthProvider(cfg *configfile.ConfigFile, tlsConfigs map[string]*AuthTLSConfig) session.Attachable {
return &authProvider{
authConfigCache: map[string]*types.AuthConfig{},
config: cfg,
seeds: &tokenSeeds{dir: config.Dir()},
loggerCache: map[string]struct{}{},
tlsConfigs: tlsConfigs,
}
}
type authProvider struct {
authConfigCache map[string]*types.AuthConfig
config *configfile.ConfigFile
seeds *tokenSeeds
logger progresswriter.Logger
loggerCache map[string]struct{}
tlsConfigs map[string]*AuthTLSConfig
// The need for this mutex is not well understood.
// Without it, the docker cli on OS X hangs when
// reading credentials from docker-credential-osxkeychain.
// See issue https://github.com/docker/cli/issues/1862
mu sync.Mutex
}
func (ap *authProvider) SetLogger(l progresswriter.Logger) {
ap.mu.Lock()
ap.logger = l
ap.mu.Unlock()
}
func (ap *authProvider) Register(server *grpc.Server) {
auth.RegisterAuthServer(server, ap)
}
func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequest) (rr *auth.FetchTokenResponse, err error) {
ac, err := ap.getAuthConfig(req.Host)
if err != nil {
return nil, err
}
// check for statically configured bearer token
if ac.RegistryToken != "" {
return toTokenResponse(ac.RegistryToken, time.Time{}, 0), nil
}
creds, err := ap.credentials(req.Host)
if err != nil {
return nil, err
}
to := authutil.TokenOptions{
Realm: req.Realm,
Service: req.Service,
Scopes: req.Scopes,
Username: creds.Username,
Secret: creds.Secret,
}
httpClient := http.DefaultClient()
if tc, err := ap.tlsConfig(req.Host); err == nil && tc != nil {
transport := http.DefaultTransport()
transport.TLSClientConfig = tc
httpClient.Transport = transport
}
if creds.Secret != "" {
done := func(progresswriter.SubLogger) error {
return err
}
defer func() {
err = errors.Wrap(err, "failed to fetch oauth token")
}()
ap.mu.Lock()
name := fmt.Sprintf("[auth] %v token for %s", strings.Join(trimScopePrefix(req.Scopes), " "), req.Host)
if _, ok := ap.loggerCache[name]; !ok {
progresswriter.Wrap(name, ap.logger, done)
}
ap.mu.Unlock()
// credential information is provided, use oauth POST endpoint
resp, err := authutil.FetchTokenWithOAuth(ctx, httpClient, nil, "buildkit-client", to)
if err != nil {
var errStatus remoteserrors.ErrUnexpectedStatus
if errors.As(err, &errStatus) {
// Registries without support for POST may return 404 for POST /v2/token.
// As of September 2017, GCR is known to return 404.
// As of February 2018, JFrog Artifactory is known to return 401.
if (errStatus.StatusCode == 405 && to.Username != "") || errStatus.StatusCode == 404 || errStatus.StatusCode == 401 {
resp, err := authutil.FetchToken(ctx, httpClient, nil, to)
if err != nil {
return nil, err
}
return toTokenResponse(resp.Token, resp.IssuedAt, resp.ExpiresIn), nil
}
}
return nil, err
}
return toTokenResponse(resp.AccessToken, resp.IssuedAt, resp.ExpiresIn), nil
}
// do request anonymously
resp, err := authutil.FetchToken(ctx, httpClient, nil, to)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch anonymous token")
}
return toTokenResponse(resp.Token, resp.IssuedAt, resp.ExpiresIn), nil
}
func (ap *authProvider) tlsConfig(host string) (*tls.Config, error) {
if ap.tlsConfigs == nil {
return nil, nil
}
c, ok := ap.tlsConfigs[host]
if !ok {
return nil, nil
}
tc := &tls.Config{}
if len(c.RootCAs) > 0 {
systemPool, err := x509.SystemCertPool()
if err != nil {
if runtime.GOOS == "windows" {
systemPool = x509.NewCertPool()
} else {
return nil, errors.Wrapf(err, "unable to get system cert pool")
}
}
tc.RootCAs = systemPool
}
for _, p := range c.RootCAs {
dt, err := os.ReadFile(p)
if err != nil {
return nil, errors.Wrapf(err, "failed to read %s", p)
}
tc.RootCAs.AppendCertsFromPEM(dt)
}
for _, kp := range c.KeyPairs {
cert, err := tls.LoadX509KeyPair(kp.Certificate, kp.Key)
if err != nil {
return nil, errors.Wrapf(err, "failed to load keypair for %s", kp.Certificate)
}
tc.Certificates = append(tc.Certificates, cert)
}
if c.Insecure {
tc.InsecureSkipVerify = true
}
return tc, nil
}
func (ap *authProvider) credentials(host string) (*auth.CredentialsResponse, error) {
ac, err := ap.getAuthConfig(host)
if err != nil {
return nil, err
}
res := &auth.CredentialsResponse{}
if ac.IdentityToken != "" {
res.Secret = ac.IdentityToken
} else {
res.Username = ac.Username
res.Secret = ac.Password
}
return res, nil
}
func (ap *authProvider) Credentials(ctx context.Context, req *auth.CredentialsRequest) (*auth.CredentialsResponse, error) {
resp, err := ap.credentials(req.Host)
if err != nil || resp.Secret != "" {
ap.mu.Lock()
defer ap.mu.Unlock()
_, ok := ap.loggerCache[req.Host]
ap.loggerCache[req.Host] = struct{}{}
if !ok && ap.logger != nil {
return resp, progresswriter.Wrap(fmt.Sprintf("[auth] sharing credentials for %s", req.Host), ap.logger, func(progresswriter.SubLogger) error {
return err
})
}
}
return resp, err
}
func (ap *authProvider) GetTokenAuthority(ctx context.Context, req *auth.GetTokenAuthorityRequest) (*auth.GetTokenAuthorityResponse, error) {
key, err := ap.getAuthorityKey(req.Host, req.Salt)
if err != nil {
return nil, err
}
return &auth.GetTokenAuthorityResponse{PublicKey: key[32:]}, nil
}
func (ap *authProvider) VerifyTokenAuthority(ctx context.Context, req *auth.VerifyTokenAuthorityRequest) (*auth.VerifyTokenAuthorityResponse, error) {
key, err := ap.getAuthorityKey(req.Host, req.Salt)
if err != nil {
return nil, err
}
priv := new([64]byte)
copy((*priv)[:], key)
return &auth.VerifyTokenAuthorityResponse{Signed: sign.Sign(nil, req.Payload, priv)}, nil
}
func (ap *authProvider) getAuthConfig(host string) (*types.AuthConfig, error) {
ap.mu.Lock()
defer ap.mu.Unlock()
if host == dockerHubRegistryHost {
host = dockerHubConfigfileKey
}
if _, exists := ap.authConfigCache[host]; !exists {
ac, err := ap.config.GetAuthConfig(host)
if err != nil {
return nil, err
}
ap.authConfigCache[host] = &ac
}
return ap.authConfigCache[host], nil
}
func (ap *authProvider) getAuthorityKey(host string, salt []byte) (ed25519.PrivateKey, error) {
if v, err := strconv.ParseBool(os.Getenv("BUILDKIT_NO_CLIENT_TOKEN")); err == nil && v {
return nil, status.Errorf(codes.Unavailable, "client side tokens disabled")
}
creds, err := ap.credentials(host)
if err != nil {
return nil, err
}
seed, err := ap.seeds.getSeed(host)
if err != nil {
return nil, err
}
mac := hmac.New(sha256.New, salt)
if creds.Secret != "" {
mac.Write(seed)
}
sum := mac.Sum(nil)
return ed25519.NewKeyFromSeed(sum[:ed25519.SeedSize]), nil
}
func toTokenResponse(token string, issuedAt time.Time, expires int) *auth.FetchTokenResponse {
if expires == 0 {
expires = defaultExpiration
}
resp := &auth.FetchTokenResponse{
Token: token,
ExpiresIn: int64(expires),
}
if !issuedAt.IsZero() {
resp.IssuedAt = issuedAt.Unix()
}
return resp
}
func trimScopePrefix(scopes []string) []string {
out := make([]string, len(scopes))
for i, s := range scopes {
out[i] = strings.TrimPrefix(s, "repository:")
}
return out
}