Skip to content

Commit 5387e5a

Browse files
authored
Merge pull request #1930 from mtrmac/encryption-mime-types
Fix conversion determination when encrypting
2 parents f5e31f1 + cd5d287 commit 5387e5a

File tree

4 files changed

+248
-50
lines changed

4 files changed

+248
-50
lines changed

copy/blob.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (ic *imageCopier) copyBlobFromStream(ctx context.Context, srcReader io.Read
4343
stream.reader = bar.ProxyReader(stream.reader)
4444

4545
// === Decrypt the stream, if required.
46-
decryptionStep, err := ic.c.blobPipelineDecryptionStep(&stream, srcInfo)
46+
decryptionStep, err := ic.blobPipelineDecryptionStep(&stream, srcInfo)
4747
if err != nil {
4848
return types.BlobInfo{}, err
4949
}
@@ -78,7 +78,7 @@ func (ic *imageCopier) copyBlobFromStream(ctx context.Context, srcReader io.Read
7878
// Before relaxing this, see the original pull request’s review if there are other reasons to reject this.
7979
return types.BlobInfo{}, errors.New("Unable to support both decryption and encryption in the same copy")
8080
}
81-
encryptionStep, err := ic.c.blobPipelineEncryptionStep(&stream, toEncrypt, srcInfo, decryptionStep)
81+
encryptionStep, err := ic.blobPipelineEncryptionStep(&stream, toEncrypt, srcInfo, decryptionStep)
8282
if err != nil {
8383
return types.BlobInfo{}, err
8484
}

copy/encryption.go

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,33 @@ type bpDecryptionStepData struct {
3333
// blobPipelineDecryptionStep updates *stream to decrypt if, it necessary.
3434
// srcInfo is only used for error messages.
3535
// Returns data for other steps; the caller should eventually use updateCryptoOperation.
36-
func (c *copier) blobPipelineDecryptionStep(stream *sourceStream, srcInfo types.BlobInfo) (*bpDecryptionStepData, error) {
37-
if isOciEncrypted(stream.info.MediaType) && c.ociDecryptConfig != nil {
38-
desc := imgspecv1.Descriptor{
39-
Annotations: stream.info.Annotations,
40-
}
41-
reader, decryptedDigest, err := ocicrypt.DecryptLayer(c.ociDecryptConfig, stream.reader, desc, false)
42-
if err != nil {
43-
return nil, fmt.Errorf("decrypting layer %s: %w", srcInfo.Digest, err)
44-
}
45-
46-
stream.reader = reader
47-
stream.info.Digest = decryptedDigest
48-
stream.info.Size = -1
49-
maps.DeleteFunc(stream.info.Annotations, func(k string, _ string) bool {
50-
return strings.HasPrefix(k, "org.opencontainers.image.enc")
51-
})
36+
func (ic *imageCopier) blobPipelineDecryptionStep(stream *sourceStream, srcInfo types.BlobInfo) (*bpDecryptionStepData, error) {
37+
if !isOciEncrypted(stream.info.MediaType) || ic.c.ociDecryptConfig == nil {
5238
return &bpDecryptionStepData{
53-
decrypting: true,
39+
decrypting: false,
5440
}, nil
5541
}
42+
43+
if ic.cannotModifyManifestReason != "" {
44+
return nil, fmt.Errorf("layer %s should be decrypted, but we can’t modify the manifest: %s", srcInfo.Digest, ic.cannotModifyManifestReason)
45+
}
46+
47+
desc := imgspecv1.Descriptor{
48+
Annotations: stream.info.Annotations,
49+
}
50+
reader, decryptedDigest, err := ocicrypt.DecryptLayer(ic.c.ociDecryptConfig, stream.reader, desc, false)
51+
if err != nil {
52+
return nil, fmt.Errorf("decrypting layer %s: %w", srcInfo.Digest, err)
53+
}
54+
55+
stream.reader = reader
56+
stream.info.Digest = decryptedDigest
57+
stream.info.Size = -1
58+
maps.DeleteFunc(stream.info.Annotations, func(k string, _ string) bool {
59+
return strings.HasPrefix(k, "org.opencontainers.image.enc")
60+
})
5661
return &bpDecryptionStepData{
57-
decrypting: false,
62+
decrypting: true,
5863
}, nil
5964
}
6065

@@ -74,34 +79,39 @@ type bpEncryptionStepData struct {
7479
// blobPipelineEncryptionStep updates *stream to encrypt if, it required by toEncrypt.
7580
// srcInfo is primarily used for error messages.
7681
// Returns data for other steps; the caller should eventually call updateCryptoOperationAndAnnotations.
77-
func (c *copier) blobPipelineEncryptionStep(stream *sourceStream, toEncrypt bool, srcInfo types.BlobInfo,
82+
func (ic *imageCopier) blobPipelineEncryptionStep(stream *sourceStream, toEncrypt bool, srcInfo types.BlobInfo,
7883
decryptionStep *bpDecryptionStepData) (*bpEncryptionStepData, error) {
79-
if toEncrypt && !isOciEncrypted(srcInfo.MediaType) && c.ociEncryptConfig != nil {
80-
var annotations map[string]string
81-
if !decryptionStep.decrypting {
82-
annotations = srcInfo.Annotations
83-
}
84-
desc := imgspecv1.Descriptor{
85-
MediaType: srcInfo.MediaType,
86-
Digest: srcInfo.Digest,
87-
Size: srcInfo.Size,
88-
Annotations: annotations,
89-
}
90-
reader, finalizer, err := ocicrypt.EncryptLayer(c.ociEncryptConfig, stream.reader, desc)
91-
if err != nil {
92-
return nil, fmt.Errorf("encrypting blob %s: %w", srcInfo.Digest, err)
93-
}
94-
95-
stream.reader = reader
96-
stream.info.Digest = ""
97-
stream.info.Size = -1
84+
if !toEncrypt || isOciEncrypted(srcInfo.MediaType) || ic.c.ociEncryptConfig == nil {
9885
return &bpEncryptionStepData{
99-
encrypting: true,
100-
finalizer: finalizer,
86+
encrypting: false,
10187
}, nil
10288
}
89+
90+
if ic.cannotModifyManifestReason != "" {
91+
return nil, fmt.Errorf("layer %s should be encrypted, but we can’t modify the manifest: %s", srcInfo.Digest, ic.cannotModifyManifestReason)
92+
}
93+
94+
var annotations map[string]string
95+
if !decryptionStep.decrypting {
96+
annotations = srcInfo.Annotations
97+
}
98+
desc := imgspecv1.Descriptor{
99+
MediaType: srcInfo.MediaType,
100+
Digest: srcInfo.Digest,
101+
Size: srcInfo.Size,
102+
Annotations: annotations,
103+
}
104+
reader, finalizer, err := ocicrypt.EncryptLayer(ic.c.ociEncryptConfig, stream.reader, desc)
105+
if err != nil {
106+
return nil, fmt.Errorf("encrypting blob %s: %w", srcInfo.Digest, err)
107+
}
108+
109+
stream.reader = reader
110+
stream.info.Digest = ""
111+
stream.info.Size = -1
103112
return &bpEncryptionStepData{
104-
encrypting: false,
113+
encrypting: true,
114+
finalizer: finalizer,
105115
}, nil
106116
}
107117

copy/manifest.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/containers/image/v5/internal/set"
1010
"github.com/containers/image/v5/manifest"
1111
"github.com/containers/image/v5/types"
12+
v1 "github.com/opencontainers/image-spec/specs-go/v1"
1213
"github.com/sirupsen/logrus"
1314
"golang.org/x/exp/slices"
1415
)
@@ -18,6 +19,9 @@ import (
1819
// Include v2s1 signed but not v2s1 unsigned, because docker/distribution requires a signature even if the unsigned MIME type is used.
1920
var preferredManifestMIMETypes = []string{manifest.DockerV2Schema2MediaType, manifest.DockerV2Schema1SignedMediaType}
2021

22+
// ociEncryptionMIMETypes lists manifest MIME types that are known to support OCI encryption.
23+
var ociEncryptionMIMETypes = []string{v1.MediaTypeImageManifest}
24+
2125
// orderedSet is a list of strings (MIME types or platform descriptors in our case), with each string appearing at most once.
2226
type orderedSet struct {
2327
list []string
@@ -76,18 +80,42 @@ func determineManifestConversion(in determineManifestConversionInputs) (manifest
7680
destSupportedManifestMIMETypes = []string{in.forceManifestMIMEType}
7781
}
7882

79-
if len(destSupportedManifestMIMETypes) == 0 && (!in.requiresOCIEncryption || manifest.MIMETypeSupportsEncryption(srcType)) {
80-
return manifestConversionPlan{ // Anything goes; just use the original as is, do not try any conversions.
81-
preferredMIMEType: srcType,
82-
otherMIMETypeCandidates: []string{},
83-
}, nil
83+
if len(destSupportedManifestMIMETypes) == 0 {
84+
if !in.requiresOCIEncryption || manifest.MIMETypeSupportsEncryption(srcType) {
85+
return manifestConversionPlan{ // Anything goes; just use the original as is, do not try any conversions.
86+
preferredMIMEType: srcType,
87+
otherMIMETypeCandidates: []string{},
88+
}, nil
89+
}
90+
destSupportedManifestMIMETypes = ociEncryptionMIMETypes
8491
}
8592
supportedByDest := set.New[string]()
8693
for _, t := range destSupportedManifestMIMETypes {
8794
if !in.requiresOCIEncryption || manifest.MIMETypeSupportsEncryption(t) {
8895
supportedByDest.Add(t)
8996
}
9097
}
98+
if supportedByDest.Empty() {
99+
if len(destSupportedManifestMIMETypes) == 0 { // Coverage: This should never happen, empty values were replaced by ociEncryptionMIMETypes
100+
return manifestConversionPlan{}, errors.New("internal error: destSupportedManifestMIMETypes is empty")
101+
}
102+
// We know, and have verified, that destSupportedManifestMIMETypes is not empty, so encryption must have been involved.
103+
if !in.requiresOCIEncryption { // Coverage: This should never happen, destSupportedManifestMIMETypes was not empty, so we should have filtered for encryption.
104+
return manifestConversionPlan{}, errors.New("internal error: supportedByDest is empty but destSupportedManifestMIMETypes is not, and not encrypting")
105+
}
106+
// destSupportedManifestMIMETypes has three possible origins:
107+
if in.forceManifestMIMEType != "" { // 1. forceManifestType specified
108+
return manifestConversionPlan{}, fmt.Errorf("encryption required together with format %s, which does not support encryption",
109+
in.forceManifestMIMEType)
110+
}
111+
if len(in.destSupportedManifestMIMETypes) == 0 { // 2. destination accepts anything and we have chosen ociEncryptionMIMETypes
112+
// Coverage: This should never happen, ociEncryptionMIMETypes all support encryption
113+
return manifestConversionPlan{}, errors.New("internal error: in.destSupportedManifestMIMETypes is empty but supportedByDest is empty as well")
114+
}
115+
// 3. destination does not support encryption.
116+
return manifestConversionPlan{}, fmt.Errorf("encryption required but the destination only supports MIME types [%s], none of which support encryption",
117+
strings.Join(destSupportedManifestMIMETypes, ", "))
118+
}
91119

92120
// destSupportedManifestMIMETypes is a static guess; a particular registry may still only support a subset of the types.
93121
// So, build a list of types to try in order of decreasing preference.
@@ -122,11 +150,13 @@ func determineManifestConversion(in determineManifestConversionInputs) (manifest
122150

123151
// Finally, try anything else the destination supports.
124152
for _, t := range destSupportedManifestMIMETypes {
125-
prioritizedTypes.append(t)
153+
if supportedByDest.Contains(t) {
154+
prioritizedTypes.append(t)
155+
}
126156
}
127157

128158
logrus.Debugf("Manifest has MIME type %s, ordered candidate list [%s]", srcType, strings.Join(prioritizedTypes.list, ", "))
129-
if len(prioritizedTypes.list) == 0 { // Coverage: destSupportedManifestMIMETypes is not empty (or we would have exited in the “Anything goes” case above), so this should never happen.
159+
if len(prioritizedTypes.list) == 0 { // Coverage: destSupportedManifestMIMETypes and supportedByDest, which is a subset, is not empty (or we would have exited above), so this should never happen.
130160
return manifestConversionPlan{}, errors.New("Internal error: no candidate MIME types")
131161
}
132162
res := manifestConversionPlan{

copy/manifest_test.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,164 @@ func TestDetermineManifestConversion(t *testing.T) {
215215
otherMIMETypeCandidates: []string{},
216216
}, res, c.description)
217217
}
218+
219+
// When encryption is required:
220+
for _, c := range []struct {
221+
description string
222+
in determineManifestConversionInputs // with requiresOCIEncryption implied
223+
expected manifestConversionPlan // Or {} to expect a failure
224+
}{
225+
{ // Destination accepts anything - no conversion necessary
226+
"OCI→anything",
227+
determineManifestConversionInputs{
228+
srcMIMEType: v1.MediaTypeImageManifest,
229+
destSupportedManifestMIMETypes: nil,
230+
},
231+
manifestConversionPlan{
232+
preferredMIMEType: v1.MediaTypeImageManifest,
233+
preferredMIMETypeNeedsConversion: false,
234+
otherMIMETypeCandidates: []string{},
235+
},
236+
},
237+
{ // Destination accepts anything - need to convert for encryption
238+
"s2→anything",
239+
determineManifestConversionInputs{
240+
srcMIMEType: manifest.DockerV2Schema2MediaType,
241+
destSupportedManifestMIMETypes: nil,
242+
},
243+
manifestConversionPlan{
244+
preferredMIMEType: v1.MediaTypeImageManifest,
245+
preferredMIMETypeNeedsConversion: true,
246+
otherMIMETypeCandidates: []string{},
247+
},
248+
},
249+
// Destination accepts an encrypted format
250+
{
251+
"OCI→OCI",
252+
determineManifestConversionInputs{
253+
srcMIMEType: v1.MediaTypeImageManifest,
254+
destSupportedManifestMIMETypes: supportS1S2OCI,
255+
},
256+
manifestConversionPlan{
257+
preferredMIMEType: v1.MediaTypeImageManifest,
258+
preferredMIMETypeNeedsConversion: false,
259+
otherMIMETypeCandidates: []string{},
260+
},
261+
},
262+
{
263+
"s2→OCI",
264+
determineManifestConversionInputs{
265+
srcMIMEType: manifest.DockerV2Schema2MediaType,
266+
destSupportedManifestMIMETypes: supportS1S2OCI,
267+
},
268+
manifestConversionPlan{
269+
preferredMIMEType: v1.MediaTypeImageManifest,
270+
preferredMIMETypeNeedsConversion: true,
271+
otherMIMETypeCandidates: []string{},
272+
},
273+
},
274+
// Destination does not accept an encrypted format
275+
{
276+
"OCI→s2",
277+
determineManifestConversionInputs{
278+
srcMIMEType: v1.MediaTypeImageManifest,
279+
destSupportedManifestMIMETypes: supportS1S2,
280+
},
281+
manifestConversionPlan{},
282+
},
283+
{
284+
"s2→s2",
285+
determineManifestConversionInputs{
286+
srcMIMEType: manifest.DockerV2Schema2MediaType,
287+
destSupportedManifestMIMETypes: supportS1S2,
288+
},
289+
manifestConversionPlan{},
290+
},
291+
// Whatever the input is, with cannotModifyManifestReason we return "keep the original as is".
292+
// Still, encryption is necessarily going to fail…
293+
{
294+
"OCI→OCI cannotModifyManifestReason",
295+
determineManifestConversionInputs{
296+
srcMIMEType: v1.MediaTypeImageManifest,
297+
destSupportedManifestMIMETypes: supportS1S2OCI,
298+
cannotModifyManifestReason: "Preserving digests",
299+
},
300+
manifestConversionPlan{
301+
preferredMIMEType: v1.MediaTypeImageManifest,
302+
preferredMIMETypeNeedsConversion: false,
303+
otherMIMETypeCandidates: []string{},
304+
},
305+
},
306+
{
307+
"s2→OCI cannotModifyManifestReason",
308+
determineManifestConversionInputs{
309+
srcMIMEType: manifest.DockerV2Schema2MediaType,
310+
destSupportedManifestMIMETypes: supportS1S2OCI,
311+
cannotModifyManifestReason: "Preserving digests",
312+
},
313+
manifestConversionPlan{
314+
preferredMIMEType: manifest.DockerV2Schema2MediaType,
315+
preferredMIMETypeNeedsConversion: false,
316+
otherMIMETypeCandidates: []string{},
317+
},
318+
},
319+
// forceManifestMIMEType to a type that supports encryption
320+
{
321+
"OCI→OCI forced",
322+
determineManifestConversionInputs{
323+
srcMIMEType: v1.MediaTypeImageManifest,
324+
destSupportedManifestMIMETypes: supportS1S2OCI,
325+
forceManifestMIMEType: v1.MediaTypeImageManifest,
326+
},
327+
manifestConversionPlan{
328+
preferredMIMEType: v1.MediaTypeImageManifest,
329+
preferredMIMETypeNeedsConversion: false,
330+
otherMIMETypeCandidates: []string{},
331+
},
332+
},
333+
{
334+
"s2→OCI forced",
335+
determineManifestConversionInputs{
336+
srcMIMEType: manifest.DockerV2Schema2MediaType,
337+
destSupportedManifestMIMETypes: supportS1S2OCI,
338+
forceManifestMIMEType: v1.MediaTypeImageManifest,
339+
},
340+
manifestConversionPlan{
341+
preferredMIMEType: v1.MediaTypeImageManifest,
342+
preferredMIMETypeNeedsConversion: true,
343+
otherMIMETypeCandidates: []string{},
344+
},
345+
},
346+
// forceManifestMIMEType to a type that does not support encryption
347+
{
348+
"OCI→s2 forced",
349+
determineManifestConversionInputs{
350+
srcMIMEType: v1.MediaTypeImageManifest,
351+
destSupportedManifestMIMETypes: supportS1S2OCI,
352+
forceManifestMIMEType: manifest.DockerV2Schema2MediaType,
353+
},
354+
manifestConversionPlan{},
355+
},
356+
{
357+
"s2→s2 forced",
358+
determineManifestConversionInputs{
359+
srcMIMEType: manifest.DockerV2Schema2MediaType,
360+
destSupportedManifestMIMETypes: supportS1S2OCI,
361+
forceManifestMIMEType: manifest.DockerV2Schema2MediaType,
362+
},
363+
manifestConversionPlan{},
364+
},
365+
} {
366+
in := c.in
367+
in.requiresOCIEncryption = true
368+
res, err := determineManifestConversion(in)
369+
if c.expected.preferredMIMEType != "" {
370+
require.NoError(t, err, c.description)
371+
assert.Equal(t, c.expected, res, c.description)
372+
} else {
373+
assert.Error(t, err, c.description)
374+
}
375+
}
218376
}
219377

220378
// fakeUnparsedImage is an implementation of types.UnparsedImage which only returns itself as a MIME type in Manifest,

0 commit comments

Comments
 (0)