...

Source file src/github.com/sigstore/cosign/v2/pkg/oci/signature/layer_test.go

Documentation: github.com/sigstore/cosign/v2/pkg/oci/signature

     1  //
     2  // Copyright 2021 The Sigstore Authors.
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  //     http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  // Unless required by applicable law or agreed to in writing, software
    11  // distributed under the License is distributed on an "AS IS" BASIS,
    12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  
    16  package signature
    17  
    18  import (
    19  	"bytes"
    20  	"encoding/base64"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  	v1 "github.com/google/go-containerregistry/pkg/v1"
    29  	"github.com/google/go-containerregistry/pkg/v1/random"
    30  	"github.com/google/go-containerregistry/pkg/v1/types"
    31  	"github.com/sigstore/cosign/v2/pkg/cosign/bundle"
    32  )
    33  
    34  func mustDecode(s string) []byte {
    35  	b, err := base64.StdEncoding.DecodeString(s)
    36  	if err != nil {
    37  		panic(err.Error())
    38  	}
    39  	return b
    40  }
    41  
    42  func TestSignature(t *testing.T) {
    43  	layer, err := random.Layer(300 /* byteSize */, types.DockerLayer)
    44  	if err != nil {
    45  		t.Fatalf("random.Layer() = %v", err)
    46  	}
    47  	digest, err := layer.Digest()
    48  	if err != nil {
    49  		t.Fatalf("Digest() = %v", err)
    50  	}
    51  
    52  	tests := []struct {
    53  		name           string
    54  		l              *sigLayer
    55  		wantPayloadErr error
    56  		wantSig        string
    57  		wantSigErr     error
    58  		wantCert       bool
    59  		wantCertErr    error
    60  		wantChain      int
    61  		wantChainErr   error
    62  		wantBundle     *bundle.RekorBundle
    63  		wantBundleErr  error
    64  	}{{
    65  		name: "just payload and signature",
    66  		l: &sigLayer{
    67  			Layer: layer,
    68  			desc: v1.Descriptor{
    69  				Digest: digest,
    70  				Annotations: map[string]string{
    71  					sigkey: "blah",
    72  				},
    73  			},
    74  		},
    75  		wantSig: "blah",
    76  	}, {
    77  		name: "with empty other keys",
    78  		l: &sigLayer{
    79  			Layer: layer,
    80  			desc: v1.Descriptor{
    81  				Digest: digest,
    82  				Annotations: map[string]string{
    83  					sigkey:    "blah",
    84  					certkey:   "",
    85  					chainkey:  "",
    86  					BundleKey: "",
    87  				},
    88  			},
    89  		},
    90  		wantSig: "blah",
    91  	}, {
    92  		name: "missing signature",
    93  		l: &sigLayer{
    94  			Layer: layer,
    95  			desc: v1.Descriptor{
    96  				Digest: digest,
    97  			},
    98  		},
    99  		wantSigErr: fmt.Errorf("signature layer %s is missing %q annotation", digest, sigkey),
   100  	}, {
   101  		name: "min plus bad bundle",
   102  		l: &sigLayer{
   103  			Layer: layer,
   104  			desc: v1.Descriptor{
   105  				Digest: digest,
   106  				Annotations: map[string]string{
   107  					sigkey:    "blah",
   108  					BundleKey: `}`,
   109  				},
   110  			},
   111  		},
   112  		wantSig:       "blah",
   113  		wantBundleErr: errors.New(`unmarshaling bundle: invalid character '}' looking for beginning of value`),
   114  	}, {
   115  		name: "min plus bad cert",
   116  		l: &sigLayer{
   117  			Layer: layer,
   118  			desc: v1.Descriptor{
   119  				Digest: digest,
   120  				Annotations: map[string]string{
   121  					sigkey:  "blah",
   122  					certkey: `GARBAGE`,
   123  				},
   124  			},
   125  		},
   126  		wantSig:     "blah",
   127  		wantCertErr: errors.New(`error during PEM decoding`),
   128  	}, {
   129  		name: "min plus bad chain",
   130  		l: &sigLayer{
   131  			Layer: layer,
   132  			desc: v1.Descriptor{
   133  				Digest: digest,
   134  				Annotations: map[string]string{
   135  					sigkey:   "blah",
   136  					chainkey: `GARBAGE`,
   137  				},
   138  			},
   139  		},
   140  		wantSig:      "blah",
   141  		wantChainErr: errors.New(`error during PEM decoding`),
   142  	}, {
   143  		name: "min plus bundle",
   144  		l: &sigLayer{
   145  			Layer: layer,
   146  			desc: v1.Descriptor{
   147  				Digest: digest,
   148  				Annotations: map[string]string{
   149  					sigkey: "blah",
   150  					// This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16.
   151  					// The Body has been removed for brevity
   152  					BundleKey: `{"SignedEntryTimestamp":"MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE=","Payload":{"body":"REMOVED","integratedTime":1631646761,"logIndex":693591,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}`,
   153  				},
   154  			},
   155  		},
   156  		wantSig: "blah",
   157  		wantBundle: &bundle.RekorBundle{
   158  			SignedEntryTimestamp: mustDecode("MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE="),
   159  			Payload: bundle.RekorPayload{
   160  				Body:           "REMOVED",
   161  				IntegratedTime: 1631646761,
   162  				LogIndex:       693591,
   163  				LogID:          "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
   164  			},
   165  		},
   166  	}, {
   167  		name: "min plus good cert",
   168  		l: &sigLayer{
   169  			Layer: layer,
   170  			desc: v1.Descriptor{
   171  				Digest: digest,
   172  				Annotations: map[string]string{
   173  					sigkey: "blah",
   174  					// This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16
   175  					certkey: `
   176  -----BEGIN CERTIFICATE-----
   177  MIICjzCCAhSgAwIBAgITV2heiswW9YldtVEAu98QxDO8TTAKBggqhkjOPQQDAzAq
   178  MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx
   179  MDkxNDE5MTI0MFoXDTIxMDkxNDE5MzIzOVowADBZMBMGByqGSM49AgEGCCqGSM49
   180  AwEHA0IABMF1AWZcfvubslc4ABNnvGbRjm6GWVHxrJ1RRthTHMCE4FpFmiHQBfGt
   181  6n80DqszGj77Whb35O33+Dal4Y2po+CjggFBMIIBPTAOBgNVHQ8BAf8EBAMCB4Aw
   182  EwYDVR0lBAwwCgYIKwYBBQUHAwMwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU340G
   183  3G1ozVNmFC5TBFV0yNuouvowHwYDVR0jBBgwFoAUyMUdAEGaJCkyUSTrDa5K7UoG
   184  0+wwgY0GCCsGAQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRl
   185  Y2EtY29udGVudC02MDNmZTdlNy0wMDAwLTIyMjctYmY3NS1mNGY1ZTgwZDI5NTQu
   186  c3RvcmFnZS5nb29nbGVhcGlzLmNvbS9jYTM2YTFlOTYyNDJiOWZjYjE0Ni9jYS5j
   187  cnQwOAYDVR0RAQH/BC4wLIEqa2V5bGVzc0BkaXN0cm9sZXNzLmlhbS5nc2Vydmlj
   188  ZWFjY291bnQuY29tMAoGCCqGSM49BAMDA2kAMGYCMQDcH9cdkxW6ugsbPHqX9qrM
   189  wlMaprcwnlktS3+5xuABr5icuqwrB/Fj5doFtS7AnM0CMQD9MjSaUmHFFF7zoLMx
   190  uThR1Z6JuA21HwxtL3GyJ8UQZcEPOlTBV593HrSAwBhiCoY=
   191  -----END CERTIFICATE-----
   192  `,
   193  				},
   194  			},
   195  		},
   196  		wantSig:  "blah",
   197  		wantCert: true,
   198  	}, {
   199  		name: "min plus bad chain",
   200  		l: &sigLayer{
   201  			Layer: layer,
   202  			desc: v1.Descriptor{
   203  				Digest: digest,
   204  				Annotations: map[string]string{
   205  					sigkey: "blah",
   206  					// This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16
   207  					chainkey: `
   208  -----BEGIN CERTIFICATE-----
   209  MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAq
   210  MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx
   211  MDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUu
   212  ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSy
   213  A7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0Jcas
   214  taRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6Nm
   215  MGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE
   216  FMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2u
   217  Su1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJx
   218  Ve/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uup
   219  Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==
   220  -----END CERTIFICATE-----
   221  `,
   222  				},
   223  			},
   224  		},
   225  		wantSig:   "blah",
   226  		wantChain: 1,
   227  	}}
   228  
   229  	for _, test := range tests {
   230  		t.Run(test.name, func(t *testing.T) {
   231  			b, err := test.l.Payload()
   232  			switch {
   233  			case (err != nil) != (test.wantPayloadErr != nil):
   234  				t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr)
   235  			case (err != nil) && (test.wantPayloadErr != nil) && err.Error() != test.wantPayloadErr.Error():
   236  				t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr)
   237  			case err == nil:
   238  				if got, _, err := v1.SHA256(bytes.NewBuffer(b)); err != nil {
   239  					t.Errorf("v1.SHA256() = %v", err)
   240  				} else if want := digest; want != got {
   241  					t.Errorf("v1.SHA256() = %v, wanted %v", got, want)
   242  				}
   243  			}
   244  
   245  			switch got, err := test.l.Base64Signature(); {
   246  			case (err != nil) != (test.wantSigErr != nil):
   247  				t.Errorf("Base64Signature() = %v, wanted %v", err, test.wantSigErr)
   248  			case (err != nil) && (test.wantSigErr != nil) && err.Error() != test.wantSigErr.Error():
   249  				t.Errorf("Base64Signature() = %v, wanted %v", err, test.wantSigErr)
   250  			case got != test.wantSig:
   251  				t.Errorf("Base64Signature() = %v, wanted %v", got, test.wantSig)
   252  			}
   253  
   254  			switch got, err := test.l.Cert(); {
   255  			case (err != nil) != (test.wantCertErr != nil):
   256  				t.Errorf("Cert() = %v, wanted %v", err, test.wantCertErr)
   257  			case (err != nil) && (test.wantCertErr != nil) && err.Error() != test.wantCertErr.Error():
   258  				t.Errorf("Cert() = %v, wanted %v", err, test.wantCertErr)
   259  			case (got != nil) != test.wantCert:
   260  				t.Errorf("Cert() = %v, wanted cert? %v", got, test.wantCert)
   261  			}
   262  
   263  			switch got, err := test.l.Chain(); {
   264  			case (err != nil) != (test.wantChainErr != nil):
   265  				t.Errorf("Chain() = %v, wanted %v", err, test.wantChainErr)
   266  			case (err != nil) && (test.wantChainErr != nil) && err.Error() != test.wantChainErr.Error():
   267  				t.Errorf("Chain() = %v, wanted %v", err, test.wantChainErr)
   268  			case len(got) != test.wantChain:
   269  				t.Errorf("Chain() = %v, wanted chain of length %d", got, test.wantChain)
   270  			}
   271  
   272  			switch got, err := test.l.Bundle(); {
   273  			case (err != nil) != (test.wantBundleErr != nil):
   274  				t.Errorf("Bundle() = %v, wanted %v", err, test.wantBundleErr)
   275  			case (err != nil) && (test.wantBundleErr != nil) && err.Error() != test.wantBundleErr.Error():
   276  				t.Errorf("Bundle() = %v, wanted %v", err, test.wantBundleErr)
   277  			case !cmp.Equal(got, test.wantBundle):
   278  				t.Errorf("Bundle() %s", cmp.Diff(got, test.wantBundle))
   279  			}
   280  		})
   281  	}
   282  }
   283  
   284  func TestSignatureWithTSAAnnotation(t *testing.T) {
   285  	layer, err := random.Layer(300 /* byteSize */, types.DockerLayer)
   286  	if err != nil {
   287  		t.Fatalf("random.Layer() = %v", err)
   288  	}
   289  	digest, err := layer.Digest()
   290  	if err != nil {
   291  		t.Fatalf("Digest() = %v", err)
   292  	}
   293  
   294  	tests := []struct {
   295  		name           string
   296  		l              *sigLayer
   297  		env            map[string]string
   298  		wantPayloadErr error
   299  		wantSig        string
   300  		wantSigErr     error
   301  		wantCert       bool
   302  		wantCertErr    error
   303  		wantChain      int
   304  		wantChainErr   error
   305  		wantBundle     *bundle.RFC3161Timestamp
   306  		wantBundleErr  error
   307  	}{{
   308  		name: "just payload and signature",
   309  		l: &sigLayer{
   310  			Layer: layer,
   311  			desc: v1.Descriptor{
   312  				Digest: digest,
   313  				Annotations: map[string]string{
   314  					sigkey: "blah",
   315  				},
   316  			},
   317  		},
   318  		wantSig: "blah",
   319  	}, {
   320  		name: "with empty other keys",
   321  		l: &sigLayer{
   322  			Layer: layer,
   323  			desc: v1.Descriptor{
   324  				Digest: digest,
   325  				Annotations: map[string]string{
   326  					sigkey:              "blah",
   327  					certkey:             "",
   328  					chainkey:            "",
   329  					RFC3161TimestampKey: "",
   330  				},
   331  			},
   332  		},
   333  		wantSig: "blah",
   334  	}, {
   335  		name: "missing signature",
   336  		l: &sigLayer{
   337  			Layer: layer,
   338  			desc: v1.Descriptor{
   339  				Digest: digest,
   340  			},
   341  		},
   342  		wantSigErr: fmt.Errorf("signature layer %s is missing %q annotation", digest, sigkey),
   343  	}, {
   344  		name: "min plus bad RFC3161 timestamp bundle",
   345  		l: &sigLayer{
   346  			Layer: layer,
   347  			desc: v1.Descriptor{
   348  				Digest: digest,
   349  				Annotations: map[string]string{
   350  					sigkey:              "blah",
   351  					RFC3161TimestampKey: `}`,
   352  				},
   353  			},
   354  		},
   355  		wantSig:       "blah",
   356  		wantBundleErr: errors.New(`unmarshaling RFC3161 timestamp bundle: invalid character '}' looking for beginning of value`),
   357  	}, {
   358  		name: "min plus bad cert",
   359  		l: &sigLayer{
   360  			Layer: layer,
   361  			desc: v1.Descriptor{
   362  				Digest: digest,
   363  				Annotations: map[string]string{
   364  					sigkey:  "blah",
   365  					certkey: `GARBAGE`,
   366  				},
   367  			},
   368  		},
   369  		wantSig:     "blah",
   370  		wantCertErr: errors.New(`error during PEM decoding`),
   371  	}, {
   372  		name: "min plus bad chain",
   373  		l: &sigLayer{
   374  			Layer: layer,
   375  			desc: v1.Descriptor{
   376  				Digest: digest,
   377  				Annotations: map[string]string{
   378  					sigkey:   "blah",
   379  					chainkey: `GARBAGE`,
   380  				},
   381  			},
   382  		},
   383  		wantSig:      "blah",
   384  		wantChainErr: errors.New(`error during PEM decoding`),
   385  	}, {
   386  		name: "min plus RFC3161 timestamp bundle",
   387  		l: &sigLayer{
   388  			Layer: layer,
   389  			desc: v1.Descriptor{
   390  				Digest: digest,
   391  				Annotations: map[string]string{
   392  					sigkey: "tsa blah",
   393  					// This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16.
   394  					// The Body has been removed for brevity
   395  					RFC3161TimestampKey: `{"SignedRFC3161Timestamp":"MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE=","Payload":{"body":"REMOVED","integratedTime":1631646761,"logIndex":693591,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}`,
   396  				},
   397  			},
   398  		},
   399  		wantSig: "tsa blah",
   400  		wantBundle: &bundle.RFC3161Timestamp{
   401  			SignedRFC3161Timestamp: mustDecode("MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE="),
   402  		},
   403  	}, {
   404  		name: "payload size exceeds default limit",
   405  		l: &sigLayer{
   406  			Layer: &mockLayer{size: 134217728 + 42}, // 128MiB + 42 bytes
   407  		},
   408  		wantPayloadErr: errors.New("size of layer (134217770) exceeded the limit (134217728)"),
   409  	}, {
   410  		name: "payload size exceeds overridden limit",
   411  		l: &sigLayer{
   412  			Layer: &mockLayer{size: 1000000000 + 42}, // 1GB + 42 bytes
   413  		},
   414  		env:            map[string]string{"COSIGN_MAX_ATTACHMENT_SIZE": "1GB"},
   415  		wantPayloadErr: errors.New("size of layer (1000000042) exceeded the limit (1000000000)"),
   416  	}, {
   417  		name: "payload size is within overridden limit",
   418  		l: &sigLayer{
   419  			Layer: layer,
   420  			desc: v1.Descriptor{
   421  				Digest: digest,
   422  				Annotations: map[string]string{
   423  					sigkey: "blah",
   424  				},
   425  			},
   426  		},
   427  		env:     map[string]string{"COSIGN_MAX_ATTACHMENT_SIZE": "5KB"},
   428  		wantSig: "blah",
   429  	}}
   430  
   431  	for _, test := range tests {
   432  		t.Run(test.name, func(t *testing.T) {
   433  			for k, v := range test.env {
   434  				t.Setenv(k, v)
   435  			}
   436  			b, err := test.l.Payload()
   437  			switch {
   438  			case (err != nil) != (test.wantPayloadErr != nil):
   439  				t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr)
   440  			case (err != nil) && (test.wantPayloadErr != nil) && err.Error() != test.wantPayloadErr.Error():
   441  				t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr)
   442  			case err == nil:
   443  				if got, _, err := v1.SHA256(bytes.NewBuffer(b)); err != nil {
   444  					t.Errorf("v1.SHA256() = %v", err)
   445  				} else if want := digest; want != got {
   446  					t.Errorf("v1.SHA256() = %v, wanted %v", got, want)
   447  				}
   448  			}
   449  			if err != nil {
   450  				return
   451  			}
   452  
   453  			switch got, err := test.l.Base64Signature(); {
   454  			case (err != nil) != (test.wantSigErr != nil):
   455  				t.Errorf("Base64Signature() = %v, wanted %v", err, test.wantSigErr)
   456  			case (err != nil) && (test.wantSigErr != nil) && err.Error() != test.wantSigErr.Error():
   457  				t.Errorf("Base64Signature() = %v, wanted %v", err, test.wantSigErr)
   458  			case got != test.wantSig:
   459  				t.Errorf("Base64Signature() = %v, wanted %v", got, test.wantSig)
   460  			}
   461  
   462  			switch got, err := test.l.Cert(); {
   463  			case (err != nil) != (test.wantCertErr != nil):
   464  				t.Errorf("Cert() = %v, wanted %v", err, test.wantCertErr)
   465  			case (err != nil) && (test.wantCertErr != nil) && err.Error() != test.wantCertErr.Error():
   466  				t.Errorf("Cert() = %v, wanted %v", err, test.wantCertErr)
   467  			case (got != nil) != test.wantCert:
   468  				t.Errorf("Cert() = %v, wanted cert? %v", got, test.wantCert)
   469  			}
   470  
   471  			switch got, err := test.l.Chain(); {
   472  			case (err != nil) != (test.wantChainErr != nil):
   473  				t.Errorf("Chain() = %v, wanted %v", err, test.wantChainErr)
   474  			case (err != nil) && (test.wantChainErr != nil) && err.Error() != test.wantChainErr.Error():
   475  				t.Errorf("Chain() = %v, wanted %v", err, test.wantChainErr)
   476  			case len(got) != test.wantChain:
   477  				t.Errorf("Chain() = %v, wanted chain of length %d", got, test.wantChain)
   478  			}
   479  
   480  			switch got, err := test.l.RFC3161Timestamp(); {
   481  			case (err != nil) != (test.wantBundleErr != nil):
   482  				t.Errorf("RFC3161Timestamp() = %v, wanted %v", err, test.wantBundleErr)
   483  			case (err != nil) && (test.wantBundleErr != nil) && err.Error() != test.wantBundleErr.Error():
   484  				t.Errorf("RFC3161Timestamp() = %v, wanted %v", err, test.wantBundleErr)
   485  			case !cmp.Equal(got, test.wantBundle):
   486  				t.Errorf("RFC3161Timestamp() %s", cmp.Diff(got, test.wantBundle))
   487  			}
   488  		})
   489  	}
   490  }
   491  
   492  type mockLayer struct {
   493  	size int64
   494  }
   495  
   496  func (m *mockLayer) Size() (int64, error) {
   497  	return m.size, nil
   498  }
   499  
   500  func (m *mockLayer) Compressed() (io.ReadCloser, error) {
   501  	return io.NopCloser(strings.NewReader("data")), nil
   502  }
   503  
   504  func (m *mockLayer) Digest() (v1.Hash, error)             { panic("not implemented") }
   505  func (m *mockLayer) DiffID() (v1.Hash, error)             { panic("not implemented") }
   506  func (m *mockLayer) Uncompressed() (io.ReadCloser, error) { panic("not implemented") }
   507  func (m *mockLayer) MediaType() (types.MediaType, error)  { panic("not implemented") }
   508  

View as plain text