Support content trust images and pull with authentication

Contact the notary server if ```--verify``` is specified (or `verify:
true` is enabled on the repo config) and verify if the image is signed,
use the returned value to pull the verified image.
This commit is contained in:
Ettore Di Giacinto
2021-03-11 17:04:26 +01:00
parent caa1cfad5c
commit 0028dd3a92
12 changed files with 237 additions and 23 deletions

View File

@@ -161,6 +161,7 @@ type LuetRepository struct {
Authentication map[string]string `json:"auth,omitempty" yaml:"auth,omitempty" mapstructure:"auth,omitempty"`
TreePath string `json:"tree_path,omitempty" yaml:"tree_path,omitempty" mapstructure:"tree_path"`
MetaPath string `json:"meta_path,omitempty" yaml:"meta_path,omitempty" mapstructure:"meta_path"`
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty" mapstructure:"verify"`
// Serialized options not used in repository configuration

View File

@@ -16,17 +16,97 @@
package helpers
import (
"context"
"encoding/hex"
"os"
"github.com/docker/cli/cli/trust"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/registry"
"github.com/mudler/luet/pkg/helpers/imgworker"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/theupdateframework/notary/tuf/data"
)
// See also https://github.com/docker/cli/blob/88c6089300a82d3373892adf6845a4fed1a4ba8d/cli/command/image/trust.go#L171
func verifyImage(image string, authConfig *types.AuthConfig) (string, error) {
ref, err := reference.ParseAnyReference(image)
if err != nil {
return "", errors.Wrapf(err, "invalid reference %s", image)
}
// only check if image ref doesn't contain hashes
if _, ok := ref.(reference.Digested); !ok {
namedRef, ok := ref.(reference.Named)
if !ok {
return "", errors.New("failed to resolve image digest using content trust: reference is not named")
}
namedRef = reference.TagNameOnly(namedRef)
taggedRef, ok := namedRef.(reference.NamedTagged)
if !ok {
return "", errors.New("failed to resolve image digest using content trust: reference is not tagged")
}
resolvedImage, err := trustedResolveDigest(context.Background(), taggedRef, authConfig, "luet")
if err != nil {
return "", errors.Wrap(err, "failed to resolve image digest using content trust")
}
resolvedFamiliar := reference.FamiliarString(resolvedImage)
return resolvedFamiliar, nil
}
return "", nil
}
func trustedResolveDigest(ctx context.Context, ref reference.NamedTagged, authConfig *types.AuthConfig, useragent string) (reference.Canonical, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return nil, err
}
notaryRepo, err := trust.GetNotaryRepository(os.Stdin, os.Stdout, useragent, repoInfo, authConfig, "pull")
if err != nil {
return nil, errors.Wrap(err, "error establishing connection to trust repository")
}
t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return nil, trust.NotaryError(repoInfo.Name.Name(), err)
}
// Only get the tag if it's in the top level targets role or the releases delegation role
// ignore it if it's in any other delegation roles
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
return nil, trust.NotaryError(repoInfo.Name.Name(), errors.Errorf("No trust data for %s", reference.FamiliarString(ref)))
}
h, ok := t.Hashes["sha256"]
if !ok {
return nil, errors.New("no valid hash, expecting sha256")
}
dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h))
// Allow returning canonical reference with tag
return reference.WithDigest(ref, dgst)
}
// DownloadAndExtractDockerImage is a re-adaption
// from genuinetools/img https://github.com/genuinetools/img/blob/54d0ca981c1260546d43961a538550eef55c87cf/pull.go
func DownloadAndExtractDockerImage(temp, image, dest string) (*imgworker.ListedImage, error) {
func DownloadAndExtractDockerImage(temp, image, dest string, auth *types.AuthConfig, verify bool) (*imgworker.ListedImage, error) {
if verify {
img, err := verifyImage(image, auth)
if err != nil {
return nil, errors.Wrapf(err, "failed verifying image")
}
image = img
}
defer os.RemoveAll(temp)
c, err := imgworker.New(temp)
c, err := imgworker.New(temp, auth)
if err != nil {
return nil, errors.Wrapf(err, "failed creating client")
}

View File

@@ -0,0 +1,36 @@
package imgworker
import (
"context"
"github.com/docker/docker/api/types"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth"
"google.golang.org/grpc"
)
func NewDockerAuthProvider(auth *types.AuthConfig) session.Attachable {
return &authProvider{
config: auth,
}
}
type authProvider struct {
config *types.AuthConfig
}
func (ap *authProvider) Register(server *grpc.Server) {
// no-op
}
func (ap *authProvider) Credentials(ctx context.Context, req *auth.CredentialsRequest) (*auth.CredentialsResponse, error) {
res := &auth.CredentialsResponse{}
if ap.config.IdentityToken != "" {
res.Secret = ap.config.IdentityToken
} else {
res.Username = ap.config.Username
res.Secret = ap.config.Password
}
return res, nil
}

View File

@@ -8,6 +8,7 @@ import (
"path/filepath"
"github.com/containerd/containerd/namespaces"
dockertypes "github.com/docker/docker/api/types"
"github.com/genuinetools/img/types"
"github.com/moby/buildkit/control"
"github.com/moby/buildkit/session"
@@ -29,10 +30,11 @@ type Client struct {
sess *session.Session
ctx context.Context
auth *dockertypes.AuthConfig
}
// New returns a new client for communicating with the buildkit controller.
func New(root string) (*Client, error) {
func New(root string, auth *dockertypes.AuthConfig) (*Client, error) {
// Native backend is fine, our images have just one layer. No need to depend on anything
backend := types.NativeBackend
@@ -45,6 +47,7 @@ func New(root string) (*Client, error) {
backend: types.NativeBackend,
root: root,
localDirs: nil,
auth: auth,
}
if err := c.prepare(); err != nil {

View File

@@ -31,6 +31,7 @@ func (c *Client) Pull(image string) (*ListedImage, error) {
if err != nil {
return nil, err
}
// Parse the image name and tag.
named, err := reference.ParseNormalizedNamed(image)
if err != nil {
@@ -114,7 +115,6 @@ func (c *Client) Pull(image string) (*ListedImage, error) {
if _, err := e.Export(ctx, exporter.Source{Ref: ref}); err != nil {
return nil, err
}
// Get the image.
img, err := opt.ImageStore.Get(ctx, image)
if err != nil {

View File

@@ -4,10 +4,8 @@ package imgworker
import (
"context"
"os"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/session/filesync"
"github.com/moby/buildkit/session/testutil"
"github.com/pkg/errors"
@@ -31,7 +29,7 @@ func (c *Client) Session(ctx context.Context) (*session.Session, session.Dialer,
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create session manager")
}
sessionName := "img"
sessionName := "luet"
s, err := session.NewSession(ctx, sessionName, "")
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create session")
@@ -41,7 +39,7 @@ func (c *Client) Session(ctx context.Context) (*session.Session, session.Dialer,
syncedDirs = append(syncedDirs, filesync.SyncedDir{Name: name, Dir: d})
}
s.Allow(filesync.NewFSSyncProvider(syncedDirs))
s.Allow(authprovider.NewDockerAuthProvider(os.Stderr))
s.Allow(NewDockerAuthProvider(c.auth))
return s, sessionDialer(s, m), err
}

View File

@@ -16,11 +16,13 @@
package client
import (
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"github.com/docker/docker/api/types"
"github.com/docker/go-units"
"github.com/pkg/errors"
@@ -32,10 +34,17 @@ import (
type DockerClient struct {
RepoData RepoData
auth *types.AuthConfig
verify bool
}
func NewDockerClient(r RepoData) *DockerClient {
return &DockerClient{RepoData: r}
auth := &types.AuthConfig{}
dat, _ := json.Marshal(r.Authentication)
json.Unmarshal(dat, auth)
return &DockerClient{RepoData: r, auth: auth}
}
func (c *DockerClient) DownloadArtifact(artifact compiler.Artifact) (compiler.Artifact, error) {
@@ -88,7 +97,7 @@ func (c *DockerClient) DownloadArtifact(artifact compiler.Artifact) (compiler.Ar
}
// imageName := fmt.Sprintf("%s/%s", uri, artifact.GetCompileSpec().GetPackage().GetPackageImageName())
info, err := helpers.DownloadAndExtractDockerImage(contentstore, imageName, temp)
info, err := helpers.DownloadAndExtractDockerImage(contentstore, imageName, temp, c.auth, c.RepoData.Verify)
if err != nil {
Debug("Failed download of image", imageName)
continue
@@ -151,7 +160,7 @@ func (c *DockerClient) DownloadFile(name string) (string, error) {
imageName := fmt.Sprintf("%s:%s", uri, name)
Info("Downloading", imageName)
info, err := helpers.DownloadAndExtractDockerImage(contentstore, imageName, temp)
info, err := helpers.DownloadAndExtractDockerImage(contentstore, imageName, temp, c.auth, c.RepoData.Verify)
if err != nil {
Debug("Failed download of image", imageName)
continue

View File

@@ -18,4 +18,5 @@ package client
type RepoData struct {
Urls []string
Authentication map[string]string
Verify bool
}

View File

@@ -81,6 +81,7 @@ type LuetSystemRepositorySerialized struct {
TreePath string `json:"treepath"`
MetaPath string `json:"metapath"`
RepositoryFiles map[string]LuetRepositoryFile `json:"repo_files"`
Verify bool `json:"verify"`
}
type LuetSystemRepositoryMetadata struct {
@@ -274,19 +275,22 @@ func NewLuetSystemRepositoryFromYaml(data []byte, db pkg.PackageDatabase) (Repos
if err != nil {
return nil, err
}
repo := config.NewLuetRepository(
p.Name,
p.Type,
p.Description,
p.Urls,
p.Priority,
true,
false,
)
repo.Verify = p.Verify
r := &LuetSystemRepository{
LuetRepository: config.NewLuetRepository(
p.Name,
p.Type,
p.Description,
p.Urls,
p.Priority,
true,
false,
),
LuetRepository: repo,
RepositoryFiles: p.RepositoryFiles,
}
if p.Revision > 0 {
r.Revision = p.Revision
}
@@ -896,6 +900,7 @@ func (r *LuetSystemRepository) Client() Client {
client.RepoData{
Urls: r.GetUrls(),
Authentication: r.GetAuthentication(),
Verify: r.Verify,
})
}
return nil