Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions app/cli/cmd/attestation_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package cmd
import (
"errors"
"fmt"
"os"

"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand All @@ -31,6 +32,15 @@ func newAttestationAddCmd() *cobra.Command {
var artifactCASConn *grpc.ClientConn
var annotationsFlag []string

// OCI registry credentials can be passed as flags or environment variables
var registryServer, registryUsername, registryPassword string
const (
registryServerEnvVarName = "CHAINLOOP_REGISTRY_SERVER"
registryUsernameEnvVarName = "CHAINLOOP_REGISTRY_USERNAME"
// nolint: gosec
registryPasswordEnvVarName = "CHAINLOOP_REGISTRY_PASSWORD"
)

cmd := &cobra.Command{
Use: "add",
Short: "add a material to the attestation",
Expand All @@ -43,6 +53,9 @@ func newAttestationAddCmd() *cobra.Command {
ActionsOpts: actionOpts,
CASURI: viper.GetString(confOptions.CASAPI.viperKey),
ConnectionInsecure: flagInsecure,
RegistryServer: registryServer,
RegistryUsername: registryUsername,
RegistryPassword: registryPassword,
},
)
if err != nil {
Expand Down Expand Up @@ -85,5 +98,22 @@ func newAttestationAddCmd() *cobra.Command {
cmd.Flags().StringSliceVar(&annotationsFlag, "annotation", nil, "additional annotation in the format of key=value")
flagAttestationID(cmd)

// Optional OCI registry credentials
cmd.Flags().StringVar(&registryServer, "registry-server", "", fmt.Sprintf("OCI repository server, ($%s)", registryServerEnvVarName))
cmd.Flags().StringVar(&registryUsername, "registry-username", "", fmt.Sprintf("registry username, ($%s)", registryUsernameEnvVarName))
cmd.Flags().StringVar(&registryPassword, "registry-password", "", fmt.Sprintf("registry password, ($%s)", registryPasswordEnvVarName))

if registryServer == "" {
registryServer = os.Getenv(registryServerEnvVarName)
}

if registryUsername == "" {
registryUsername = os.Getenv(registryUsernameEnvVarName)
}

if registryPassword == "" {
registryPassword = os.Getenv(registryPasswordEnvVarName)
}

return cmd
}
4 changes: 2 additions & 2 deletions app/cli/internal/action/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func toTimePtr(t time.Time) *time.Time {
}

// load a crafter with either local or remote state
func newCrafter(enableRemoteState bool, conn *grpc.ClientConn, logger *zerolog.Logger) (*crafter.Crafter, error) {
func newCrafter(enableRemoteState bool, conn *grpc.ClientConn, opts ...crafter.NewOpt) (*crafter.Crafter, error) {
var stateManager crafter.StateManager
var err error

Expand All @@ -55,5 +55,5 @@ func newCrafter(enableRemoteState bool, conn *grpc.ClientConn, logger *zerolog.L
return nil, fmt.Errorf("failed to create state manager: %w", err)
}

return crafter.NewCrafter(stateManager, crafter.WithLogger(logger))
return crafter.NewCrafter(stateManager, opts...)
}
10 changes: 9 additions & 1 deletion app/cli/internal/action/attestation_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type AttestationAddOpts struct {
ArtifactsCASConn *grpc.ClientConn
CASURI string
ConnectionInsecure bool
// OCI registry credentials used for CONTAINER_IMAGE material type
RegistryServer, RegistryUsername, RegistryPassword string
}

type AttestationAdd struct {
Expand All @@ -42,7 +44,13 @@ type AttestationAdd struct {
}

func NewAttestationAdd(cfg *AttestationAddOpts) (*AttestationAdd, error) {
c, err := newCrafter(cfg.UseAttestationRemoteState, cfg.CPConnection, &cfg.Logger)
opts := []crafter.NewOpt{crafter.WithLogger(&cfg.Logger)}
if cfg.RegistryServer != "" && cfg.RegistryUsername != "" && cfg.RegistryPassword != "" {
cfg.Logger.Debug().Str("server", cfg.RegistryServer).Str("username", cfg.RegistryUsername).Msg("using OCI registry credentials")
opts = append(opts, crafter.WithOCIAuth(cfg.RegistryServer, cfg.RegistryUsername, cfg.RegistryPassword))
}

c, err := newCrafter(cfg.UseAttestationRemoteState, cfg.CPConnection, opts...)
if err != nil {
return nil, fmt.Errorf("failed to load crafter: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion app/cli/internal/action/attestation_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (e ErrRunnerContextNotFound) Error() string {
}

func NewAttestationInit(cfg *AttestationInitOpts) (*AttestationInit, error) {
c, err := newCrafter(cfg.UseAttestationRemoteState, cfg.CPConnection, &cfg.Logger)
c, err := newCrafter(cfg.UseAttestationRemoteState, cfg.CPConnection, crafter.WithLogger(&cfg.Logger))
if err != nil {
return nil, fmt.Errorf("failed to load crafter: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion app/cli/internal/action/attestation_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type AttestationPush struct {
}

func NewAttestationPush(cfg *AttestationPushOpts) (*AttestationPush, error) {
c, err := newCrafter(cfg.UseAttestationRemoteState, cfg.CPConnection, &cfg.Logger)
c, err := newCrafter(cfg.UseAttestationRemoteState, cfg.CPConnection, crafter.WithLogger(&cfg.Logger))
if err != nil {
return nil, fmt.Errorf("failed to load crafter: %w", err)
}
Expand Down
6 changes: 3 additions & 3 deletions app/cli/internal/action/attestation_reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ type AttestationReset struct {
c *crafter.Crafter
}

func NewAttestationReset(opts *ActionsOpts) (*AttestationReset, error) {
c, err := newCrafter(opts.UseAttestationRemoteState, opts.CPConnection, &opts.Logger)
func NewAttestationReset(cfg *ActionsOpts) (*AttestationReset, error) {
c, err := newCrafter(cfg.UseAttestationRemoteState, cfg.CPConnection, crafter.WithLogger(&cfg.Logger))
if err != nil {
return nil, fmt.Errorf("failed to load crafter: %w", err)
}

return &AttestationReset{ActionsOpts: opts, c: c}, nil
return &AttestationReset{ActionsOpts: cfg, c: c}, nil
}

func (action *AttestationReset) Run(ctx context.Context, attestationID, trigger, reason string) error {
Expand Down
2 changes: 1 addition & 1 deletion app/cli/internal/action/attestation_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ type AttestationStatusResultMaterial struct {
}

func NewAttestationStatus(cfg *AttestationStatusOpts) (*AttestationStatus, error) {
c, err := newCrafter(cfg.UseAttestationRemoteState, cfg.CPConnection, &cfg.Logger)
c, err := newCrafter(cfg.UseAttestationRemoteState, cfg.CPConnection, crafter.WithLogger(&cfg.Logger))
if err != nil {
return nil, fmt.Errorf("failed to load crafter: %w", err)
}
Expand Down
32 changes: 27 additions & 5 deletions internal/attestation/crafter/crafter.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ import (
api "github.com/chainloop-dev/chainloop/internal/attestation/crafter/api/attestation/v1"
"github.com/chainloop-dev/chainloop/internal/attestation/crafter/materials"
"github.com/chainloop-dev/chainloop/internal/casclient"
"github.com/chainloop-dev/chainloop/internal/ociauth"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/rs/zerolog"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/timestamppb"
Expand All @@ -59,21 +61,37 @@ type Crafter struct {
Runner supportedRunner
workingDir string
stateManager StateManager
// Authn is used to authenticate with the OCI registry
ociRegistryAuth authn.Keychain
}

var ErrAttestationStateNotLoaded = errors.New("crafting state not loaded")

type NewOpt func(c *Crafter)
type NewOpt func(c *Crafter) error

func WithLogger(l *zerolog.Logger) NewOpt {
return func(c *Crafter) {
return func(c *Crafter) error {
c.logger = l
return nil
}
}

func WithWorkingDirPath(path string) NewOpt {
return func(c *Crafter) {
return func(c *Crafter) error {
c.workingDir = path
return nil
}
}

func WithOCIAuth(server, username, password string) NewOpt {
return func(c *Crafter) error {
k, err := ociauth.NewCredentialsFromRegistry(server, username, password)
if err != nil {
return fmt.Errorf("failed to load OCI credentials: %w", err)
}

c.ociRegistryAuth = k
return nil
}
}

Expand All @@ -86,10 +104,14 @@ func NewCrafter(stateManager StateManager, opts ...NewOpt) (*Crafter, error) {
logger: &noopLogger,
workingDir: cw,
stateManager: stateManager,
// By default we authenticate with the current user's keychain (i.e ~/.docker/config.json)
ociRegistryAuth: authn.DefaultKeychain,
}

for _, opt := range opts {
opt(c)
if err := opt(c); err != nil {
return nil, err
}
}

return c, nil
Expand Down Expand Up @@ -435,7 +457,7 @@ func (c *Crafter) AddMaterial(ctx context.Context, attestationID, key, value str
}

// 3 - Craft resulting material
mt, err := materials.Craft(context.Background(), m, value, casBackend, c.logger)
mt, err := materials.Craft(context.Background(), m, value, casBackend, c.ociRegistryAuth, c.logger)
if err != nil {
return err
}
Expand Down
5 changes: 3 additions & 2 deletions internal/attestation/crafter/materials/materials.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
api "github.com/chainloop-dev/chainloop/internal/attestation/crafter/api/attestation/v1"
"github.com/chainloop-dev/chainloop/internal/casclient"
"github.com/google/go-containerregistry/pkg/authn"
cr_v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/rs/zerolog"
"google.golang.org/protobuf/types/known/timestamppb"
Expand Down Expand Up @@ -135,7 +136,7 @@ type Craftable interface {
Craft(ctx context.Context, value string) (*api.Attestation_Material, error)
}

func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Material, value string, casBackend *casclient.CASBackend, logger *zerolog.Logger) (*api.Attestation_Material, error) {
func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Material, value string, casBackend *casclient.CASBackend, ociAuth authn.Keychain, logger *zerolog.Logger) (*api.Attestation_Material, error) {
var crafter Craftable
var err error

Expand All @@ -147,7 +148,7 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia
case schemaapi.CraftingSchema_Material_STRING:
crafter, err = NewStringCrafter(materialSchema)
case schemaapi.CraftingSchema_Material_CONTAINER_IMAGE:
crafter, err = NewOCIImageCrafter(materialSchema, logger)
crafter, err = NewOCIImageCrafter(materialSchema, ociAuth, logger)
case schemaapi.CraftingSchema_Material_ARTIFACT:
crafter, err = NewArtifactCrafter(materialSchema, casBackend, logger)
case schemaapi.CraftingSchema_Material_SBOM_CYCLONEDX_JSON:
Expand Down
2 changes: 1 addition & 1 deletion internal/attestation/crafter/materials/materials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestCraft(t *testing.T) {
},
}

got, err := materials.Craft(context.TODO(), schema, "test-value", nil, nil)
got, err := materials.Craft(context.TODO(), schema, "test-value", nil, nil, nil)
require.NoError(t, err)
assert.Equal(contractAPI.CraftingSchema_Material_STRING, got.MaterialType)
assert.False(got.UploadedToCas)
Expand Down
7 changes: 4 additions & 3 deletions internal/attestation/crafter/materials/oci_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ import (

type OCIImageCrafter struct {
*crafterCommon
keychain authn.Keychain
}

func NewOCIImageCrafter(schema *schemaapi.CraftingSchema_Material, l *zerolog.Logger) (*OCIImageCrafter, error) {
func NewOCIImageCrafter(schema *schemaapi.CraftingSchema_Material, ociAuth authn.Keychain, l *zerolog.Logger) (*OCIImageCrafter, error) {
if schema.Type != schemaapi.CraftingSchema_Material_CONTAINER_IMAGE {
return nil, fmt.Errorf("material type is not container image")
}

craftCommon := &crafterCommon{logger: l, input: schema}
return &OCIImageCrafter{craftCommon}, nil
return &OCIImageCrafter{craftCommon, ociAuth}, nil
}

func (i *OCIImageCrafter) Craft(_ context.Context, imageRef string) (*api.Attestation_Material, error) {
Expand All @@ -48,7 +49,7 @@ func (i *OCIImageCrafter) Craft(_ context.Context, imageRef string) (*api.Attest
return nil, err
}

descriptor, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
descriptor, err := remote.Get(ref, remote.WithAuthFromKeychain(i.keychain))
if err != nil {
return nil, err
}
Expand Down
12 changes: 12 additions & 0 deletions internal/ociauth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ type Credentials struct {
username, password, server string
}

// Get and validate OCI credentials by providing a registry URL (no path)
func NewCredentialsFromRegistry(repoURI, username, password string) (authn.Keychain, error) {
reg, err := name.NewRegistry(repoURI)
if err != nil {
return nil, fmt.Errorf("invalid registry server URI: %w", err)
}

c := &Credentials{username, password, reg.RegistryStr()}
return validateOCICredentials(c)
}

// Get and validate OCI credentials by providing a repository URL
func NewCredentials(repoURI, username, password string) (authn.Keychain, error) {
repo, err := name.NewRepository(repoURI)
if err != nil {
Expand Down