From 74e50a9311ce30453c625793dbebe6b3661a90c1 Mon Sep 17 00:00:00 2001 From: Uwe Krueger Date: Tue, 3 Sep 2024 15:32:19 +0200 Subject: [PATCH] Resource resolution on CD set (#865) #### What this PR does / why we need it: Introduction resolution of relative resource references of the level of component descriptors. So far, this the resolution was only possible on the level of the complete component version access abstraction. This PR adds such a functionality on the level of plain component descriptors. Therefore, it provides a `ComponentVersionResolver` abstraction for component descriptors, with two implementations: a set of component descriptors and a compound resolver similar to the one for component version accesses. To align the element names in package `compdesc`, the `ComponentReference`ans been renamed to `Reference` (like `Resource` and `Source`). The old type has been deprecated. This functionality is required for the OCM controllers working an deep resources for a component version. To verify the digest of the retrieved content of a resource, a new function `signing.VerifyResourceDigest(cv, index, content)` is provided. it uses the digesting type described by the component version to recalculate the digest of the retrieved resource and verify it against the digest described by the component version. --- api/helper/builder/oci_artifactset.go | 49 ++++ api/helper/builder/ocm_reference.go | 2 +- api/oci/interface.go | 1 + api/ocm/compdesc/componentdescriptor.go | 44 ++-- api/ocm/compdesc/copy_test.go | 2 +- api/ocm/compdesc/default.go | 2 +- api/ocm/compdesc/deprecated.go | 10 +- api/ocm/compdesc/equal_test.go | 14 +- api/ocm/compdesc/generic.go | 31 +++ api/ocm/compdesc/helper.go | 12 +- api/ocm/compdesc/helper_test.go | 10 +- api/ocm/compdesc/meta/v1/identity.go | 16 ++ api/ocm/compdesc/resolver.go | 99 ++++++++ api/ocm/compdesc/resourceref.go | 85 +++++++ api/ocm/compdesc/resourceref_test.go | 143 +++++++++++ api/ocm/compdesc/selector.go | 6 +- api/ocm/compdesc/selectors.go | 4 +- .../versions/ocm.software/v3alpha1/version.go | 8 +- api/ocm/compdesc/versions/v2/version.go | 8 +- api/ocm/cpi/repocpi/view_cv.go | 6 +- api/ocm/elements/common.go | 2 +- api/ocm/elements/digests.go | 2 +- api/ocm/elements/references.go | 4 +- api/ocm/internal/errors.go | 11 +- api/ocm/internal/repository.go | 2 +- api/ocm/testhelper/resources.go | 14 +- api/ocm/tools/signing/digest.go | 103 ++++++++ api/ocm/tools/signing/digest_test.go | 80 +++++++ api/ocm/tools/signing/handle.go | 23 +- api/ocm/tools/signing/handler_test.go | 13 +- api/ocm/tools/signing/options.go | 19 ++ api/ocm/tools/signing/signing_test.go | 60 +++++ api/ocm/tools/signing/store.go | 224 ++++++++++++++++++ api/ocm/tools/signing/store_test.go | 91 +++++++ api/ocm/tools/transfer/merge_test.go | 2 +- .../transfer/transferhandler/spiff/handler.go | 2 +- .../transferhandler/standard/handler.go | 2 +- .../transferhandler/transferhandler.go | 2 +- api/ocm/utils.go | 2 +- cmds/ocm/commands/ocmcmds/cli/download/cmd.go | 3 +- cmds/ocm/commands/ocmcmds/cmd.go | 2 + .../ocmcmds/common/addhdlrs/refs/elements.go | 2 +- .../ocmcmds/common/cmds/signing/cmd.go | 9 +- .../common/handlers/elemhdlr/typehandler.go | 5 + .../handlers/verifiedhdlr/typehandler.go | 91 +++++++ .../common/options/signoption/option.go | 12 +- .../common/options/storeoption/option.go | 72 ++++++ .../ocmcmds/components/sign/cmd_test.go | 53 ++++- .../ocmcmds/components/verify/cmd_test.go | 112 +++++++-- cmds/ocm/commands/ocmcmds/names/names.go | 1 + .../ocmcmds/references/add/cmd_test.go | 12 +- .../ocmcmds/references/common/typehandler.go | 4 +- .../ocmcmds/resources/download/action.go | 26 ++ .../ocmcmds/resources/download/cmd.go | 10 +- .../ocmcmds/resources/download/cmd_test.go | 120 ++++++++++ .../ocmcmds/resources/download/options.go | 2 + cmds/ocm/commands/ocmcmds/verified/cmd.go | 21 ++ cmds/ocm/commands/ocmcmds/verified/get/cmd.go | 106 +++++++++ .../commands/ocmcmds/verified/get/cmd_test.go | 124 ++++++++++ .../ocmcmds/verified/get/suite_test.go | 13 + cmds/ocm/commands/verbs/get/cmd.go | 2 + cmds/ocm/common/output/options.go | 33 ++- docs/reference/ocm_download_cli.md | 12 + docs/reference/ocm_download_resources.md | 12 + docs/reference/ocm_get.md | 1 + docs/reference/ocm_get_verified.md | 44 ++++ docs/reference/ocm_ocm.md | 1 + docs/reference/ocm_sign_componentversions.md | 10 + .../reference/ocm_verify_componentversions.md | 10 + 69 files changed, 1998 insertions(+), 137 deletions(-) create mode 100644 api/ocm/compdesc/generic.go create mode 100644 api/ocm/compdesc/resolver.go create mode 100644 api/ocm/compdesc/resourceref.go create mode 100644 api/ocm/compdesc/resourceref_test.go create mode 100644 api/ocm/tools/signing/digest.go create mode 100644 api/ocm/tools/signing/digest_test.go create mode 100644 api/ocm/tools/signing/store.go create mode 100644 api/ocm/tools/signing/store_test.go create mode 100644 cmds/ocm/commands/ocmcmds/common/handlers/verifiedhdlr/typehandler.go create mode 100644 cmds/ocm/commands/ocmcmds/common/options/storeoption/option.go create mode 100644 cmds/ocm/commands/ocmcmds/verified/cmd.go create mode 100644 cmds/ocm/commands/ocmcmds/verified/get/cmd.go create mode 100644 cmds/ocm/commands/ocmcmds/verified/get/cmd_test.go create mode 100644 cmds/ocm/commands/ocmcmds/verified/get/suite_test.go create mode 100644 docs/reference/ocm_get_verified.md diff --git a/api/helper/builder/oci_artifactset.go b/api/helper/builder/oci_artifactset.go index 55a77fe93..d306ced23 100644 --- a/api/helper/builder/oci_artifactset.go +++ b/api/helper/builder/oci_artifactset.go @@ -1,9 +1,18 @@ package builder import ( + "fmt" + "slices" + + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/oci" + "ocm.software/ocm/api/oci/cpi" "ocm.software/ocm/api/oci/extensions/repositories/artifactset" "ocm.software/ocm/api/utils/accessio" "ocm.software/ocm/api/utils/accessobj" + "ocm.software/ocm/api/utils/blobaccess" + "ocm.software/ocm/api/utils/blobaccess/file" ) const T_OCIARTIFACTSET = "artifact set" @@ -18,3 +27,43 @@ func (b *Builder) ArtifactSet(path string, fmt accessio.FileFormat, f ...func()) r.Annotate(name, value) }}, f) } + +func (b *Builder) ArtifactSetBlob(main string, f ...func()) { + b.expect(b.blob, T_BLOBACCESS) + arch, err := vfs.TempFile(b.FileSystem(), "", "artifact-*.tgz") + b.failOn(err) + b.FileSystem().Remove(arch.Name()) + + r, err := artifactset.Open(accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, arch.Name(), 0o777, accessio.FormatTGZ, accessio.PathFileSystem(b.FileSystem())) + b.failOn(err) + + b.configure(&ociNamespace{NamespaceAccess: &artifactSink{b, arch.Name(), main, r, ""}, kind: T_OCIARTIFACTSET, annofunc: func(name, value string) { + r.Annotate(name, value) + }}, append(f, func() { + b.Annotation(artifactset.MAINARTIFACT_ANNOTATION, main) + })) +} + +type artifactSink struct { + builder *Builder + arch string + main string + oci.NamespaceAccess + mime string +} + +func (s *artifactSink) AddArtifact(art cpi.Artifact, tags ...string) (blobaccess.BlobAccess, error) { + if slices.Contains(tags, s.main) { + s.mime = art.Artifact().MimeType() + } + return s.NamespaceAccess.AddArtifact(art, tags...) +} + +func (s *artifactSink) Close() error { + if s.mime == "" { + s.NamespaceAccess.Close() + return fmt.Errorf("main artifact not defined") + } + *s.builder.blob = blobaccess.ForTemporaryFilePath(artifactset.MediaType(s.mime), s.arch, file.WithFileSystem(s.builder.FileSystem())) + return s.NamespaceAccess.Close() +} diff --git a/api/helper/builder/ocm_reference.go b/api/helper/builder/ocm_reference.go index c6a43ad83..93fc02f93 100644 --- a/api/helper/builder/ocm_reference.go +++ b/api/helper/builder/ocm_reference.go @@ -7,7 +7,7 @@ import ( type ocmReference struct { base - meta compdesc.ComponentReference + meta compdesc.Reference } const T_OCMREF = "reference" diff --git a/api/oci/interface.go b/api/oci/interface.go index 3cde1e783..4c78a52b4 100644 --- a/api/oci/interface.go +++ b/api/oci/interface.go @@ -30,6 +30,7 @@ type ( IntermediateRepositorySpecAspect = internal.IntermediateRepositorySpecAspect GenericRepositorySpec = internal.GenericRepositorySpec ArtifactAccess = internal.ArtifactAccess + Artifact = internal.Artifact NamespaceLister = internal.NamespaceLister NamespaceAccess = internal.NamespaceAccess ManifestAccess = internal.ManifestAccess diff --git a/api/ocm/compdesc/componentdescriptor.go b/api/ocm/compdesc/componentdescriptor.go index f8fe31ea5..bf111c802 100644 --- a/api/ocm/compdesc/componentdescriptor.go +++ b/api/ocm/compdesc/componentdescriptor.go @@ -10,6 +10,7 @@ import ( "ocm.software/ocm/api/ocm/compdesc/equivalent" metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" "ocm.software/ocm/api/ocm/selectors/accessors" + "ocm.software/ocm/api/utils/errkind" "ocm.software/ocm/api/utils/runtime" "ocm.software/ocm/api/utils/semverutils" ) @@ -18,7 +19,13 @@ const InternalSchemaVersion = "internal" var NotFound = errors.ErrNotFound() -const KIND_REFERENCE = "component reference" +const ( + KIND_COMPONENT = errkind.KIND_COMPONENT + KIND_COMPONENTVERSION = "component version" + KIND_RESOURCE = "component resource" + KIND_SOURCE = "component source" + KIND_REFERENCE = "component reference" +) const ComponentDescriptorFileName = "component-descriptor.yaml" @@ -716,7 +723,7 @@ func NewResourceMeta(name string, typ string, relation metav1.ResourceRelation) } } -type References []ComponentReference +type References []Reference func (r References) Equivalent(o References) equivalent.EqualState { return EquivalentElems(r, o) @@ -753,10 +760,10 @@ func (r References) Copy() References { return out } -// ComponentReference describes the reference to another component in the registry. +// Reference describes the reference to another component in the registry. // +k8s:deepcopy-gen=true // +k8s:openapi-gen=true -type ComponentReference struct { +type Reference struct { ElementMeta `json:",inline"` // ComponentName describes the remote name of the referenced object ComponentName string `json:"componentName"` @@ -765,8 +772,11 @@ type ComponentReference struct { Digest *metav1.DigestSpec `json:"digest,omitempty"` } -func NewComponentReference(name, componentName, version string, extraIdentity metav1.Identity) *ComponentReference { - return &ComponentReference{ +// Deprecated: use Reference. +type ComponentReference = Reference + +func NewComponentReference(name, componentName, version string, extraIdentity metav1.Identity) *Reference { + return &Reference{ ElementMeta: ElementMeta{ Name: name, Version: version, @@ -776,41 +786,41 @@ func NewComponentReference(name, componentName, version string, extraIdentity me } } -func (r ComponentReference) String() string { +func (r Reference) String() string { return fmt.Sprintf("%s[%s:%s]", r.Name, r.ComponentName, r.Version) } // WithVersion returns a new reference with a dedicated version. -func (o *ComponentReference) WithVersion(v string) *ComponentReference { +func (o *Reference) WithVersion(v string) *Reference { n := o.Copy() n.Version = v return n } // WithExtraIdentity returns a new reference with a dedicated version. -func (o *ComponentReference) WithExtraIdentity(extras ...string) *ComponentReference { +func (o *Reference) WithExtraIdentity(extras ...string) *Reference { n := o.Copy() n.AddExtraIdentity(NewExtraIdentity(extras...)) return n } // Fresh returns a digest-free copy. -func (o *ComponentReference) Fresh() *ComponentReference { +func (o *Reference) Fresh() *Reference { n := o.Copy() n.Digest = nil return n } -func (r *ComponentReference) GetDigest() *metav1.DigestSpec { +func (r *Reference) GetDigest() *metav1.DigestSpec { return r.Digest } -func (r *ComponentReference) SetDigest(d *metav1.DigestSpec) { +func (r *Reference) SetDigest(d *metav1.DigestSpec) { r.Digest = d } -func (r *ComponentReference) Equivalent(e ElementMetaAccessor) equivalent.EqualState { - if o, ok := e.(*ComponentReference); !ok { +func (r *Reference) Equivalent(e ElementMetaAccessor) equivalent.EqualState { + if o, ok := e.(*Reference); !ok { state := equivalent.StateNotLocalHashEqual() if r.Digest != nil { state = state.Apply(equivalent.StateNotArtifactEqual(true)) @@ -831,12 +841,12 @@ func (r *ComponentReference) Equivalent(e ElementMetaAccessor) equivalent.EqualS } } -func (r *ComponentReference) GetComponentName() string { +func (r *Reference) GetComponentName() string { return r.ComponentName } -func (r *ComponentReference) Copy() *ComponentReference { - return &ComponentReference{ +func (r *Reference) Copy() *Reference { + return &Reference{ ElementMeta: *r.ElementMeta.Copy(), ComponentName: r.ComponentName, Digest: r.Digest.Copy(), diff --git a/api/ocm/compdesc/copy_test.go b/api/ocm/compdesc/copy_test.go index 1cb1c69f2..f854d774f 100644 --- a/api/ocm/compdesc/copy_test.go +++ b/api/ocm/compdesc/copy_test.go @@ -70,7 +70,7 @@ var _ = Describe("Component Descripor Copy Test Suitet", func() { }, } cd.References = compdesc.References{ - compdesc.ComponentReference{ + compdesc.Reference{ ElementMeta: compdesc.ElementMeta{}, ComponentName: "", Digest: nil, diff --git a/api/ocm/compdesc/default.go b/api/ocm/compdesc/default.go index bb9879a8e..af504034f 100644 --- a/api/ocm/compdesc/default.go +++ b/api/ocm/compdesc/default.go @@ -14,7 +14,7 @@ func DefaultComponent(component *ComponentDescriptor) *ComponentDescriptor { component.Sources = make([]Source, 0) } if component.References == nil { - component.References = make([]ComponentReference, 0) + component.References = make([]Reference, 0) } if component.Resources == nil { component.Resources = make([]Resource, 0) diff --git a/api/ocm/compdesc/deprecated.go b/api/ocm/compdesc/deprecated.go index 9b10b32d5..cea854ea0 100644 --- a/api/ocm/compdesc/deprecated.go +++ b/api/ocm/compdesc/deprecated.go @@ -200,8 +200,8 @@ func (cd *ComponentDescriptor) GetSourcesByName(name string, selectors ...Identi // GetComponentReferences returns all component references that matches the given selectors. // // Deprectated: use GetReferences with appropriate selectors. -func (cd *ComponentDescriptor) GetComponentReferences(selectors ...IdentitySelector) ([]ComponentReference, error) { - refs := make([]ComponentReference, 0) +func (cd *ComponentDescriptor) GetComponentReferences(selectors ...IdentitySelector) ([]Reference, error) { + refs := make([]Reference, 0) for _, ref := range cd.References { ok, err := selector.MatchSelectors(ref.GetIdentity(cd.References), selectors...) if err != nil { @@ -220,13 +220,13 @@ func (cd *ComponentDescriptor) GetComponentReferences(selectors ...IdentitySelec // GetComponentReferenceIndex returns the index of a given component reference. // If the index is not found -1 is returned. // Deprecated: use GetReferenceIndex. -func (cd *ComponentDescriptor) GetComponentReferenceIndex(ref ComponentReference) int { +func (cd *ComponentDescriptor) GetComponentReferenceIndex(ref Reference) int { return cd.GetReferenceIndex(ref.GetMeta()) } // GetReferenceAccessByIdentity returns a pointer to the reference that matches the given identity. // Deprectated: use GetReferenceByIdentity. -func (cd *ComponentDescriptor) GetReferenceAccessByIdentity(id v1.Identity) *ComponentReference { +func (cd *ComponentDescriptor) GetReferenceAccessByIdentity(id v1.Identity) *Reference { dig := id.Digest() for i, ref := range cd.References { if bytes.Equal(ref.GetIdentityDigest(cd.Resources), dig) { @@ -270,7 +270,7 @@ func (cd *ComponentDescriptor) GetReferencesBySelectors(selectors []IdentitySele if !ok { continue } - references = append(references, *selctx.ComponentReference) + references = append(references, *selctx.Reference) } if len(references) == 0 { return references, NotFound diff --git a/api/ocm/compdesc/equal_test.go b/api/ocm/compdesc/equal_test.go index 9007cb91e..7363032ad 100644 --- a/api/ocm/compdesc/equal_test.go +++ b/api/ocm/compdesc/equal_test.go @@ -487,10 +487,10 @@ var _ = Describe("equivalence", func() { }) Context("reference", func() { - var a, b *compdesc.ComponentReference + var a, b *compdesc.Reference BeforeEach(func() { - a = &compdesc.ComponentReference{ + a = &compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "r1", Version: "v1", @@ -536,7 +536,7 @@ var _ = Describe("equivalence", func() { BeforeEach(func() { a = compdesc.References{ - compdesc.ComponentReference{ + compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "s1", Version: "v1", @@ -544,7 +544,7 @@ var _ = Describe("equivalence", func() { }, ComponentName: "c1", }, - compdesc.ComponentReference{ + compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "s2", Version: "v1", @@ -583,7 +583,7 @@ var _ = Describe("equivalence", func() { }) It("handles additional entry", func() { - b = append(b, compdesc.ComponentReference{ + b = append(b, compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "s3", ExtraIdentity: compdesc.NewExtraIdentity("platform", "linux"), @@ -596,7 +596,7 @@ var _ = Describe("equivalence", func() { }) It("handles additional entry without any other metadata", func() { - b = append(b, compdesc.ComponentReference{ + b = append(b, compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "s3", ExtraIdentity: compdesc.NewExtraIdentity("platform", "linux"), @@ -608,7 +608,7 @@ var _ = Describe("equivalence", func() { }) It("handles additional entry without any other metadata", func() { - b = append(b, compdesc.ComponentReference{ + b = append(b, compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "s3", ExtraIdentity: compdesc.NewExtraIdentity("platform", "linux"), diff --git a/api/ocm/compdesc/generic.go b/api/ocm/compdesc/generic.go new file mode 100644 index 000000000..e9a37171b --- /dev/null +++ b/api/ocm/compdesc/generic.go @@ -0,0 +1,31 @@ +package compdesc + +import ( + "encoding/json" + + "github.com/mandelsoft/goutils/generics" +) + +type GenericComponentDescriptor ComponentDescriptor + +var ( + _ json.Marshaler = (*GenericComponentDescriptor)(nil) + _ json.Unmarshaler = (*GenericComponentDescriptor)(nil) +) + +func (g GenericComponentDescriptor) MarshalJSON() ([]byte, error) { + return Encode(generics.Pointer(ComponentDescriptor(g)), DefaultJSONCodec) +} + +func (g *GenericComponentDescriptor) UnmarshalJSON(bytes []byte) error { + cd, err := Decode(bytes, DefaultJSONCodec) + if err != nil { + return err + } + *g = *((*GenericComponentDescriptor)(cd)) + return nil +} + +func (g *GenericComponentDescriptor) Descriptor() *ComponentDescriptor { + return (*ComponentDescriptor)(g) +} diff --git a/api/ocm/compdesc/helper.go b/api/ocm/compdesc/helper.go index e6cc28fd5..fa48889fe 100644 --- a/api/ocm/compdesc/helper.go +++ b/api/ocm/compdesc/helper.go @@ -175,24 +175,24 @@ func (cd *ComponentDescriptor) GetSourceIndex(src *SourceMeta) int { } // GetReferenceByIdentity returns reference that matches the given identity. -func (cd *ComponentDescriptor) GetReferenceByIdentity(id v1.Identity) (ComponentReference, error) { +func (cd *ComponentDescriptor) GetReferenceByIdentity(id v1.Identity) (Reference, error) { dig := id.Digest() for _, ref := range cd.References { if bytes.Equal(ref.GetIdentityDigest(cd.Resources), dig) { return ref, nil } } - return ComponentReference{}, errors.ErrNotFound(KIND_REFERENCE, id.String()) + return Reference{}, errors.ErrNotFound(KIND_REFERENCE, id.String()) } -func (cd *ComponentDescriptor) SelectReferences(sel ...refsel.Selector) ([]ComponentReference, error) { +func (cd *ComponentDescriptor) SelectReferences(sel ...refsel.Selector) ([]Reference, error) { err := selectors.ValidateSelectors(sel...) if err != nil { return nil, err } list := MapToSelectorElementList(cd.References) - result := []ComponentReference{} + result := []Reference{} for _, r := range cd.References { if len(sel) > 0 { mr := MapToSelectorReference(&r) @@ -207,8 +207,8 @@ func (cd *ComponentDescriptor) SelectReferences(sel ...refsel.Selector) ([]Compo return result, nil } -func (cd *ComponentDescriptor) GetReferences() []ComponentReference { - result := []ComponentReference{} +func (cd *ComponentDescriptor) GetReferences() []Reference { + result := []Reference{} for _, r := range cd.References { result = append(result, r) } diff --git a/api/ocm/compdesc/helper_test.go b/api/ocm/compdesc/helper_test.go index e4385d42a..3ba37220a 100644 --- a/api/ocm/compdesc/helper_test.go +++ b/api/ocm/compdesc/helper_test.go @@ -281,7 +281,7 @@ var _ = Describe("helper", func() { Context("reference selection", func() { cd := &compdesc.ComponentDescriptor{} - r1v1 := compdesc.ComponentReference{ + r1v1 := compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "r1", Version: "v1", @@ -296,14 +296,14 @@ var _ = Describe("helper", func() { }, ComponentName: "c1", } - r1v2 := compdesc.ComponentReference{ + r1v2 := compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "r1", Version: "v2", }, ComponentName: "c1", } - r2v1 := compdesc.ComponentReference{ + r2v1 := compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "r2", Version: "v1", @@ -318,7 +318,7 @@ var _ = Describe("helper", func() { }, ComponentName: "c2", } - r3v2 := compdesc.ComponentReference{ + r3v2 := compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "r3", Version: "v2", @@ -345,7 +345,7 @@ var _ = Describe("helper", func() { ComponentName: "c3", } - r4v3 := compdesc.ComponentReference{ + r4v3 := compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "r4", Version: "v3", diff --git a/api/ocm/compdesc/meta/v1/identity.go b/api/ocm/compdesc/meta/v1/identity.go index 80ab79d00..bbea5a607 100644 --- a/api/ocm/compdesc/meta/v1/identity.go +++ b/api/ocm/compdesc/meta/v1/identity.go @@ -103,6 +103,22 @@ func (i Identity) Remove(name string) bool { return false } +// ExtraIdentity extracts the extra identity part of an identity. +func (i Identity) ExtraIdentity() Identity { + if i == nil { + return nil + } + if _, ok := i[SystemIdentityName]; !ok { + return i + } + if len(i) == 1 { + return nil + } + r := i.Copy() + delete(r, SystemIdentityName) + return r +} + func (i Identity) String() string { if i == nil { return "" diff --git a/api/ocm/compdesc/resolver.go b/api/ocm/compdesc/resolver.go new file mode 100644 index 000000000..791533ff9 --- /dev/null +++ b/api/ocm/compdesc/resolver.go @@ -0,0 +1,99 @@ +package compdesc + +import ( + "sync" + + "github.com/mandelsoft/goutils/errors" + + common "ocm.software/ocm/api/utils/misc" +) + +type ComponentVersionResolver interface { + LookupComponentVersion(name string, version string) (*ComponentDescriptor, error) +} + +//////////////////////////////////////////////////////////////////////////////// + +type ComponentVersionSet struct { + lock sync.RWMutex + cds map[common.NameVersion]*ComponentDescriptor +} + +var _ ComponentVersionResolver = (*ComponentVersionSet)(nil) + +func NewComponentVersionSet(cds ...*ComponentDescriptor) *ComponentVersionSet { + r := map[common.NameVersion]*ComponentDescriptor{} + for _, cd := range cds { + r[common.NewNameVersion(cd.Name, cd.Version)] = cd.Copy() + } + return &ComponentVersionSet{cds: r} +} + +func (c *ComponentVersionSet) LookupComponentVersion(name string, version string) (*ComponentDescriptor, error) { + c.lock.RLock() + defer c.lock.RUnlock() + + nv := common.NewNameVersion(name, version) + + cd := c.cds[nv] + if cd == nil { + return nil, errors.ErrNotFound(KIND_COMPONENTVERSION, nv.String()) + } + return cd, nil +} + +func (c *ComponentVersionSet) AddVersion(cd *ComponentDescriptor) { + c.lock.Lock() + defer c.lock.Unlock() + + c.cds[common.NewNameVersion(cd.Name, cd.Version)] = cd.Copy() +} + +//////////////////////////////////////////////////////////////////////////////// + +type CompoundResolver struct { + lock sync.RWMutex + resolvers []ComponentVersionResolver +} + +var _ ComponentVersionResolver = (*CompoundResolver)(nil) + +func NewCompoundResolver(res ...ComponentVersionResolver) ComponentVersionResolver { + for i := 0; i < len(res); i++ { + if res[i] == nil { + res = append(res[:i], res[i+1:]...) + i-- + } + } + if len(res) == 1 { + return res[0] + } + return &CompoundResolver{resolvers: res} +} + +func (c *CompoundResolver) LookupComponentVersion(name string, version string) (*ComponentDescriptor, error) { + c.lock.RLock() + defer c.lock.RUnlock() + for _, r := range c.resolvers { + if r == nil { + continue + } + cv, err := r.LookupComponentVersion(name, version) + if err == nil && cv != nil { + return cv, nil + } + if !errors.IsErrNotFoundKind(err, KIND_COMPONENTVERSION) && !errors.IsErrNotFoundKind(err, KIND_COMPONENT) { + return nil, err + } + } + return nil, errors.ErrNotFound(KIND_REFERENCE, common.NewNameVersion(name, version).String()) +} + +func (c *CompoundResolver) AddResolver(r ComponentVersionResolver) { + c.lock.Lock() + defer c.lock.Unlock() + + if r != nil { + c.resolvers = append(c.resolvers, r) + } +} diff --git a/api/ocm/compdesc/resourceref.go b/api/ocm/compdesc/resourceref.go new file mode 100644 index 000000000..91a467b38 --- /dev/null +++ b/api/ocm/compdesc/resourceref.go @@ -0,0 +1,85 @@ +package compdesc + +import ( + "fmt" + + "github.com/mandelsoft/goutils/errors" + + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + common "ocm.software/ocm/api/utils/misc" +) + +func ResolveReferencePath(cv *ComponentDescriptor, path []metav1.Identity, resolver ComponentVersionResolver) (*ComponentDescriptor, error) { + if cv == nil { + return nil, fmt.Errorf("no component version specified") + } + + eff := cv + for _, cr := range path { + cref, err := eff.GetReferenceByIdentity(cr) + if err != nil { + return nil, errors.Wrapf(err, "%s", common.VersionedElementKey(cv)) + } + + compoundResolver := NewCompoundResolver(NewComponentVersionSet(cv), resolver) + eff, err = compoundResolver.LookupComponentVersion(cref.GetComponentName(), cref.GetVersion()) + if err != nil { + return nil, errors.Wrapf(err, "cannot resolve component version for reference %s", cr.String()) + } + if eff == nil { + return nil, errors.ErrNotFound(KIND_COMPONENTVERSION, cref.String()) + } + } + return eff, nil +} + +func MatchResourceReference(cv *ComponentDescriptor, typ string, ref metav1.ResourceReference, resolver ComponentVersionResolver) (*Resource, *ComponentDescriptor, error) { + eff, err := ResolveReferencePath(cv, ref.ReferencePath, resolver) + if err != nil { + return nil, nil, err + } + + if len(eff.Resources) == 0 && len(ref.Resource) == 0 { + return nil, nil, errors.ErrNotFound(KIND_RESOURCE) + } +outer: + for i, r := range eff.Resources { + if r.Type != typ && typ != "" { + continue + } + for k, v := range ref.Resource { + switch k { + case metav1.SystemIdentityName: + if v != r.Name { + continue outer + } + case metav1.SystemIdentityVersion: + if v != r.Version { + continue outer + } + default: + if r.ExtraIdentity == nil || r.ExtraIdentity[k] != v { + continue outer + } + } + } + return &eff.Resources[i], eff, nil + } + return nil, nil, errors.ErrNotFound(KIND_RESOURCE, ref.Resource.String()) +} + +func ResolveResourceReference(cv *ComponentDescriptor, ref metav1.ResourceReference, resolver ComponentVersionResolver) (*Resource, *ComponentDescriptor, error) { + if len(ref.Resource) == 0 || len(ref.Resource["name"]) == 0 { + return nil, nil, errors.Newf("at least resource name must be specified for resource reference") + } + + eff, err := ResolveReferencePath(cv, ref.ReferencePath, resolver) + if err != nil { + return nil, nil, err + } + r, err := eff.GetResourceByIdentity(ref.Resource) + if err != nil { + return nil, nil, err + } + return &r, eff, nil +} diff --git a/api/ocm/compdesc/resourceref_test.go b/api/ocm/compdesc/resourceref_test.go new file mode 100644 index 000000000..077b65162 --- /dev/null +++ b/api/ocm/compdesc/resourceref_test.go @@ -0,0 +1,143 @@ +package compdesc_test + +import ( + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/ocm/compdesc" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" +) + +const ( + VERSION = "v1" + COMPONENT = "github.com/mandelsoft/test" + COMPONENT2 = "github.com/mandelsoft/test2" + COMPONENT3 = "github.com/mandelsoft/test3" + + EXTRA_ATTR = "platform" + EXTRA_VAL = "test" +) + +func CheckResourceRef(cv *compdesc.ComponentDescriptor, resolver compdesc.ComponentVersionResolver, comp string, rsc metav1.Identity, path ...metav1.Identity) { + ref := metav1.NewNestedResourceRef(rsc, path) + + r, cd := Must2(compdesc.ResolveResourceReference(cv, ref, resolver)) + ExpectWithOffset(1, r).NotTo(BeNil()) + ExpectWithOffset(1, cd).NotTo(BeNil()) + + ExpectWithOffset(1, cd.Name).To(Equal(comp)) + ExpectWithOffset(1, r.Name).To(Equal(rsc.Get(compdesc.SystemIdentityName))) + ExpectWithOffset(1, r.ExtraIdentity).To(Equal(rsc.ExtraIdentity())) +} + +var _ = Describe("resolving local resource references", func() { + var set *compdesc.ComponentVersionSet + + BeforeEach(func() { + set = compdesc.NewComponentVersionSet() + + cd := compdesc.New(COMPONENT, VERSION) + cd.Resources = append(cd.Resources, compdesc.Resource{ + ResourceMeta: compdesc.ResourceMeta{ + ElementMeta: compdesc.ElementMeta{ + Name: "testata", + Version: "", + }, + Type: "PlainText", + Relation: metav1.LocalRelation, + }, + }) + set.AddVersion(cd) + + cd = compdesc.New(COMPONENT2, VERSION) + cd.Resources = append(cd.Resources, compdesc.Resource{ + ResourceMeta: compdesc.ResourceMeta{ + ElementMeta: compdesc.ElementMeta{ + Name: "otherdata", + Version: "", + }, + Type: "PlainText", + Relation: metav1.LocalRelation, + }, + }) + cd.Resources = append(cd.Resources, compdesc.Resource{ + ResourceMeta: compdesc.ResourceMeta{ + ElementMeta: compdesc.ElementMeta{ + Name: "otherdata", + Version: "", + ExtraIdentity: metav1.NewExtraIdentity(EXTRA_ATTR, EXTRA_VAL), + }, + Type: "PlainText", + Relation: metav1.LocalRelation, + }, + }) + cd.References = append(cd.References, compdesc.Reference{ + ElementMeta: compdesc.ElementMeta{ + Name: "ref", + Version: VERSION, + }, + ComponentName: COMPONENT, + }) + set.AddVersion(cd) + + cd = compdesc.New(COMPONENT3, VERSION) + cd.Resources = append(cd.Resources, compdesc.Resource{ + ResourceMeta: compdesc.ResourceMeta{ + ElementMeta: compdesc.ElementMeta{ + Name: "topdata", + Version: "", + }, + Type: "PlainText", + Relation: metav1.LocalRelation, + }, + }) + cd.References = append(cd.References, compdesc.Reference{ + ElementMeta: compdesc.ElementMeta{ + Name: "nested", + Version: VERSION, + }, + ComponentName: COMPONENT2, + }) + cd.References = append(cd.References, compdesc.Reference{ + ElementMeta: compdesc.ElementMeta{ + Name: "nested", + Version: VERSION, + ExtraIdentity: metav1.NewExtraIdentity(EXTRA_ATTR, EXTRA_VAL), + }, + ComponentName: COMPONENT2, + }) + set.AddVersion(cd) + }) + + It("resolves a direct local resource", func() { + CheckResourceRef(Must(set.LookupComponentVersion(COMPONENT3, VERSION)), set, COMPONENT3, metav1.NewIdentity("topdata")) + }) + + It("resolves an indirect resource", func() { + CheckResourceRef(Must(set.LookupComponentVersion(COMPONENT3, VERSION)), set, COMPONENT2, metav1.NewIdentity("otherdata"), metav1.NewIdentity("nested")) + }) + It("resolves an indirect resource with extra id", func() { + CheckResourceRef(Must(set.LookupComponentVersion(COMPONENT3, VERSION)), set, COMPONENT2, metav1.NewIdentity("otherdata", EXTRA_ATTR, EXTRA_VAL), metav1.NewIdentity("nested")) + }) + + It("fails resolving an indirect resource with non existing extra id", func() { + ref := metav1.NewNestedResourceRef(metav1.NewIdentity("otherdata", EXTRA_ATTR, "dummy"), []metav1.Identity{metav1.NewIdentity("nested")}) + ExpectError(compdesc.ResolveResourceReference(Must(set.LookupComponentVersion(COMPONENT3, VERSION)), ref, set)).To( + MatchError("not found")) + }) + + It("skips an intermediate component version", func() { + CheckResourceRef(Must(set.LookupComponentVersion(COMPONENT3, VERSION)), set, COMPONENT, metav1.NewIdentity("testata"), metav1.NewIdentity("nested"), metav1.NewIdentity("ref")) + }) + + It("skips an intermediate component version with extra id", func() { + CheckResourceRef(Must(set.LookupComponentVersion(COMPONENT3, VERSION)), set, COMPONENT, metav1.NewIdentity("testata"), metav1.NewIdentity("nested", EXTRA_ATTR, EXTRA_VAL), metav1.NewIdentity("ref")) + }) + + It("fails resolving an indirect resource with non existing intermediate ref", func() { + ref := metav1.NewNestedResourceRef(metav1.NewIdentity("testdata"), []metav1.Identity{metav1.NewIdentity("nested", EXTRA_ATTR, "dummy")}) + ExpectError(compdesc.ResolveResourceReference(Must(set.LookupComponentVersion(COMPONENT3, VERSION)), ref, set)).To( + MatchError("github.com/mandelsoft/test3:v1: component reference \"\"name\"=\"nested\",\"platform\"=\"dummy\"\" not found")) + }) +}) diff --git a/api/ocm/compdesc/selector.go b/api/ocm/compdesc/selector.go index 568f67bbb..ef70176b2 100644 --- a/api/ocm/compdesc/selector.go +++ b/api/ocm/compdesc/selector.go @@ -49,13 +49,13 @@ func MapToSelectorSource(r *Source) accessors.SourceAccessor { //////////////////////////////////////////////////////////////////////////////// type refAcc struct { - *ComponentReference + *Reference } func (a refAcc) GetMeta() accessors.ElementMeta { - return a.ComponentReference.GetMeta() + return a.Reference.GetMeta() } -func MapToSelectorReference(r *ComponentReference) accessors.ReferenceAccessor { +func MapToSelectorReference(r *Reference) accessors.ReferenceAccessor { return refAcc{r} } diff --git a/api/ocm/compdesc/selectors.go b/api/ocm/compdesc/selectors.go index ce3ec3776..0e079d3ec 100644 --- a/api/ocm/compdesc/selectors.go +++ b/api/ocm/compdesc/selectors.go @@ -589,14 +589,14 @@ func (s ReferenceSelectorFunc) MatchReference(obj ReferenceSelectionContext) (bo } type referenceSelectionContext struct { - *ComponentReference + *Reference identity } // Deprecated: use package selectors and its sub packages. func NewReferenceSelectionContext(index int, refs References) ReferenceSelectionContext { return &referenceSelectionContext{ - ComponentReference: &refs[index], + Reference: &refs[index], identity: identity{ accessor: refs, index: index, diff --git a/api/ocm/compdesc/versions/ocm.software/v3alpha1/version.go b/api/ocm/compdesc/versions/ocm.software/v3alpha1/version.go index f39556867..6c36971e5 100644 --- a/api/ocm/compdesc/versions/ocm.software/v3alpha1/version.go +++ b/api/ocm/compdesc/versions/ocm.software/v3alpha1/version.go @@ -91,8 +91,8 @@ func (v *DescriptorVersion) ConvertTo(obj compdesc.ComponentDescriptorVersion) ( return out, nil } -func convertReferenceTo(in Reference) compdesc.ComponentReference { - return compdesc.ComponentReference{ +func convertReferenceTo(in Reference) compdesc.Reference { + return compdesc.Reference{ ElementMeta: convertElementmetaTo(in.ElementMeta), ComponentName: in.ComponentName, Digest: in.Digest.Copy(), @@ -212,7 +212,7 @@ func (v *DescriptorVersion) ConvertFrom(in *compdesc.ComponentDescriptor) (compd return out, nil } -func convertReferenceFrom(in compdesc.ComponentReference) Reference { +func convertReferenceFrom(in compdesc.Reference) Reference { return Reference{ ElementMeta: convertElementmetaFrom(in.ElementMeta), ComponentName: in.ComponentName, @@ -220,7 +220,7 @@ func convertReferenceFrom(in compdesc.ComponentReference) Reference { } } -func convertReferencesFrom(in []compdesc.ComponentReference) []Reference { +func convertReferencesFrom(in []compdesc.Reference) []Reference { if in == nil { return nil } diff --git a/api/ocm/compdesc/versions/v2/version.go b/api/ocm/compdesc/versions/v2/version.go index 3d2ea687b..108d47273 100644 --- a/api/ocm/compdesc/versions/v2/version.go +++ b/api/ocm/compdesc/versions/v2/version.go @@ -97,8 +97,8 @@ func (v *DescriptorVersion) ConvertTo(obj compdesc.ComponentDescriptorVersion) ( return out, nil } -func convertComponentReferenceTo(in ComponentReference) compdesc.ComponentReference { - return compdesc.ComponentReference{ +func convertComponentReferenceTo(in ComponentReference) compdesc.Reference { + return compdesc.Reference{ ElementMeta: convertElementMetaTo(in.ElementMeta), ComponentName: in.ComponentName, Digest: in.Digest.Copy(), @@ -234,7 +234,7 @@ func (v *DescriptorVersion) ConvertFrom(in *compdesc.ComponentDescriptor) (compd return out, nil } -func convertComponentReferenceFrom(in compdesc.ComponentReference) ComponentReference { +func convertComponentReferenceFrom(in compdesc.Reference) ComponentReference { return ComponentReference{ ElementMeta: convertElementMetaFrom(in.ElementMeta), ComponentName: in.ComponentName, @@ -242,7 +242,7 @@ func convertComponentReferenceFrom(in compdesc.ComponentReference) ComponentRefe } } -func convertComponentReferencesFrom(in []compdesc.ComponentReference) []ComponentReference { +func convertComponentReferencesFrom(in []compdesc.Reference) []ComponentReference { if in == nil { return nil } diff --git a/api/ocm/cpi/repocpi/view_cv.go b/api/ocm/cpi/repocpi/view_cv.go index 43916063b..0320314fe 100644 --- a/api/ocm/cpi/repocpi/view_cv.go +++ b/api/ocm/cpi/repocpi/view_cv.go @@ -791,7 +791,7 @@ func (c *componentVersionAccessView) GetSources() []cpi.SourceAccess { return result } -func (c *componentVersionAccessView) SelectReferences(sel ...refsel.Selector) ([]compdesc.ComponentReference, error) { +func (c *componentVersionAccessView) SelectReferences(sel ...refsel.Selector) ([]compdesc.Reference, error) { err := selectors.ValidateSelectors(sel...) if err != nil { return nil, err @@ -799,7 +799,7 @@ func (c *componentVersionAccessView) SelectReferences(sel ...refsel.Selector) ([ return c.GetDescriptor().SelectReferences(sel...) } -func (c *componentVersionAccessView) GetReferences() []compdesc.ComponentReference { +func (c *componentVersionAccessView) GetReferences() []compdesc.Reference { return c.GetDescriptor().GetReferences() } @@ -861,7 +861,7 @@ func (c *componentVersionAccessView) GetReferencesBySelectors(selectors []compde if !ok { continue } - references = append(references, *selctx.ComponentReference) + references = append(references, *selctx.Reference) } if len(references) == 0 { return references, compdesc.NotFound diff --git a/api/ocm/elements/common.go b/api/ocm/elements/common.go index 1b210eeab..8d5d2eeb9 100644 --- a/api/ocm/elements/common.go +++ b/api/ocm/elements/common.go @@ -31,7 +31,7 @@ func (o commonOption) ApplyToSourceMeta(m *compdesc.SourceMeta) error { return o.apply(&m.ElementMeta) } -func (o commonOption) ApplyToReference(m *compdesc.ComponentReference) error { +func (o commonOption) ApplyToReference(m *compdesc.Reference) error { return o.apply(&m.ElementMeta) } diff --git a/api/ocm/elements/digests.go b/api/ocm/elements/digests.go index 4aa372761..cd32e3be6 100644 --- a/api/ocm/elements/digests.go +++ b/api/ocm/elements/digests.go @@ -14,7 +14,7 @@ type ResourceReferenceOption interface { type digest metav1.DigestSpec -func (o *digest) ApplyToReference(m *compdesc.ComponentReference) error { +func (o *digest) ApplyToReference(m *compdesc.Reference) error { if !(*metav1.DigestSpec)(o).IsNone() { m.Digest = (*metav1.DigestSpec)(o).Copy() } diff --git a/api/ocm/elements/references.go b/api/ocm/elements/references.go index 64bb1619f..60f1b5fb3 100644 --- a/api/ocm/elements/references.go +++ b/api/ocm/elements/references.go @@ -7,10 +7,10 @@ import ( ) type ReferenceOption interface { - ApplyToReference(reference *compdesc.ComponentReference) error + ApplyToReference(reference *compdesc.Reference) error } -func Reference(name, comp, vers string, opts ...ReferenceOption) (*compdesc.ComponentReference, error) { +func Reference(name, comp, vers string, opts ...ReferenceOption) (*compdesc.Reference, error) { m := compdesc.NewComponentReference(name, comp, vers, nil) list := errors.ErrList() for _, o := range opts { diff --git a/api/ocm/internal/errors.go b/api/ocm/internal/errors.go index 8c52e130d..eabcd3233 100644 --- a/api/ocm/internal/errors.go +++ b/api/ocm/internal/errors.go @@ -10,13 +10,14 @@ import ( ) const ( - KIND_REPOSITORY = "ocm repository" + KIND_REPOSITORY = "ocm repository" + KIND_REPOSITORYSPEC = "repository specification" + KIND_COMPONENT = errkind.KIND_COMPONENT - KIND_COMPONENTVERSION = "component version" - KIND_RESOURCE = "component resource" - KIND_SOURCE = "component source" + KIND_COMPONENTVERSION = compdesc.KIND_COMPONENTVERSION + KIND_RESOURCE = compdesc.KIND_RESOURCE + KIND_SOURCE = compdesc.KIND_SOURCE KIND_REFERENCE = compdesc.KIND_REFERENCE - KIND_REPOSITORYSPEC = "repository specification" KIND_OCM_REFERENCE = "ocm reference" ) diff --git a/api/ocm/internal/repository.go b/api/ocm/internal/repository.go index b45d3d7cf..f79646c1b 100644 --- a/api/ocm/internal/repository.go +++ b/api/ocm/internal/repository.go @@ -109,7 +109,7 @@ type ( SourceAccess = ArtifactAccess[SourceMeta] ) -type ComponentReference = compdesc.ComponentReference +type ComponentReference = compdesc.Reference type ComponentVersionAccess interface { resource.ResourceView[ComponentVersionAccess] diff --git a/api/ocm/testhelper/resources.go b/api/ocm/testhelper/resources.go index 0d608ad17..ebf19e7dc 100644 --- a/api/ocm/testhelper/resources.go +++ b/api/ocm/testhelper/resources.go @@ -23,9 +23,10 @@ var Digests = testutils.Substitutions{ "D_OTHERDATA": D_OTHERDATA, } -const S_TESTDATA = "testdata" - -const D_TESTDATA = "810ff2fb242a5dee4220f2cb0e6a519891fb67f2f828a6cab4ef8894633b1f50" +const ( + S_TESTDATA = "testdata" + D_TESTDATA = "810ff2fb242a5dee4220f2cb0e6a519891fb67f2f828a6cab4ef8894633b1f50" +) var DS_TESTDATA = TextResourceDigestSpec(D_TESTDATA) @@ -36,9 +37,10 @@ func TestDataResource(env *builder.Builder, funcs ...func()) { }) } -const S_OTHERDATA = "otherdata" - -const D_OTHERDATA = "54b8007913ec5a907ca69001d59518acfd106f7b02f892eabf9cae3f8b2414b4" +const ( + S_OTHERDATA = "otherdata" + D_OTHERDATA = "54b8007913ec5a907ca69001d59518acfd106f7b02f892eabf9cae3f8b2414b4" +) var DS_OTHERDATA = TextResourceDigestSpec(D_OTHERDATA) diff --git a/api/ocm/tools/signing/digest.go b/api/ocm/tools/signing/digest.go new file mode 100644 index 000000000..8636166a8 --- /dev/null +++ b/api/ocm/tools/signing/digest.go @@ -0,0 +1,103 @@ +package signing + +import ( + "fmt" + "io" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/general" + + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/extensions/accessmethods/none" + "ocm.software/ocm/api/ocm/extensions/attrs/signingattr" + common "ocm.software/ocm/api/utils/misc" +) + +// VerifyResourceDigest verify the digest of a resource taken from a component version. +// The data of the resources (typically after fetching the content) is given by a ocm.DataAccess. +// The digest info is table from the resource described by a component version, which has +// been used to retrieve the data. +// The function returns true if the verification has been executed. If an error occurs, or +// the verification has been failed, an appropriate error occurs. +// If the resource is not signature relevant (false,nil) is returned. +func VerifyResourceDigest(cv ocm.ComponentVersionAccess, i int, bacc ocm.DataAccess, ostore ...VerifiedStore) (bool, error) { + octx := cv.GetContext() + cd := cv.GetDescriptor() + raw := &cd.Resources[i] + + store := general.Optional(ostore...) + if store != nil { + vcd := store.Get(cv) + if vcd == nil { + return false, fmt.Errorf("component version %s not verified", common.VersionedElementKey(cv)) + } + if !vcd.Resources[i].Digest.Equal(raw.Digest) { + return false, fmt.Errorf("component version %s corrupted", common.VersionedElementKey(cv)) + } + } + acc, err := octx.AccessSpecForSpec(raw.Access) + if err != nil { + return false, errors.Wrapf(err, resMsg(raw, "", "failed getting access for resource")) + } + + if none.IsNone(acc.GetKind()) { + return false, nil + } + if raw.Digest == nil { + return false, errors.ErrNotFound("digest") + } + // special digest notation indicates to not digest the content + if raw.Digest.IsExcluded() { + return false, nil + } + + meth, err := acc.AccessMethod(cv) + if err != nil { + return false, errors.Wrapf(err, resMsg(raw, acc.Describe(octx), "failed creating access for resource")) + } + defer meth.Close() + + meth = NewRedirectedAccessMethod(meth, bacc) + rdigest := raw.Digest + + dtype := DigesterType(rdigest) + req := []ocm.DigesterType{dtype} + + registry := signingattr.Get(octx).HandlerRegistry() + hasher := registry.GetHasher(dtype.HashAlgorithm) + digest, err := octx.BlobDigesters().DetermineDigests(raw.Type, hasher, registry, meth, req...) + if err != nil { + return false, errors.Wrapf(err, resMsg(raw, acc.Describe(octx), "failed determining digest for resource")) + } + if len(digest) == 0 { + return false, errors.Newf(resMsg(raw, acc.Describe(octx), "no digester accepts resource")) + } + if !checkDigest(rdigest, &digest[0]) { + return true, errors.Newf(resMsg(raw, acc.Describe(octx), "calculated resource digest (%+v) mismatches existing digest (%+v) for", &digest[0], rdigest)) + } + return true, nil +} + +type redirectedAccessMethod struct { + ocm.AccessMethod + acc ocm.DataAccess +} + +func NewRedirectedAccessMethod(m ocm.AccessMethod, bacc ocm.DataAccess) ocm.AccessMethod { + return &redirectedAccessMethod{m, bacc} +} + +func (m *redirectedAccessMethod) Close() error { + list := errors.ErrList() + list.Add(m.acc.Close()) + list.Add(m.AccessMethod.Close()) + return list.Result() +} + +func (m *redirectedAccessMethod) Reader() (io.ReadCloser, error) { + return m.acc.Reader() +} + +func (m *redirectedAccessMethod) Get() ([]byte, error) { + return m.acc.Get() +} diff --git a/api/ocm/tools/signing/digest_test.go b/api/ocm/tools/signing/digest_test.go new file mode 100644 index 000000000..13cd48522 --- /dev/null +++ b/api/ocm/tools/signing/digest_test.go @@ -0,0 +1,80 @@ +package signing_test + +import ( + "strings" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "ocm.software/ocm/api/helper/builder" + + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/oci/extensions/repositories/artifactset" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" + "ocm.software/ocm/api/ocm/extensions/digester/digesters/artifact" + "ocm.software/ocm/api/ocm/extensions/repositories/ctf" + "ocm.software/ocm/api/ocm/tools/signing" + "ocm.software/ocm/api/tech/signing/hasher/sha256" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" +) + +var _ = Describe("Digest Test Environment", func() { + var env *Builder + + BeforeEach(func() { + env = NewBuilder() + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.ComponentVersion(COMPONENTA, VERSION, func() { + env.Resource("image", VERSION, resourcetypes.OCI_ARTIFACT, metav1.LocalRelation, func() { + env.ArtifactSetBlob(VERSION, func() { + env.Manifest(VERSION, func() { + env.Config(func() { + env.BlobStringData(ociv1.MediaTypeImageConfig, "{}") + }) + env.Layer(func() { + env.BlobStringData(ociv1.MediaTypeImageLayerGzip, "fake") + }) + }) + }) + }) + }) + }) + }) + + AfterEach(func() { + env.Cleanup() + }) + + It("created oci artifact", func() { + repo := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, ARCH, 0, env)) + defer Close(repo, "repo") + + cv := Must(repo.LookupComponentVersion(COMPONENTA, VERSION)) + defer Close(cv, "cv") + + Expect(len(cv.GetDescriptor().Resources)).To(Equal(1)) + + rsc := Must(cv.GetResourceByIndex(0)) + + m := Must(rsc.AccessMethod()) + defer Close(m, "meth") + Expect(m.MimeType()).To(Equal(artifactset.MediaType(artdesc.MediaTypeImageManifest))) + + dig := rsc.Meta().Digest + Expect(dig).NotTo(BeNil()) + Expect(dig.HashAlgorithm).To(Equal(sha256.Algorithm)) + Expect(dig.NormalisationAlgorithm).To(Equal(artifact.OciArtifactDigestV1)) + + Expect(Must(signing.VerifyResourceDigest(cv, 0, m))).To(BeTrue()) + + orig := dig.Value + dig.Value = strings.Replace(dig.Value, "a", "b", -1) + done, err := signing.VerifyResourceDigest(cv, 0, m) + dig.Value = orig + Expect(done).To(BeTrue()) + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/api/ocm/tools/signing/handle.go b/api/ocm/tools/signing/handle.go index 172f18a22..a683e9f8b 100644 --- a/api/ocm/tools/signing/handle.go +++ b/api/ocm/tools/signing/handle.go @@ -277,6 +277,8 @@ func _apply(state WalkingState, nv common.NameVersion, cv ocm.ComponentVersionAc if dig != nil { spec = dig } + + addVerified(state, cd, opts, signatureNames...) } err := ctx.Propagate(spec) if err != nil { @@ -350,6 +352,7 @@ func _apply(state WalkingState, nv common.NameVersion, cv ocm.ComponentVersionAc } else { cd.Signatures = append(cd.Signatures, signature) } + addVerified(state, cd, opts, signatureNames...) } state.Closure[nv] = vi @@ -392,7 +395,7 @@ func checkDigest(orig *metav1.DigestSpec, act *metav1.DigestSpec) bool { return true } -func refMsg(ref compdesc.ComponentReference, msg string, args ...interface{}) string { +func refMsg(ref compdesc.Reference, msg string, args ...interface{}) string { return fmt.Sprintf("%s %s", fmt.Sprintf(msg, args...), ref) } @@ -717,3 +720,21 @@ func GetDigestMode(cd *compdesc.ComponentDescriptor, def ...string) string { } return general.Optional(def...) } + +func addVerified(state WalkingState, cd *compdesc.ComponentDescriptor, opts *Options, signatures ...string) { + if opts.VerifiedStore != nil { + _addVerified(state, common.VersionedElementKey(cd), cd, opts, signatures...) + } +} + +func _addVerified(state WalkingState, ctx common.NameVersion, cd *compdesc.ComponentDescriptor, opts *Options, signatures ...string) { + opts.VerifiedStore.Add(cd, signatures...) + for _, ref := range cd.References { + nv := common.NewNameVersion(ref.ComponentName, ref.Version) + s := state.Get(nv) + rs := s.GetContext(ctx) + if rs != nil { + _addVerified(state, ctx, rs.Descriptor, opts) + } + } +} diff --git a/api/ocm/tools/signing/handler_test.go b/api/ocm/tools/signing/handler_test.go index bfe41265c..6707f84a8 100644 --- a/api/ocm/tools/signing/handler_test.go +++ b/api/ocm/tools/signing/handler_test.go @@ -28,19 +28,28 @@ var _ = Describe("Simple signing handlers", func() { priv, pub = Must2(rsa.CreateKeyPair()) }) - Context("", func() { + Context("standard", func() { BeforeEach(func() { cv = composition.NewComponentVersion(ctx, COMPONENTA, VERSION) MustBeSuccessful(cv.SetResourceBlob(ocm.NewResourceMeta("blob", resourcetypes.PLAIN_TEXT, v1.LocalRelation), blobaccess.ForString(mime.MIME_TEXT, "test data"), "", nil)) }) DescribeTable("rsa handlers", func(kind string) { - Must(signing.SignComponentVersion(cv, "signature", signing.PrivateKey("signature", priv))) + Must(signing.SignComponentVersion(cv, "signature", signing.PrivateKey("signature", priv), signing.SignerByAlgo(kind))) Must(signing.VerifyComponentVersion(cv, "signature", signing.PublicKey("signature", pub))) }, Entry("rsa", rsa.Algorithm), Entry("rsapss", rsa_pss.Algorithm), ) + + It("uses verified store", func() { + store := signing.NewLocalVerifiedStore() + Must(signing.SignComponentVersion(cv, "signature", signing.PrivateKey("signature", priv), signing.UseVerifiedStore(store))) + + cd := store.Get(cv) + Expect(cd).NotTo(BeNil()) + Expect(len(cd.Signatures)).To(Equal(1)) + }) }) Context("non-unique resources", func() { diff --git a/api/ocm/tools/signing/options.go b/api/ocm/tools/signing/options.go index b7fe9a72e..e5a90e1e7 100644 --- a/api/ocm/tools/signing/options.go +++ b/api/ocm/tools/signing/options.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/general" "github.com/mandelsoft/goutils/generics" "ocm.software/ocm/api/datacontext/attrs/rootcertsattr" @@ -431,6 +432,22 @@ func (o *tsaOpt) ApplySigningOption(opts *Options) { //////////////////////////////////////////////////////////////////////////////// +type verifyedstore struct { + store VerifiedStore +} + +// UseVerifiedStore configures a store for providing verify component decariptors. +// If no store is given, a local store is created. +func UseVerifiedStore(s ...VerifiedStore) Option { + return &verifyedstore{general.OptionalDefaulted(NewLocalVerifiedStore(), s...)} +} + +func (o *verifyedstore) ApplySigningOption(opts *Options) { + opts.VerifiedStore = o.store +} + +//////////////////////////////////////////////////////////////////////////////// + type Options struct { Printer common.Printer Update bool @@ -455,6 +472,8 @@ type Options struct { UseTSA bool effectiveRegistry signing.Registry + + VerifiedStore VerifiedStore } var _ Option = (*Options)(nil) diff --git a/api/ocm/tools/signing/signing_test.go b/api/ocm/tools/signing/signing_test.go index 32c988daa..b8dc351ae 100644 --- a/api/ocm/tools/signing/signing_test.go +++ b/api/ocm/tools/signing/signing_test.go @@ -1232,8 +1232,68 @@ applying to version "github.com/mandelsoft/test:v1"[github.com/mandelsoft/test:v RootCertificates(ca), PKIXIssuer(*issuer)) }) }) + + Context("verified store", func() { + BeforeEach(func() { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.ComponentVersion(COMPONENTA, VERSION, func() { + env.Provider(PROVIDER) + }) + env.ComponentVersion(COMPONENTB, VERSION, func() { + env.Provider(PROVIDER) + env.Reference("refa", COMPONENTA, VERSION) + }) + env.ComponentVersion(COMPONENTC, VERSION, func() { + env.Provider(PROVIDER) + env.Reference("refb", COMPONENTB, VERSION) + }) + }) + }) + + It("rembers all indirectly signed component descriptors", func() { + src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_WRITABLE, ARCH, 0, env)) + defer Close(src, "ctf") + + resolver := ocm.NewCompoundResolver(src) + + cv := Must(resolver.LookupComponentVersion(COMPONENTC, VERSION)) + defer Close(cv, "cv") + + store := NewLocalVerifiedStore() + opts := NewOptions( + Sign(signing.DefaultHandlerRegistry().GetSigner(SIGN_ALGO), SIGNATURE), + Resolver(resolver), + VerifyDigests(), + UseVerifiedStore(store), + ) + MustBeSuccessful(opts.Complete(env)) + + pr, buf := common.NewBufferedPrinter() + Must(Apply(pr, nil, cv, opts)) + + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +applying to version "github.com/mandelsoft/ref2:v1"[github.com/mandelsoft/ref2:v1]... + no digest found for "github.com/mandelsoft/ref:v1" + applying to version "github.com/mandelsoft/ref:v1"[github.com/mandelsoft/ref2:v1]... + no digest found for "github.com/mandelsoft/test:v1" + applying to version "github.com/mandelsoft/test:v1"[github.com/mandelsoft/ref2:v1]... + reference 0: github.com/mandelsoft/test:v1: digest SHA-256:5ed8bb27309c3c2fff43f3b0f3ebb56a5737ad6db4bc8ace73c5455cb86faf54[jsonNormalisation/v1] + reference 0: github.com/mandelsoft/ref:v1: digest SHA-256:e85e324ff16bafe26db235567d9232319c36f48ce995aa3f4957e55002207277[jsonNormalisation/v1] +`)) + + CheckStore(store, cv) + CheckStore(store, common.NewNameVersion(COMPONENTB, VERSION)) + CheckStore(store, common.NewNameVersion(COMPONENTA, VERSION)) + }) + }) }) +func CheckStore(store VerifiedStore, ve common.VersionedElement) { + e := store.Get(ve) + ExpectWithOffset(1, e).NotTo(BeNil()) + ExpectWithOffset(1, common.VersionedElementKey(e)).To(Equal(common.VersionedElementKey(ve))) +} + func HashComponent(resolver ocm.ComponentVersionResolver, name string, digest string, other ...Option) string { cv, err := resolver.LookupComponentVersion(name, VERSION) Expect(err).To(Succeed()) diff --git a/api/ocm/tools/signing/store.go b/api/ocm/tools/signing/store.go new file mode 100644 index 000000000..adeef6ac2 --- /dev/null +++ b/api/ocm/tools/signing/store.go @@ -0,0 +1,224 @@ +package signing + +import ( + "io" + "os" + "sync" + + "github.com/mandelsoft/filepath/pkg/filepath" + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/sliceutils" + "github.com/mandelsoft/vfs/pkg/vfs" + "golang.org/x/exp/maps" + + "ocm.software/ocm/api/ocm/compdesc" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/utils" + common "ocm.software/ocm/api/utils/misc" + "ocm.software/ocm/api/utils/runtime" +) + +// VerifiedStore is an interface for some kind of +// memory providing information about verified +// component versions and the digests of the verified +// artifacts. It is used to verify downloaded resource content, +// without requiring to verify the complete component version, again. +// If the component version has already been marked as being verified +// only the digest of the downloaded content has be compared with the +// digest already marked as verified in the context of its component version. +// +// A typical implementation is a file based store, which stored the serialized +// component versions (see NewVerifiedStore). +type VerifiedStore interface { + Add(cd *compdesc.ComponentDescriptor, signatures ...string) + Remove(n common.VersionedElement) + Get(n common.VersionedElement) *compdesc.ComponentDescriptor + GetEntry(n common.VersionedElement) *StorageEntry + + GetResourceDigest(n common.VersionedElement, id metav1.Identity) *metav1.DigestSpec + GetResourceDigestByIndex(n common.VersionedElement, idx int) *metav1.DigestSpec + + Entries() []common.NameVersion + + Load() error + Save() error +} + +type verifiedStore struct { + lock sync.Mutex + storage *StorageDescriptor + fs vfs.FileSystem + file string +} + +var _ VerifiedStore = (*verifiedStore)(nil) + +// NewLocalVerifiedStore creates a memory based VerifiedStore. +func NewLocalVerifiedStore() VerifiedStore { + return &verifiedStore{storage: &StorageDescriptor{}} +} + +// NewVerifiedStore loads or creates a new filesystem based VerifiedStore. +func NewVerifiedStore(path string, fss ...vfs.FileSystem) (VerifiedStore, error) { + eff, err := utils.ResolvePath(path) + if err != nil { + return nil, err + } + + fs := utils.FileSystem(fss...) + + s := &verifiedStore{ + fs: fs, + file: eff, + } + + err = s.Load() + if err != nil { + return nil, err + } + return s, nil +} + +func (v *verifiedStore) Load() error { + v.lock.Lock() + defer v.lock.Unlock() + if v.fs == nil { + return nil + } + + dir := filepath.Dir(v.file) + + if ok, err := vfs.DirExists(v.fs, dir); !ok || err != nil { + if err != nil { + return err + } + return errors.ErrNotFound("directory", dir) + } + + var storage StorageDescriptor + f, err := v.fs.Open(v.file) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return err + } + } else { + defer f.Close() + data, err := io.ReadAll(f) + if err != nil { + return err + } + err = runtime.DefaultYAMLEncoding.Unmarshal(data, &storage) + if err != nil { + return err + } + } + v.storage = &storage + return nil +} + +func (v *verifiedStore) Save() error { + v.lock.Lock() + defer v.lock.Unlock() + if v.fs == nil { + return nil + } + + data, err := runtime.DefaultYAMLEncoding.Marshal(v.storage) + if err != nil { + return err + } + + err = vfs.WriteFile(v.fs, v.file, data, 0o600) + if err != nil { + return err + } + return nil +} + +func (v *verifiedStore) Entries() []common.NameVersion { + v.lock.Lock() + defer v.lock.Unlock() + + return sliceutils.Transform(maps.Values(v.storage.ComponentVersions), + func(cd *StorageEntry) common.NameVersion { return common.VersionedElementKey(cd.Descriptor) }) +} + +func (v *verifiedStore) Add(cd *compdesc.ComponentDescriptor, signatures ...string) { + v.lock.Lock() + defer v.lock.Unlock() + + if v.storage == nil { + v.storage = &StorageDescriptor{} + } + if v.storage.ComponentVersions == nil { + v.storage.ComponentVersions = map[string]*StorageEntry{} + } + key := common.VersionedElementKey(cd).String() + old := v.storage.ComponentVersions[key] + if old == nil || !old.Descriptor.Descriptor().Equal(cd) { + old = &StorageEntry{ + Descriptor: (*compdesc.GenericComponentDescriptor)(cd), + } + } + for _, e := range signatures { + old.Signatures = sliceutils.AppendUnique(old.Signatures, e) + } + v.storage.ComponentVersions[key] = old +} + +func (v *verifiedStore) Remove(n common.VersionedElement) { + v.lock.Lock() + defer v.lock.Unlock() + + delete(v.storage.ComponentVersions, common.VersionedElementKey(n).String()) +} + +func (v *verifiedStore) GetEntry(n common.VersionedElement) *StorageEntry { + v.lock.Lock() + defer v.lock.Unlock() + + return v.storage.ComponentVersions[common.VersionedElementKey(n).String()] +} + +func (v *verifiedStore) Get(n common.VersionedElement) *compdesc.ComponentDescriptor { + v.lock.Lock() + defer v.lock.Unlock() + + entry := v.storage.ComponentVersions[common.VersionedElementKey(n).String()] + if entry == nil { + return nil + } + return entry.Descriptor.Descriptor() +} + +func (v *verifiedStore) GetResourceDigest(n common.VersionedElement, id metav1.Identity) *metav1.DigestSpec { + cd := v.Get(n) + if cd == nil { + return nil + } + r, err := cd.GetResourceByIdentity(id) + if err != nil { + return nil + } + return r.Digest +} + +func (v *verifiedStore) GetResourceDigestByIndex(n common.VersionedElement, idx int) *metav1.DigestSpec { + cd := v.Get(n) + if cd == nil { + return nil + } + if idx < 0 || idx >= len(cd.Resources) { + return nil + } + return cd.Resources[idx].Digest +} + +type StorageEntry struct { + Signatures []string `json:"signatures,omitempty"` + Descriptor *compdesc.GenericComponentDescriptor `json:"descriptor"` +} + +type StorageDescriptor struct { + ComponentVersions map[string]*StorageEntry `json:"componentVersions,omitempty"` +} diff --git a/api/ocm/tools/signing/store_test.go b/api/ocm/tools/signing/store_test.go new file mode 100644 index 000000000..df2ff1d6b --- /dev/null +++ b/api/ocm/tools/signing/store_test.go @@ -0,0 +1,91 @@ +package signing_test + +import ( + "os" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/ocm/compdesc" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/ocm/compdesc/versions/ocm.software/v3alpha1" + v2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" + "ocm.software/ocm/api/ocm/tools/signing" + common "ocm.software/ocm/api/utils/misc" + "ocm.software/ocm/api/utils/runtime" +) + +var _ = Describe("Store", func() { + Context("persistence", func() { + It("", func() { + file := Must(os.CreateTemp("", "store-*")) + + os.Remove(file.Name()) + store := Must(signing.NewVerifiedStore(file.Name())) + Expect(store).NotTo(BeNil()) + + cd1 := compdesc.DefaultComponent(&compdesc.ComponentDescriptor{ + Metadata: compdesc.Metadata{ + ConfiguredVersion: v3alpha1.SchemaVersion, + }, + ComponentSpec: compdesc.ComponentSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: COMPONENTA, + Version: VERSION, + Provider: metav1.Provider{ + Name: "acme.org", + }, + }, + }, + }) + cd2 := compdesc.DefaultComponent(&compdesc.ComponentDescriptor{ + Metadata: compdesc.Metadata{ + ConfiguredVersion: v2.SchemaVersion, + }, + ComponentSpec: compdesc.ComponentSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: COMPONENTB, + Version: VERSION, + Provider: metav1.Provider{ + Name: "acme.org", + }, + }, + }, + }) + + store.Add(cd1, "a") + store.Add(cd2, "b") + store.Add(cd2, "c") + + MustBeSuccessful(store.Save()) + + desc := signing.StorageDescriptor{ + ComponentVersions: map[string]*signing.StorageEntry{ + common.VersionedElementKey(cd1).String(): &signing.StorageEntry{ + Signatures: []string{"a"}, + Descriptor: (*compdesc.GenericComponentDescriptor)(cd1), + }, + common.VersionedElementKey(cd2).String(): &signing.StorageEntry{ + Signatures: []string{"b", "c"}, + Descriptor: (*compdesc.GenericComponentDescriptor)(cd2), + }, + }, + } + + data := Must(os.ReadFile(file.Name())) + + exp := Must(runtime.DefaultYAMLEncoding.Marshal(desc)) + Expect(data).To(YAMLEqual(exp)) + + store = Must(signing.NewVerifiedStore(file.Name())) + + Expect(store.Get(cd1)).To(YAMLEqual(cd1)) + Expect(store.Get(cd1).Equal(cd1)).To(BeTrue()) + + Expect(store.Get(cd2)).To(YAMLEqual(cd2)) + Expect(store.Get(cd2).Equal(cd2)).To(BeTrue()) + + }) + }) +}) diff --git a/api/ocm/tools/transfer/merge_test.go b/api/ocm/tools/transfer/merge_test.go index 0cdd8d8c8..2a7892890 100644 --- a/api/ocm/tools/transfer/merge_test.go +++ b/api/ocm/tools/transfer/merge_test.go @@ -204,7 +204,7 @@ var _ = Describe("basic merge operations for transport", func() { }, }, References: compdesc.References{ - compdesc.ComponentReference{ + compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: "ref1", Version: "v1.0.0", diff --git a/api/ocm/tools/transfer/transferhandler/spiff/handler.go b/api/ocm/tools/transfer/transferhandler/spiff/handler.go index 0a8aafff4..4044ffdf9 100644 --- a/api/ocm/tools/transfer/transferhandler/spiff/handler.go +++ b/api/ocm/tools/transfer/transferhandler/spiff/handler.go @@ -76,7 +76,7 @@ func (h *Handler) OverwriteVersion(src ocm.ComponentVersionAccess, tgt ocm.Compo return h.EvalBool("overwrite", binding, "process") } -func (h *Handler) TransferVersion(repo ocm.Repository, src ocm.ComponentVersionAccess, meta *compdesc.ComponentReference, tgt ocm.Repository) (ocm.ComponentVersionAccess, transferhandler.TransferHandler, error) { +func (h *Handler) TransferVersion(repo ocm.Repository, src ocm.ComponentVersionAccess, meta *compdesc.Reference, tgt ocm.Repository) (ocm.ComponentVersionAccess, transferhandler.TransferHandler, error) { if src == nil || h.opts.IsRecursive() { if h.opts.GetScript() == nil { return h.Handler.TransferVersion(repo, src, meta, tgt) diff --git a/api/ocm/tools/transfer/transferhandler/standard/handler.go b/api/ocm/tools/transfer/transferhandler/standard/handler.go index 06020464e..5792f2cd2 100644 --- a/api/ocm/tools/transfer/transferhandler/standard/handler.go +++ b/api/ocm/tools/transfer/transferhandler/standard/handler.go @@ -47,7 +47,7 @@ func (h *Handler) OverwriteVersion(src ocm.ComponentVersionAccess, tgt ocm.Compo return h.opts.IsOverwrite(), nil } -func (h *Handler) TransferVersion(repo ocm.Repository, src ocm.ComponentVersionAccess, meta *compdesc.ComponentReference, tgt ocm.Repository) (ocm.ComponentVersionAccess, transferhandler.TransferHandler, error) { +func (h *Handler) TransferVersion(repo ocm.Repository, src ocm.ComponentVersionAccess, meta *compdesc.Reference, tgt ocm.Repository) (ocm.ComponentVersionAccess, transferhandler.TransferHandler, error) { if src == nil || h.opts.IsRecursive() { if h.opts.IsStopOnExistingVersion() && tgt != nil { if found, err := tgt.ExistsComponentVersion(meta.ComponentName, meta.Version); found || err != nil { diff --git a/api/ocm/tools/transfer/transferhandler/transferhandler.go b/api/ocm/tools/transfer/transferhandler/transferhandler.go index 1a5f72314..260d3f921 100644 --- a/api/ocm/tools/transfer/transferhandler/transferhandler.go +++ b/api/ocm/tools/transfer/transferhandler/transferhandler.go @@ -93,7 +93,7 @@ type TransferHandler interface { OverwriteVersion(src ocm.ComponentVersionAccess, tgt ocm.ComponentVersionAccess) (bool, error) // TransferVersion decides on continuing with a component version (reference). - TransferVersion(repo ocm.Repository, src ocm.ComponentVersionAccess, meta *compdesc.ComponentReference, tgt ocm.Repository) (ocm.ComponentVersionAccess, TransferHandler, error) + TransferVersion(repo ocm.Repository, src ocm.ComponentVersionAccess, meta *compdesc.Reference, tgt ocm.Repository) (ocm.ComponentVersionAccess, TransferHandler, error) // TransferResource decides on the value transport of a resource. TransferResource(src ocm.ComponentVersionAccess, a ocm.AccessSpec, r ocm.ResourceAccess) (bool, error) // TransferSource decides on the value transport of a source. diff --git a/api/ocm/utils.go b/api/ocm/utils.go index 964f22879..67a4256da 100644 --- a/api/ocm/utils.go +++ b/api/ocm/utils.go @@ -95,7 +95,7 @@ func IsIntermediate(spec RepositorySpec) bool { return false } -func ComponentRefKey(ref *compdesc.ComponentReference) common.NameVersion { +func ComponentRefKey(ref *compdesc.Reference) common.NameVersion { return common.NewNameVersion(ref.GetComponentName(), ref.GetVersion()) } diff --git a/cmds/ocm/commands/ocmcmds/cli/download/cmd.go b/cmds/ocm/commands/ocmcmds/cli/download/cmd.go index 7f4b757c6..a16033773 100644 --- a/cmds/ocm/commands/ocmcmds/cli/download/cmd.go +++ b/cmds/ocm/commands/ocmcmds/cli/download/cmd.go @@ -20,6 +20,7 @@ import ( ocmcommon "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/handlers/elemhdlr" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/repooption" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/storeoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/versionconstraintsoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/names" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/resources/common" @@ -52,7 +53,7 @@ func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { return utils.SetupCommand(&Command{BaseCommand: utils.NewBaseCommand(ctx, versionconstraintsoption.New(true).SetLatest(), repooption.New(), - output.OutputOptions(output.NewOutputs(f), downloadcmd.NewOptions(true).SetUseHandlers(), destoption.New()), + output.OutputOptions(output.NewOutputs(f), downloadcmd.NewOptions(true).SetUseHandlers(), destoption.New(), storeoption.New("use-verified")), )}, utils.Names(Names, names...)...) } diff --git a/cmds/ocm/commands/ocmcmds/cmd.go b/cmds/ocm/commands/ocmcmds/cmd.go index 845e2e524..a35b28540 100644 --- a/cmds/ocm/commands/ocmcmds/cmd.go +++ b/cmds/ocm/commands/ocmcmds/cmd.go @@ -15,6 +15,7 @@ import ( "ocm.software/ocm/cmds/ocm/commands/ocmcmds/routingslips" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/sourceconfig" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/sources" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/verified" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/versions" "ocm.software/ocm/cmds/ocm/common/utils" topicocmaccessmethods "ocm.software/ocm/cmds/ocm/topics/ocm/accessmethods" @@ -40,6 +41,7 @@ func NewCommand(ctx clictx.Context) *cobra.Command { cmd.AddCommand(plugins.NewCommand(ctx)) cmd.AddCommand(routingslips.NewCommand(ctx)) cmd.AddCommand(pubsub.NewCommand(ctx)) + cmd.AddCommand(verified.NewCommand(ctx)) cmd.AddCommand(utils.DocuCommandPath(topicocmrefs.New(ctx), "ocm")) cmd.AddCommand(utils.DocuCommandPath(topicocmaccessmethods.New(ctx), "ocm")) diff --git a/cmds/ocm/commands/ocmcmds/common/addhdlrs/refs/elements.go b/cmds/ocm/commands/ocmcmds/common/addhdlrs/refs/elements.go index edea237dd..3c529accd 100644 --- a/cmds/ocm/commands/ocmcmds/common/addhdlrs/refs/elements.go +++ b/cmds/ocm/commands/ocmcmds/common/addhdlrs/refs/elements.go @@ -57,7 +57,7 @@ func (h *ResourceSpecHandler) Set(v ocm.ComponentVersionAccess, r addhdlrs.Eleme if vers == "" { vers = v.GetVersion() } - meta := &compdesc.ComponentReference{ + meta := &compdesc.Reference{ ElementMeta: compdesc.ElementMeta{ Name: spec.Name, Version: vers, diff --git a/cmds/ocm/commands/ocmcmds/common/cmds/signing/cmd.go b/cmds/ocm/commands/ocmcmds/common/cmds/signing/cmd.go index 3cbfafc31..131385546 100644 --- a/cmds/ocm/commands/ocmcmds/common/cmds/signing/cmd.go +++ b/cmds/ocm/commands/ocmcmds/common/cmds/signing/cmd.go @@ -97,7 +97,14 @@ func (o *SignatureCommand) Run() (rerr error) { if err != nil { return err } - return utils.HandleOutput(NewAction(o.spec.terms, o.Context.OCMContext(), common.NewPrinter(o.Context.StdOut()), sopts), handler, utils.StringElemSpecs(o.Refs...)...) + err = utils.HandleOutput(NewAction(o.spec.terms, o.Context.OCMContext(), common.NewPrinter(o.Context.StdOut()), sopts), handler, utils.StringElemSpecs(o.Refs...)...) + if err != nil { + return err + } + if sopts.VerifiedStore != nil { + return errors.Wrapf(sopts.VerifiedStore.Save(), "cannot save verified store %q", signoption.From(o).Verified.File) + } + return nil } ///////////////////////////////////////////////////////////////////////////// diff --git a/cmds/ocm/commands/ocmcmds/common/handlers/elemhdlr/typehandler.go b/cmds/ocm/commands/ocmcmds/common/handlers/elemhdlr/typehandler.go index dd335e9ad..a6631821c 100644 --- a/cmds/ocm/commands/ocmcmds/common/handlers/elemhdlr/typehandler.go +++ b/cmds/ocm/commands/ocmcmds/common/handlers/elemhdlr/typehandler.go @@ -21,6 +21,7 @@ type Object struct { VersionId metav1.Identity Spec metav1.Identity + Index int Id metav1.Identity Node *common.NameVersion Element compdesc.ElementMetaAccessor @@ -150,6 +151,7 @@ func (h *TypeHandler) all(c *comphdlr.Object) ([]output.Object, error) { History: c.History.Append(common.VersionedElementKey(c.ComponentVersion)), Version: c.ComponentVersion, VersionId: c.Identity, + Index: i, Id: e.GetMeta().GetIdentity(elemaccess), Element: e, }) @@ -161,6 +163,7 @@ func (h *TypeHandler) all(c *comphdlr.Object) ([]output.Object, error) { History: c.History.Append(common.VersionedElementKey(c.ComponentVersion)), Version: c.ComponentVersion, VersionId: c.Identity, + Index: -1, Id: metav1.Identity{}, Element: nil, }) @@ -203,6 +206,7 @@ func (h *TypeHandler) get(c *comphdlr.Object, elemspec utils.ElemSpec) ([]output History: c.History.Append(common.VersionedElementKey(c.ComponentVersion)), Version: c.ComponentVersion, VersionId: c.Identity, + Index: i, Spec: selector, Id: m.GetIdentity(elemaccess), Element: e, @@ -214,6 +218,7 @@ func (h *TypeHandler) get(c *comphdlr.Object, elemspec utils.ElemSpec) ([]output History: c.History.Append(common.VersionedElementKey(c.ComponentVersion)), Version: c.ComponentVersion, VersionId: c.Identity, + Index: -1, Id: metav1.Identity{}, Element: nil, }) diff --git a/cmds/ocm/commands/ocmcmds/common/handlers/verifiedhdlr/typehandler.go b/cmds/ocm/commands/ocmcmds/common/handlers/verifiedhdlr/typehandler.go new file mode 100644 index 000000000..71ee53ffd --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/handlers/verifiedhdlr/typehandler.go @@ -0,0 +1,91 @@ +package pluginhdlr + +import ( + "slices" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/general" + "github.com/mandelsoft/vfs/pkg/vfs" + + clictx "ocm.software/ocm/api/cli" + "ocm.software/ocm/api/datacontext/attrs/vfsattr" + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/compdesc" + "ocm.software/ocm/api/ocm/tools/signing" + "ocm.software/ocm/cmds/ocm/common/output" + "ocm.software/ocm/cmds/ocm/common/utils" +) + +func Elem(e interface{}) *Object { + return e.(*Object) +} + +//////////////////////////////////////////////////////////////////////////////// + +type Object struct { + Signatures []string `json:"signatures,omitempty"` + Element *compdesc.ComponentDescriptor `json:"descriptor"` +} + +func (o *Object) AsManifest() interface{} { + return o +} + +//////////////////////////////////////////////////////////////////////////////// + +type TypeHandler struct { + octx clictx.OCM + store signing.VerifiedStore +} + +func NewTypeHandler(octx clictx.OCM, path string, fss ...vfs.FileSystem) (utils.TypeHandler, error) { + fs := general.OptionalDefaulted(vfsattr.Get(octx.Context()), fss...) + store, err := signing.NewVerifiedStore(path, fs) + if err != nil { + return nil, err + } + return &TypeHandler{ + octx: octx, + store: store, + }, nil +} + +func (h *TypeHandler) Close() error { + return nil +} + +func (h *TypeHandler) All() ([]output.Object, error) { + result := []output.Object{} + + for _, nv := range h.store.Entries() { + e := h.store.GetEntry(nv) + result = append(result, &Object{slices.Clone(e.Signatures), e.Descriptor.Descriptor().Copy()}) + } + return result, nil +} + +func (h *TypeHandler) Get(elemspec utils.ElemSpec) ([]output.Object, error) { + comp, err := ocm.ParseComp(elemspec.String()) + if err != nil { + return nil, err + } + if comp.IsVersion() { + e := h.store.GetEntry(comp.NameVersion()) + if e == nil { + return nil, errors.ErrNotFound(ocm.KIND_COMPONENTVERSION, elemspec.String()) + } + return []output.Object{&Object{slices.Clone(e.Signatures), e.Descriptor.Descriptor().Copy()}}, nil + } + + var objs []output.Object + for _, nv := range h.store.Entries() { + if nv.GetName() == comp.Component { + e := h.store.GetEntry(nv) + objs = append(objs, &Object{slices.Clone(e.Signatures), e.Descriptor.Descriptor().Copy()}) + } + } + if len(objs) == 0 { + return nil, errors.ErrNotFound(ocm.KIND_COMPONENT, elemspec.String()) + } + return objs, nil +} diff --git a/cmds/ocm/commands/ocmcmds/common/options/signoption/option.go b/cmds/ocm/commands/ocmcmds/common/options/signoption/option.go index 6e27d1d0d..14723d097 100644 --- a/cmds/ocm/commands/ocmcmds/common/options/signoption/option.go +++ b/cmds/ocm/commands/ocmcmds/common/options/signoption/option.go @@ -19,6 +19,7 @@ import ( "ocm.software/ocm/api/utils/listformat" "ocm.software/ocm/cmds/ocm/commands/common/options/keyoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/hashoption" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/storeoption" "ocm.software/ocm/cmds/ocm/common/options" ) @@ -58,10 +59,15 @@ type Option struct { Hash hashoption.Option Keyless bool + + Verified storeoption.Option } +const DEFAULT_VERIFIED_FILE = "~/.ocm/verified" + func (o *Option) AddFlags(fs *pflag.FlagSet) { o.Option.AddFlags(fs) + o.Verified.AddFlags(fs) fs.StringArrayVarP(&o.SignatureNames, "signature", "s", nil, "signature name") if o.SignMode { o.Hash.AddFlags(fs) @@ -116,7 +122,7 @@ func (o *Option) Configure(ctx clictx.Context) error { return err } - return nil + return o.Verified.Configure(ctx) } func (o *Option) Usage() string { @@ -130,7 +136,7 @@ signature name specified with the option --signature. Alternatively a key can be specified as base64 encoded string if the argument start with the prefix ! or as direct string with the prefix =. -` +` + o.Verified.Usage() if o.SignMode { s += ` @@ -194,4 +200,6 @@ func (o *Option) ApplySigningOption(opts *ocmsign.Options) { } opts.Update = o.Update opts.Keyless = o.Keyless + + opts.VerifiedStore = o.Verified.Store } diff --git a/cmds/ocm/commands/ocmcmds/common/options/storeoption/option.go b/cmds/ocm/commands/ocmcmds/common/options/storeoption/option.go new file mode 100644 index 000000000..64fabc80c --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/options/storeoption/option.go @@ -0,0 +1,72 @@ +package storeoption + +import ( + "github.com/mandelsoft/goutils/general" + "github.com/spf13/pflag" + + clictx "ocm.software/ocm/api/cli" + "ocm.software/ocm/api/datacontext/attrs/vfsattr" + ocmsign "ocm.software/ocm/api/ocm/tools/signing" + "ocm.software/ocm/cmds/ocm/common/options" +) + +func From(o options.OptionSetProvider) *Option { + var opt *Option + o.AsOptionSet().Get(&opt) + return opt +} + +var _ options.Options = (*Option)(nil) + +func New(name ...string) *Option { + return &Option{name: general.OptionalDefaulted("remember-verified", name...)} +} + +type Option struct { + name string + + // File is used to remember verify component versions. + File string + RememberVerification bool + Store ocmsign.VerifiedStore +} + +const DEFAULT_VERIFIED_FILE = "~/.ocm/verified" + +func (o *Option) AddFlags(fs *pflag.FlagSet) { + fs.BoolVar(&o.RememberVerification, o.name, false, "enable verification store") + fs.StringVarP(&o.File, "verified", "", DEFAULT_VERIFIED_FILE, "file used to remember verifications for downloads") +} + +func (o *Option) Configure(ctx clictx.Context) error { + var err error + if o.Store == nil && (o.RememberVerification || o.File != DEFAULT_VERIFIED_FILE) { + o.Store, err = ocmsign.NewVerifiedStore(o.File, vfsattr.Get(ctx)) + if err != nil { + return err + } + } + if o.Store != nil { + o.RememberVerification = true + } + return nil +} + +func (o *Option) Usage() string { + s := ` +If the verification store is enabled, resources downloaded from +signed or verified component versions are verified against their digests +provided by the component version.(not supported for using downloaders for the +resource download). + +The usage of the verification store is enabled by --` + o.name + ` or by +specifying a verification file with --verified. +` + return s +} + +var _ ocmsign.Option = (*Option)(nil) + +func (o *Option) ApplySigningOption(opts *ocmsign.Options) { + opts.VerifiedStore = o.Store +} diff --git a/cmds/ocm/commands/ocmcmds/components/sign/cmd_test.go b/cmds/ocm/commands/ocmcmds/components/sign/cmd_test.go index df586218a..9ff7b3612 100644 --- a/cmds/ocm/commands/ocmcmds/components/sign/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/components/sign/cmd_test.go @@ -9,6 +9,7 @@ import ( . "github.com/onsi/gomega" . "ocm.software/ocm/api/oci/testhelper" . "ocm.software/ocm/api/ocm/testhelper" + common "ocm.software/ocm/api/utils/misc" . "ocm.software/ocm/cmds/ocm/testhelper" "github.com/mandelsoft/vfs/pkg/vfs" @@ -19,6 +20,7 @@ import ( "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" "ocm.software/ocm/api/ocm/extensions/repositories/ctf" + "ocm.software/ocm/api/ocm/tools/signing" "ocm.software/ocm/api/tech/signing/handlers/rsa" "ocm.software/ocm/api/tech/signing/signutils" "ocm.software/ocm/api/utils/accessio" @@ -54,6 +56,8 @@ const ( D_COMPONENTB = "5f416ec59629d6af91287e2ba13c6360339b6a0acf624af2abd2a810ce4aefce" ) +const VERIFIED_FILE = "verified.yaml" + var substitutions = Substitutions{ "test": D_COMPONENTA, "r0": D_TESTDATA, @@ -108,7 +112,7 @@ var _ = Describe("access method", func() { Expect(r.Meta().Digest).To(Equal(DS_OCIMANIFEST2)) }) - It("sign single component in component archive", func() { + It("signs single component in component archive", func() { prepareEnv(env, ARCH, "") buf := bytes.NewBuffer(nil) @@ -135,7 +139,7 @@ successfully signed github.com/mandelsoft/test:v1 (digest SHA-256:${test})`, Expect(cv.GetDescriptor().Signatures[0].Digest.Value).To(Equal(D_COMPONENTA)) }) - It("sign component archive", func() { + It("signs transport archive", func() { prepareEnv(env, ARCH, ARCH) buf := bytes.NewBuffer(nil) @@ -165,7 +169,7 @@ successfully signed github.com/mandelsoft/ref:v1 (digest SHA-256:${ref}) Expect(cv.GetDescriptor().Signatures[0].Digest.Value).To(Equal(D_COMPONENTB)) }) - It("sign component archive with --lookup option", func() { + It("signs transport archive with --lookup option", func() { prepareEnv(env, ARCH2, ARCH) buf := bytes.NewBuffer(nil) @@ -356,8 +360,51 @@ successfully verified github.com/mandelsoft/test:v1 (digest SHA-256:5ed8bb27309c Expect(len(certs)).To(Equal(3)) Expect(algo).To(Equal(rsa.Algorithm)) }) + + Context("verified store", func() { + BeforeEach(func() { + FakeOCIRepo(env.Builder, OCIPATH, OCIHOST) + + env.OCICommonTransport(OCIPATH, accessio.FormatDirectory, func() { + OCIManifest1(env.Builder) + OCIManifest2(env.Builder) + }) + }) + + It("signs transport archive", func() { + prepareEnv(env, ARCH, ARCH) + + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("sign", "components", "--verified", VERIFIED_FILE, "-s", SIGNATURE, "-K", PRIVKEY, "--repo", ARCH, COMPONENTB+":"+VERSION)).To(Succeed()) + + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +applying to version "github.com/mandelsoft/ref:v1"[github.com/mandelsoft/ref:v1]... + no digest found for "github.com/mandelsoft/test:v1" + applying to version "github.com/mandelsoft/test:v1"[github.com/mandelsoft/ref:v1]... + resource 0: "name"="testdata": digest SHA-256:${r0}[genericBlobDigest/v1] + resource 1: "name"="value": digest SHA-256:${r1}[ociArtifactDigest/v1] + resource 2: "name"="ref": digest SHA-256:${r2}[ociArtifactDigest/v1] + reference 0: github.com/mandelsoft/test:v1: digest SHA-256:${test}[jsonNormalisation/v1] + resource 0: "name"="otherdata": digest SHA-256:${rb0}[genericBlobDigest/v1] +successfully signed github.com/mandelsoft/ref:v1 (digest SHA-256:${ref}) +`, substitutions)) + + Expect(Must(env.FileExists(VERIFIED_FILE))).To(BeTrue()) + + store := Must(signing.NewVerifiedStore(VERIFIED_FILE, env.FileSystem())) + + CheckStore(store, common.NewNameVersion(COMPONENTA, VERSION)) + CheckStore(store, common.NewNameVersion(COMPONENTB, VERSION)) + }) + }) }) +func CheckStore(store signing.VerifiedStore, ve common.VersionedElement) { + e := store.Get(ve) + ExpectWithOffset(1, e).NotTo(BeNil()) + ExpectWithOffset(1, common.VersionedElementKey(e)).To(Equal(common.VersionedElementKey(ve))) +} + func prepareEnv(env *TestEnv, componentAArchive, componentBArchive string) { env.OCMCommonTransport(componentAArchive, accessio.FormatDirectory, func() { env.Component(COMPONENTA, func() { diff --git a/cmds/ocm/commands/ocmcmds/components/verify/cmd_test.go b/cmds/ocm/commands/ocmcmds/components/verify/cmd_test.go index 2633d1722..ee27993b6 100644 --- a/cmds/ocm/commands/ocmcmds/components/verify/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/components/verify/cmd_test.go @@ -27,6 +27,7 @@ import ( "ocm.software/ocm/api/utils/accessio" "ocm.software/ocm/api/utils/accessobj" "ocm.software/ocm/api/utils/mime" + common "ocm.software/ocm/api/utils/misc" ) const ( @@ -50,6 +51,32 @@ const ( PRIVKEY = "/tmp/priv" ) +const ( + S_TESTDATA = "testdata" + D_TESTDATA = "810ff2fb242a5dee4220f2cb0e6a519891fb67f2f828a6cab4ef8894633b1f50" +) + +const ( + S_OTHERDATA = "otherdata" + D_OTHERDATA = "54b8007913ec5a907ca69001d59518acfd106f7b02f892eabf9cae3f8b2414b4" +) + +const VERIFIED_FILE = "verified.yaml" + +const ( + D_COMPONENTA = "01de99400030e8336020059a435cea4e7fe8f21aad4faf619da882134b85569d" + D_COMPONENTB = "5f416ec59629d6af91287e2ba13c6360339b6a0acf624af2abd2a810ce4aefce" +) + +var substitutions = Substitutions{ + "test": D_COMPONENTA, + "r0": D_TESTDATA, + "r1": DS_OCIMANIFEST1.Value, + "r2": DS_OCIMANIFEST2.Value, + "ref": D_COMPONENTB, + "rb0": D_OTHERDATA, +} + var _ = Describe("access method", func() { var ( env *TestEnv @@ -83,7 +110,7 @@ var _ = Describe("access method", func() { env.Version(VERSION, func() { env.Provider(PROVIDER) env.Resource("testdata", "", "PlainText", metav1.LocalRelation, func() { - env.BlobStringData(mime.MIME_TEXT, "testdata") + env.BlobStringData(mime.MIME_TEXT, S_TESTDATA) }) env.Resource("value", "", resourcetypes.OCI_IMAGE, metav1.LocalRelation, func() { env.Access( @@ -102,7 +129,7 @@ var _ = Describe("access method", func() { env.Version(VERSION, func() { env.Provider(PROVIDER) env.Resource("otherdata", "", "PlainText", metav1.LocalRelation, func() { - env.BlobStringData(mime.MIME_TEXT, "otherdata") + env.BlobStringData(mime.MIME_TEXT, S_OTHERDATA) }) env.Reference("ref", COMPONENTA, VERSION) }) @@ -114,21 +141,16 @@ var _ = Describe("access method", func() { env.Cleanup() }) - It("sign component archive", func() { - buf := bytes.NewBuffer(nil) - digest := "5f416ec59629d6af91287e2ba13c6360339b6a0acf624af2abd2a810ce4aefce" - + Prepare := func() { session := datacontext.NewSession() defer session.Close() - src, err := ctf.Open(env.OCMContext(), accessobj.ACC_WRITABLE, ARCH, 0, env) - Expect(err).To(Succeed()) - archcloser := session.AddCloser(src) + src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_WRITABLE, ARCH, 0, env)) + session.AddCloser(src) resolver := resolvers.NewCompoundResolver(src) - cv, err := resolver.LookupComponentVersion(COMPONENTB, VERSION) - Expect(err).To(Succeed()) - closer := session.AddCloser(cv) + cv := Must(resolver.LookupComponentVersion(COMPONENTB, VERSION)) + session.AddCloser(cv) opts := NewOptions( Sign(signingattr.Get(env.OCMContext()).GetSigner(SIGN_ALGO), SIGNATURE), @@ -137,12 +159,19 @@ var _ = Describe("access method", func() { Update(), VerifyDigests(), ) Expect(opts.Complete(DefaultContext)).To(Succeed()) - dig, err := Apply(nil, nil, cv, opts) - Expect(err).To(Succeed()) - closer.Close() - archcloser.Close() + + dig := Must(Apply(nil, nil, cv, opts)) log.Info("dig result", "dig", dig.String()) - Expect(dig.Value).To(Equal(digest)) + Expect(dig.Value).To(Equal(D_COMPONENTB)) + } + + It("verifies transport archive", func() { + buf := bytes.NewBuffer(nil) + + Prepare() + + session := datacontext.NewSession() + defer session.Close() Expect(env.CatchOutput(buf).Execute("verify", "components", "-V", "-s", SIGNATURE, "-k", PUBKEY, "--repo", ARCH, COMPONENTB+":"+VERSION)).To(Succeed()) @@ -150,12 +179,47 @@ var _ = Describe("access method", func() { applying to version "github.com/mandelsoft/ref:v1"[github.com/mandelsoft/ref:v1]... no digest found for "github.com/mandelsoft/test:v1" applying to version "github.com/mandelsoft/test:v1"[github.com/mandelsoft/ref:v1]... - resource 0: "name"="testdata": digest SHA-256:810ff2fb242a5dee4220f2cb0e6a519891fb67f2f828a6cab4ef8894633b1f50[genericBlobDigest/v1] - resource 1: "name"="value": digest SHA-256:0c4abdb72cf59cb4b77f4aacb4775f9f546ebc3face189b2224a966c8826ca9f[ociArtifactDigest/v1] - resource 2: "name"="ref": digest SHA-256:c2d2dca275c33c1270dea6168a002d67c0e98780d7a54960758139ae19984bd7[ociArtifactDigest/v1] - reference 0: github.com/mandelsoft/test:v1: digest SHA-256:01de99400030e8336020059a435cea4e7fe8f21aad4faf619da882134b85569d[jsonNormalisation/v1] - resource 0: "name"="otherdata": digest SHA-256:54b8007913ec5a907ca69001d59518acfd106f7b02f892eabf9cae3f8b2414b4[genericBlobDigest/v1] -successfully verified github.com/mandelsoft/ref:v1 (digest SHA-256:` + digest + `) -`)) + resource 0: "name"="testdata": digest SHA-256:${r0}[genericBlobDigest/v1] + resource 1: "name"="value": digest SHA-256:${r1}[ociArtifactDigest/v1] + resource 2: "name"="ref": digest SHA-256:${r2}[ociArtifactDigest/v1] + reference 0: github.com/mandelsoft/test:v1: digest SHA-256:${test}[jsonNormalisation/v1] + resource 0: "name"="otherdata": digest SHA-256:${rb0}[genericBlobDigest/v1] +successfully verified github.com/mandelsoft/ref:v1 (digest SHA-256:${ref}) +`, substitutions)) + }) + + Context("verified store", func() { + + It("signs transport archive", func() { + Prepare() + + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("verify", "components", "--verified", VERIFIED_FILE, "-s", SIGNATURE, "-k", PUBKEY, "--repo", ARCH, COMPONENTB+":"+VERSION)).To(Succeed()) + + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +applying to version "github.com/mandelsoft/ref:v1"[github.com/mandelsoft/ref:v1]... + no digest found for "github.com/mandelsoft/test:v1" + applying to version "github.com/mandelsoft/test:v1"[github.com/mandelsoft/ref:v1]... + resource 0: "name"="testdata": digest SHA-256:${r0}[genericBlobDigest/v1] + resource 1: "name"="value": digest SHA-256:${r1}[ociArtifactDigest/v1] + resource 2: "name"="ref": digest SHA-256:${r2}[ociArtifactDigest/v1] + reference 0: github.com/mandelsoft/test:v1: digest SHA-256:${test}[jsonNormalisation/v1] + resource 0: "name"="otherdata": digest SHA-256:${rb0}[genericBlobDigest/v1] +successfully verified github.com/mandelsoft/ref:v1 (digest SHA-256:${ref}) +`, substitutions)) + + Expect(Must(env.FileExists(VERIFIED_FILE))).To(BeTrue()) + + store := Must(NewVerifiedStore(VERIFIED_FILE, env.FileSystem())) + + CheckStore(store, common.NewNameVersion(COMPONENTA, VERSION)) + CheckStore(store, common.NewNameVersion(COMPONENTB, VERSION)) + }) }) }) + +func CheckStore(store VerifiedStore, ve common.VersionedElement) { + e := store.Get(ve) + ExpectWithOffset(1, e).NotTo(BeNil()) + ExpectWithOffset(1, common.VersionedElementKey(e)).To(Equal(common.VersionedElementKey(ve))) +} diff --git a/cmds/ocm/commands/ocmcmds/names/names.go b/cmds/ocm/commands/ocmcmds/names/names.go index 7080e38dc..24927051f 100644 --- a/cmds/ocm/commands/ocmcmds/names/names.go +++ b/cmds/ocm/commands/ocmcmds/names/names.go @@ -16,4 +16,5 @@ var ( Action = []string{"action"} RoutingSlips = []string{"routingslips", "routingslip", "rs"} PubSub = []string{"pubsub", "ps"} + Verified = []string{"verified"} ) diff --git a/cmds/ocm/commands/ocmcmds/references/add/cmd_test.go b/cmds/ocm/commands/ocmcmds/references/add/cmd_test.go index cde4c0db6..db064e9f3 100644 --- a/cmds/ocm/commands/ocmcmds/references/add/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/references/add/cmd_test.go @@ -20,7 +20,7 @@ const ( REF = "github.com/mandelsoft/ref" ) -func CheckReference(env *TestEnv, cd *compdesc.ComponentDescriptor, name string, add ...func(compdesc.ComponentReference)) { +func CheckReference(env *TestEnv, cd *compdesc.ComponentDescriptor, name string, add ...func(compdesc.Reference)) { rs, _ := cd.GetReferencesByName(name) if len(rs) != 1 { Fail(fmt.Sprintf("%d reference(s) with name %s found", len(rs), name), 1) @@ -64,7 +64,7 @@ var _ = Describe("Add references", func() { Expect(err).To(Succeed()) Expect(len(cd.References)).To(Equal(1)) - CheckReference(env, cd, "testdata", func(r compdesc.ComponentReference) { + CheckReference(env, cd, "testdata", func(r compdesc.Reference) { Expect(r.ExtraIdentity).To(Equal(metav1.Identity{"purpose": "test", "label": "local"})) }) }) @@ -127,7 +127,7 @@ labels: labels := metav1.Labels{} labels.Set("test", "value") - CheckReference(env, cd, "testdata", func(r compdesc.ComponentReference) { + CheckReference(env, cd, "testdata", func(r compdesc.Reference) { ExpectWithOffset(2, r.GetLabels()).To(Equal(labels)) }) }) @@ -148,7 +148,7 @@ labels: labels := metav1.Labels{} labels.Set("test", "value") - CheckReference(env, cd, "testdata", func(r compdesc.ComponentReference) { + CheckReference(env, cd, "testdata", func(r compdesc.Reference) { Expect(r.GetLabels()).To(Equal(labels)) }) }) @@ -163,7 +163,7 @@ labels: labels := metav1.Labels{} labels.Set("test", "value") - CheckReference(env, cd, "testdata", func(r compdesc.ComponentReference) { + CheckReference(env, cd, "testdata", func(r compdesc.Reference) { Expect(r.ExtraIdentity).To(Equal(metav1.Identity{"purpose": "test", "label": "local"})) }) }) @@ -179,7 +179,7 @@ labels: labels := metav1.Labels{} labels.Set("purpose", "test", metav1.WithSigning()) labels.Set("label", map[string]interface{}{"local": true}, metav1.WithVersion("v1")) - CheckReference(env, cd, "testdata", func(r compdesc.ComponentReference) { + CheckReference(env, cd, "testdata", func(r compdesc.Reference) { Expect(r.GetLabels()).To(Equal(labels)) }) }) diff --git a/cmds/ocm/commands/ocmcmds/references/common/typehandler.go b/cmds/ocm/commands/ocmcmds/references/common/typehandler.go index 5bb267644..4adeb7a00 100644 --- a/cmds/ocm/commands/ocmcmds/references/common/typehandler.go +++ b/cmds/ocm/commands/ocmcmds/references/common/typehandler.go @@ -9,8 +9,8 @@ import ( "ocm.software/ocm/cmds/ocm/common/utils" ) -func Elem(e interface{}) *compdesc.ComponentReference { - return e.(*elemhdlr.Object).Element.(*compdesc.ComponentReference) +func Elem(e interface{}) *compdesc.Reference { + return e.(*elemhdlr.Object).Element.(*compdesc.Reference) } var OptionsFor = elemhdlr.OptionsFor diff --git a/cmds/ocm/commands/ocmcmds/resources/download/action.go b/cmds/ocm/commands/ocmcmds/resources/download/action.go index 27d36e72a..8fada4b0b 100644 --- a/cmds/ocm/commands/ocmcmds/resources/download/action.go +++ b/cmds/ocm/commands/ocmcmds/resources/download/action.go @@ -12,10 +12,13 @@ import ( "ocm.software/ocm/api/ocm" v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" "ocm.software/ocm/api/ocm/extensions/download" + "ocm.software/ocm/api/ocm/tools/signing" + "ocm.software/ocm/api/utils/blobaccess" common2 "ocm.software/ocm/api/utils/misc" "ocm.software/ocm/api/utils/out" "ocm.software/ocm/cmds/ocm/commands/common/options/destoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/handlers/elemhdlr" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/storeoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/resources/common" "ocm.software/ocm/cmds/ocm/common/options" "ocm.software/ocm/cmds/ocm/common/output" @@ -94,11 +97,16 @@ func (d *Action) Save(o *elemhdlr.Object, f string) error { printer := common2.NewPrinter(d.opts.Context.StdOut()) dest := destoption.From(d.opts) local := From(d.opts) + verify := storeoption.From(d.opts) pathIn := true r := common.Elem(o) if f == "" { pathIn = false } + if verify.Store != nil { + local.Verify = true + } + var tmp vfs.File var err error if f == "-" { @@ -129,6 +137,24 @@ func (d *Action) Save(o *elemhdlr.Object, f string) error { ok, eff, err = d.downloaders.Download(printer, racc, f, dest.PathFilesystem) } else { ok, eff, err = d.downloaders.DownloadAsBlob(printer, racc, f, dest.PathFilesystem) + + if local.Verify { + var done bool + done, err = signing.VerifyResourceDigest(o.Version, o.Index, blobaccess.DataAccessForFile(dest.PathFilesystem, f), verify.Store) + if err != nil { + if done { + printer.Printf("%s: verification failed: %s\n", eff, err) + } else { + printer.Printf("%s: cannot verify: %s\n", eff, err) + } + } else { + if done { + printer.Printf("%s: resource content verified\n", eff) + } else { + printer.Printf("%s: no resource content verification possible\n", eff) + } + } + } } if err != nil { return err diff --git a/cmds/ocm/commands/ocmcmds/resources/download/cmd.go b/cmds/ocm/commands/ocmcmds/resources/download/cmd.go index ec6117660..1f895840c 100644 --- a/cmds/ocm/commands/ocmcmds/resources/download/cmd.go +++ b/cmds/ocm/commands/ocmcmds/resources/download/cmd.go @@ -3,6 +3,7 @@ package download import ( "runtime" + "github.com/mandelsoft/goutils/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -18,6 +19,7 @@ import ( "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/downloaderoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/lookupoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/repooption" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/storeoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/versionconstraintsoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/names" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/resources/common" @@ -50,7 +52,7 @@ func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { versionconstraintsoption.New(), repooption.New(), downloaderoption.New(ctx.OCMContext()), - output.OutputOptions(output.NewOutputs(f), NewOptions(), closureoption.New("component reference"), lookupoption.New(), destoption.New()), + output.OutputOptions(output.NewOutputs(f), NewOptions(), closureoption.New("component reference"), lookupoption.New(), destoption.New(), storeoption.New("check-verified")), )}, utils.Names(Names, names...)...) } @@ -145,6 +147,12 @@ func (o *Command) Run() error { From(opts).UseHandlers = true } + if storeoption.From(o).Store != nil { + if From(opts).UseHandlers { + return errors.Newf("verification fot supported together with download handlers") + } + } + hdlr, err := common.NewTypeHandler(o.Context.OCM(), opts, repooption.From(o).Repository, session, []string{o.Comp}, o.handlerOptions()...) if err != nil { return err diff --git a/cmds/ocm/commands/ocmcmds/resources/download/cmd_test.go b/cmds/ocm/commands/ocmcmds/resources/download/cmd_test.go index f7ed44fe2..5f1a96008 100644 --- a/cmds/ocm/commands/ocmcmds/resources/download/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/resources/download/cmd_test.go @@ -2,6 +2,7 @@ package download_test import ( "bytes" + "os" . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" @@ -10,9 +11,17 @@ import ( "github.com/mandelsoft/vfs/pkg/vfs" + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/ocm" metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/ocm/extensions/attrs/signingattr" + "ocm.software/ocm/api/ocm/extensions/repositories/ctf" + "ocm.software/ocm/api/ocm/tools/signing" + "ocm.software/ocm/api/tech/signing/handlers/rsa" "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" "ocm.software/ocm/api/utils/mime" + common "ocm.software/ocm/api/utils/misc" ) const ( @@ -24,6 +33,18 @@ const ( OUT = "/tmp/res" ) +const ( + PUBKEY = "/tmp/pub" + PRIVKEY = "/tmp/priv" +) + +const ( + SIGNATURE = "test" + SIGN_ALGO = rsa.Algorithm +) + +const VERIFIED_FILE = "verified.yaml" + var _ = Describe("Test Environment", func() { var env *TestEnv @@ -117,4 +138,103 @@ var _ = Describe("Test Environment", func() { Expect(env.ReadFile(vfs.Join(env.FileSystem(), OUT, COMP2+"/"+VERSION+"/"+COMP+"/"+VERSION+"/testdata"))).To(Equal([]byte("testdata"))) }) }) + + Context("verification", func() { + priv, pub, err := rsa.Handler{}.CreateKeyPair() + Expect(err).To(Succeed()) + + BeforeEach(func() { + env = NewTestEnv() + data, err := rsa.KeyData(pub) + Expect(err).To(Succeed()) + Expect(vfs.WriteFile(env.FileSystem(), PUBKEY, data, os.ModePerm)).To(Succeed()) + data, err = rsa.KeyData(priv) + Expect(err).To(Succeed()) + Expect(vfs.WriteFile(env.FileSystem(), PRIVKEY, data, os.ModePerm)).To(Succeed()) + + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.Component(COMP, func() { + env.Version(VERSION, func() { + env.Provider(PROVIDER) + env.Resource("testdata", "", "PlainText", metav1.LocalRelation, func() { + env.BlobStringData(mime.MIME_TEXT, "testdata") + }) + }) + }) + }) + }) + + Prepare := func() { + src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_WRITABLE, ARCH, 0, env)) + defer Close(src, "repo") + + resolver := ocm.NewCompoundResolver(src) + cv := Must(resolver.LookupComponentVersion(COMP, VERSION)) + defer Close(cv, "cv") + + opts := signing.NewOptions( + signing.Sign(signingattr.Get(env.OCMContext()).GetSigner(SIGN_ALGO), SIGNATURE), + signing.Resolver(resolver), + signing.PrivateKey(SIGNATURE, priv), + signing.Update(), signing.VerifyDigests(), + ) + Expect(opts.Complete(env.OCMContext())).To(Succeed()) + + Must(signing.Apply(nil, nil, cv, opts)) + } + + It("verifies download after component verification", func() { + buf := bytes.NewBuffer(nil) + + Prepare() + + session := datacontext.NewSession() + defer session.Close() + + Expect(env.CatchOutput(buf).Execute("verify", "components", "--verified", VERIFIED_FILE, "-V", "-s", SIGNATURE, "-k", PUBKEY, "--repo", ARCH, COMP+":"+VERSION)).To(Succeed()) + + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +applying to version "test.de/x:v1"[test.de/x:v1]... + resource 0: "name"="testdata": digest SHA-256:810ff2fb242a5dee4220f2cb0e6a519891fb67f2f828a6cab4ef8894633b1f50[genericBlobDigest/v1] +successfully verified test.de/x:v1 (digest SHA-256:ba5b4af72fcb707a4a8ebc48b088c0d8ed772cb021995b9b1fc7a01fbac29cd2) +`)) + + buf.Reset() + Expect(env.CatchOutput(buf).Execute("download", "resources", "--verified", VERIFIED_FILE, "-O", OUT, ARCH)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +/tmp/res: 8 byte(s) written +/tmp/res: resource content verified +`)) + Expect(env.FileExists(OUT)).To(BeTrue()) + Expect(env.ReadFile(OUT)).To(Equal([]byte("testdata"))) + }) + + It("detects manipulated download after component verification", func() { + buf := bytes.NewBuffer(nil) + + Prepare() + + session := datacontext.NewSession() + defer session.Close() + + Expect(env.CatchOutput(buf).Execute("verify", "components", "--verified", VERIFIED_FILE, "-V", "-s", SIGNATURE, "-k", PUBKEY, "--repo", ARCH, COMP+":"+VERSION)).To(Succeed()) + + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +applying to version "test.de/x:v1"[test.de/x:v1]... + resource 0: "name"="testdata": digest SHA-256:810ff2fb242a5dee4220f2cb0e6a519891fb67f2f828a6cab4ef8894633b1f50[genericBlobDigest/v1] +successfully verified test.de/x:v1 (digest SHA-256:ba5b4af72fcb707a4a8ebc48b088c0d8ed772cb021995b9b1fc7a01fbac29cd2) +`)) + + store := Must(signing.NewVerifiedStore(VERIFIED_FILE, env.FileSystem())) + + cd := store.Get(common.NewNameVersion(COMP, VERSION)) + + cd.Resources[0].Digest.Value = "b" + cd.Resources[0].Digest.Value[1:] + store.Add(cd) + MustBeSuccessful(store.Save()) + + ExpectError(env.Execute("download", "resources", "--verified", VERIFIED_FILE, "-O", OUT, ARCH)).To(MatchError("component version test.de/x:v1 corrupted")) + }) + }) }) diff --git a/cmds/ocm/commands/ocmcmds/resources/download/options.go b/cmds/ocm/commands/ocmcmds/resources/download/options.go index f411d4b4c..4942477e1 100644 --- a/cmds/ocm/commands/ocmcmds/resources/download/options.go +++ b/cmds/ocm/commands/ocmcmds/resources/download/options.go @@ -20,6 +20,7 @@ func NewOptions(silent ...bool) *Option { type Option struct { SilentOption bool UseHandlers bool + Verify bool } func (o *Option) SetUseHandlers(ok ...bool) *Option { @@ -31,6 +32,7 @@ func (o *Option) AddFlags(fs *pflag.FlagSet) { if !o.SilentOption { fs.BoolVarP(&o.UseHandlers, "download-handlers", "d", false, "use download handler if possible") } + fs.BoolVarP(&o.Verify, "verify", "", false, "verify downloads") } func (o *Option) Usage() string { diff --git a/cmds/ocm/commands/ocmcmds/verified/cmd.go b/cmds/ocm/commands/ocmcmds/verified/cmd.go new file mode 100644 index 000000000..26fc5c494 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/verified/cmd.go @@ -0,0 +1,21 @@ +package verified + +import ( + "github.com/spf13/cobra" + + clictx "ocm.software/ocm/api/cli" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/names" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/verified/get" + "ocm.software/ocm/cmds/ocm/common/utils" +) + +var Names = names.Verified + +// NewCommand creates a new command. +func NewCommand(ctx clictx.Context) *cobra.Command { + cmd := utils.MassageCommand(&cobra.Command{ + Short: "Commands acting on verified component versions", + }, Names...) + cmd.AddCommand(get.NewCommand(ctx, get.Verb)) + return cmd +} diff --git a/cmds/ocm/commands/ocmcmds/verified/get/cmd.go b/cmds/ocm/commands/ocmcmds/verified/get/cmd.go new file mode 100644 index 000000000..fcaeb36db --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/verified/get/cmd.go @@ -0,0 +1,106 @@ +package get + +import ( + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + clictx "ocm.software/ocm/api/cli" + handler "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/handlers/verifiedhdlr" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/storeoption" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/names" + "ocm.software/ocm/cmds/ocm/commands/verbs" + "ocm.software/ocm/cmds/ocm/common/output" + "ocm.software/ocm/cmds/ocm/common/processing" + "ocm.software/ocm/cmds/ocm/common/utils" +) + +var ( + Names = names.Verified + Verb = verbs.Get +) + +type Command struct { + utils.BaseCommand + + path string + Names []string +} + +// NewCommand creates a new ctf command. +func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { + return utils.SetupCommand( + &Command{ + BaseCommand: utils.NewBaseCommand(ctx, output.OutputOptions(outputs).SortColumns("VERSION", "COMPONENT")), + }, + utils.Names(Names, names...)..., + ) +} + +func (o *Command) ForName(name string) *cobra.Command { + return &cobra.Command{ + Use: "[] { 0 { + var fields []string + for _, f := range o.sort { + split := strings.Split(f, ",") + for _, s := range split { + s = strings.TrimSpace(s) + if s != "" { + if avail.Contains(s) { + fields = append(fields, s) + } else { + return errors.ErrInvalid("sort field", s) + } } } } + o.Sort = fields } - o.Sort = fields return nil } diff --git a/docs/reference/ocm_download_cli.md b/docs/reference/ocm_download_cli.md index d6ca2db1f..ff71ec89d 100644 --- a/docs/reference/ocm_download_cli.md +++ b/docs/reference/ocm_download_cli.md @@ -20,6 +20,9 @@ cli, ocmcli, ocm-cli -O, --outfile string output file or directory -p, --path lookup executable in PATH --repo string repository name or spec + --use-verified enable verification store + --verified string file used to remember verifications for downloads (default "~/.ocm/verified") + --verify verify downloads ``` ### Description @@ -89,6 +92,15 @@ can be download directly as helm chart archive, even if stored as OCI artifact. This is handled by download handler. Their usage can be enabled with the --download-handlers option. Otherwise the resource as returned by the access method is stored. + +If the verification store is enabled, resources downloaded from +signed or verified component versions are verified against their digests +provided by the component version.(not supported for using downloaders for the +resource download). + +The usage of the verification store is enabled by --use-verified or by +specifying a verification file with --verified. + ### SEE ALSO #### Parents diff --git a/docs/reference/ocm_download_resources.md b/docs/reference/ocm_download_resources.md index 7a931b4fb..368244159 100644 --- a/docs/reference/ocm_download_resources.md +++ b/docs/reference/ocm_download_resources.md @@ -15,6 +15,7 @@ resources, resource, res, r ### Options ```text + --check-verified enable verification store -c, --constraints constraints version constraint -d, --download-handlers use download handler if possible --downloader = artifact downloader ([:[:]]=--check-verified or by +specifying a verification file with --verified. + ### SEE ALSO #### Parents diff --git a/docs/reference/ocm_get.md b/docs/reference/ocm_get.md index f585f6940..28a479765 100644 --- a/docs/reference/ocm_get.md +++ b/docs/reference/ocm_get.md @@ -31,4 +31,5 @@ ocm get [] ... * [ocm get resources](ocm_get_resources.md) — get resources of a component version * [ocm get routingslips](ocm_get_routingslips.md) — get routings slips for a component version * [ocm get sources](ocm_get_sources.md) — get sources of a component version +* [ocm get verified](ocm_get_verified.md) — get verified component versions diff --git a/docs/reference/ocm_get_verified.md b/docs/reference/ocm_get_verified.md new file mode 100644 index 000000000..039eaba30 --- /dev/null +++ b/docs/reference/ocm_get_verified.md @@ -0,0 +1,44 @@ +## ocm get verified — Get Verified Component Versions + +### Synopsis + +```bash +ocm get verified [] {--output the output mode can be selected. +The following modes are supported: + - (default) + - JSON + - json + - wide + - yaml + +### Examples + +```text +$ ocm get verified +$ ocm get verified -f verified.yaml acme.org/component -o yaml +``` + +### SEE ALSO + +#### Parents + +* [ocm get](ocm_get.md) — Get information about artifacts and components +* [ocm](ocm.md) — Open Component Model command line client + diff --git a/docs/reference/ocm_ocm.md b/docs/reference/ocm_ocm.md index 152f07731..2c1f092df 100644 --- a/docs/reference/ocm_ocm.md +++ b/docs/reference/ocm_ocm.md @@ -32,6 +32,7 @@ ocm ocm [] ... * ocm ocm routingslips — Commands working on routing slips * ocm ocm source-configuration — Commands acting on component source specifications * ocm ocm sources — Commands acting on component sources +* ocm ocm verified — Commands acting on verified component versions * ocm ocm versions — Commands acting on component version names diff --git a/docs/reference/ocm_sign_componentversions.md b/docs/reference/ocm_sign_componentversions.md index 646d92f50..89f5cbd39 100644 --- a/docs/reference/ocm_sign_componentversions.md +++ b/docs/reference/ocm_sign_componentversions.md @@ -15,6 +15,7 @@ componentversions, componentversion, cv, components, component, comps, comp, c ### Options ```text + -- enable verification store -S, --algorithm string signature handler (default "RSASSA-PKCS1-V1_5") --ca-cert stringArray additional root certificate authorities (for signing certificates) -c, --constraints constraints version constraint @@ -33,6 +34,7 @@ componentversions, componentversion, cv, components, component, comps, comp, c --tsa use timestamp authority (default server: http://timestamp.digicert.com) --tsa-url string TSA server URL --update update digest in component versions (default true) + --verified string file used to remember verifications for downloads (default "~/.ocm/verified") -V, --verify verify existing digests (default true) ``` @@ -101,6 +103,14 @@ Alternatively a key can be specified as base64 encoded string if the argument start with the prefix ! or as direct string with the prefix =. +If the verification store is enabled, resources downloaded from +signed or verified component versions are verified against their digests +provided by the component version.(not supported for using downloaders for the +resource download). + +The usage of the verification store is enabled by -- or by +specifying a verification file with --verified. + If in signing mode a public key is specified, existing signatures for the given signature name will be verified, instead of recreated. diff --git a/docs/reference/ocm_verify_componentversions.md b/docs/reference/ocm_verify_componentversions.md index dad9e5b01..3ec362add 100644 --- a/docs/reference/ocm_verify_componentversions.md +++ b/docs/reference/ocm_verify_componentversions.md @@ -15,6 +15,7 @@ componentversions, componentversion, cv, components, component, comps, comp, c ### Options ```text + -- enable verification store --ca-cert stringArray additional root certificate authorities (for signing certificates) -c, --constraints constraints version constraint -h, --help help for componentversions @@ -27,6 +28,7 @@ componentversions, componentversion, cv, components, component, comps, comp, c -k, --public-key stringArray public key setting --repo string repository name or spec -s, --signature stringArray signature name + --verified string file used to remember verifications for downloads (default "~/.ocm/verified") -V, --verify verify existing digests ``` @@ -98,6 +100,14 @@ Alternatively a key can be specified as base64 encoded string if the argument start with the prefix ! or as direct string with the prefix =. +If the verification store is enabled, resources downloaded from +signed or verified component versions are verified against their digests +provided by the component version.(not supported for using downloaders for the +resource download). + +The usage of the verification store is enabled by -- or by +specifying a verification file with --verified. + \ If a component lookup for building a reference closure is required the --lookup option can be used to specify a fallback