...

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

Documentation: github.com/sigstore/cosign/v2/pkg/oci/internal/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  		env            map[string]string
    56  		wantPayloadErr error
    57  		wantSig        string
    58  		wantSigErr     error
    59  		wantCert       bool
    60  		wantCertErr    error
    61  		wantChain      int
    62  		wantChainErr   error
    63  		wantBundle     *bundle.RekorBundle
    64  		wantBundleErr  error
    65  	}{{
    66  		name: "just payload and signature",
    67  		l: &sigLayer{
    68  			Layer: layer,
    69  			desc: v1.Descriptor{
    70  				Digest: digest,
    71  				Annotations: map[string]string{
    72  					sigkey: "blah",
    73  				},
    74  			},
    75  		},
    76  		wantSig: "blah",
    77  	}, {
    78  		name: "with empty other keys",
    79  		l: &sigLayer{
    80  			Layer: layer,
    81  			desc: v1.Descriptor{
    82  				Digest: digest,
    83  				Annotations: map[string]string{
    84  					sigkey:    "blah",
    85  					certkey:   "",
    86  					chainkey:  "",
    87  					BundleKey: "",
    88  				},
    89  			},
    90  		},
    91  		wantSig: "blah",
    92  	}, {
    93  		name: "missing signature",
    94  		l: &sigLayer{
    95  			Layer: layer,
    96  			desc: v1.Descriptor{
    97  				Digest: digest,
    98  			},
    99  		},
   100  		wantSigErr: fmt.Errorf("signature layer %s is missing %q annotation", digest, sigkey),
   101  	}, {
   102  		name: "min plus bad bundle",
   103  		l: &sigLayer{
   104  			Layer: layer,
   105  			desc: v1.Descriptor{
   106  				Digest: digest,
   107  				Annotations: map[string]string{
   108  					sigkey:    "blah",
   109  					BundleKey: `}`,
   110  				},
   111  			},
   112  		},
   113  		wantSig:       "blah",
   114  		wantBundleErr: errors.New(`unmarshaling bundle: invalid character '}' looking for beginning of value`),
   115  	}, {
   116  		name: "min plus bad cert",
   117  		l: &sigLayer{
   118  			Layer: layer,
   119  			desc: v1.Descriptor{
   120  				Digest: digest,
   121  				Annotations: map[string]string{
   122  					sigkey:  "blah",
   123  					certkey: `GARBAGE`,
   124  				},
   125  			},
   126  		},
   127  		wantSig:     "blah",
   128  		wantCertErr: errors.New(`error during PEM decoding`),
   129  	}, {
   130  		name: "min plus bad chain",
   131  		l: &sigLayer{
   132  			Layer: layer,
   133  			desc: v1.Descriptor{
   134  				Digest: digest,
   135  				Annotations: map[string]string{
   136  					sigkey:   "blah",
   137  					chainkey: `GARBAGE`,
   138  				},
   139  			},
   140  		},
   141  		wantSig:      "blah",
   142  		wantChainErr: errors.New(`error during PEM decoding`),
   143  	}, {
   144  		name: "min plus bundle",
   145  		l: &sigLayer{
   146  			Layer: layer,
   147  			desc: v1.Descriptor{
   148  				Digest: digest,
   149  				Annotations: map[string]string{
   150  					sigkey: "blah",
   151  					// This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16.
   152  					// The Body has been removed for brevity
   153  					BundleKey: `{"SignedEntryTimestamp":"MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE=","Payload":{"body":"REMOVED","integratedTime":1631646761,"logIndex":693591,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}`,
   154  				},
   155  			},
   156  		},
   157  		wantSig: "blah",
   158  		wantBundle: &bundle.RekorBundle{
   159  			SignedEntryTimestamp: mustDecode("MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE="),
   160  			Payload: bundle.RekorPayload{
   161  				Body:           "REMOVED",
   162  				IntegratedTime: 1631646761,
   163  				LogIndex:       693591,
   164  				LogID:          "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
   165  			},
   166  		},
   167  	}, {
   168  		name: "min plus good cert",
   169  		l: &sigLayer{
   170  			Layer: layer,
   171  			desc: v1.Descriptor{
   172  				Digest: digest,
   173  				Annotations: map[string]string{
   174  					sigkey: "blah",
   175  					// This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16
   176  					certkey: `
   177  -----BEGIN CERTIFICATE-----
   178  MIICjzCCAhSgAwIBAgITV2heiswW9YldtVEAu98QxDO8TTAKBggqhkjOPQQDAzAq
   179  MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx
   180  MDkxNDE5MTI0MFoXDTIxMDkxNDE5MzIzOVowADBZMBMGByqGSM49AgEGCCqGSM49
   181  AwEHA0IABMF1AWZcfvubslc4ABNnvGbRjm6GWVHxrJ1RRthTHMCE4FpFmiHQBfGt
   182  6n80DqszGj77Whb35O33+Dal4Y2po+CjggFBMIIBPTAOBgNVHQ8BAf8EBAMCB4Aw
   183  EwYDVR0lBAwwCgYIKwYBBQUHAwMwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU340G
   184  3G1ozVNmFC5TBFV0yNuouvowHwYDVR0jBBgwFoAUyMUdAEGaJCkyUSTrDa5K7UoG
   185  0+wwgY0GCCsGAQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRl
   186  Y2EtY29udGVudC02MDNmZTdlNy0wMDAwLTIyMjctYmY3NS1mNGY1ZTgwZDI5NTQu
   187  c3RvcmFnZS5nb29nbGVhcGlzLmNvbS9jYTM2YTFlOTYyNDJiOWZjYjE0Ni9jYS5j
   188  cnQwOAYDVR0RAQH/BC4wLIEqa2V5bGVzc0BkaXN0cm9sZXNzLmlhbS5nc2Vydmlj
   189  ZWFjY291bnQuY29tMAoGCCqGSM49BAMDA2kAMGYCMQDcH9cdkxW6ugsbPHqX9qrM
   190  wlMaprcwnlktS3+5xuABr5icuqwrB/Fj5doFtS7AnM0CMQD9MjSaUmHFFF7zoLMx
   191  uThR1Z6JuA21HwxtL3GyJ8UQZcEPOlTBV593HrSAwBhiCoY=
   192  -----END CERTIFICATE-----
   193  `,
   194  				},
   195  			},
   196  		},
   197  		wantSig:  "blah",
   198  		wantCert: true,
   199  	}, {
   200  		name: "min plus bad chain",
   201  		l: &sigLayer{
   202  			Layer: layer,
   203  			desc: v1.Descriptor{
   204  				Digest: digest,
   205  				Annotations: map[string]string{
   206  					sigkey: "blah",
   207  					// This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16
   208  					chainkey: `
   209  -----BEGIN CERTIFICATE-----
   210  MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAq
   211  MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx
   212  MDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUu
   213  ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSy
   214  A7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0Jcas
   215  taRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6Nm
   216  MGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE
   217  FMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2u
   218  Su1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJx
   219  Ve/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uup
   220  Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==
   221  -----END CERTIFICATE-----
   222  `,
   223  				},
   224  			},
   225  		},
   226  		wantSig:   "blah",
   227  		wantChain: 1,
   228  	}, {
   229  		name: "payload size exceeds default limit",
   230  		l: &sigLayer{
   231  			Layer: &mockLayer{size: 134217728 + 42}, // 128MB + 42 bytes
   232  		},
   233  		wantPayloadErr: errors.New("size of layer (134217770) exceeded the limit (134217728)"),
   234  	}, {
   235  		name: "payload size exceeds overridden limit",
   236  		l: &sigLayer{
   237  			Layer: &mockLayer{size: 1000000000 + 42}, // 1GB + 42 bytes
   238  		},
   239  		env:            map[string]string{"COSIGN_MAX_ATTACHMENT_SIZE": "1GB"},
   240  		wantPayloadErr: errors.New("size of layer (1000000042) exceeded the limit (1000000000)"),
   241  	}, {
   242  		name: "payload size is within overridden limit",
   243  		l: &sigLayer{
   244  			Layer: layer,
   245  			desc: v1.Descriptor{
   246  				Digest: digest,
   247  				Annotations: map[string]string{
   248  					sigkey: "blah",
   249  				},
   250  			},
   251  		},
   252  		env:     map[string]string{"COSIGN_MAX_ATTACHMENT_SIZE": "5KB"},
   253  		wantSig: "blah",
   254  	}}
   255  
   256  	for _, test := range tests {
   257  		t.Run(test.name, func(t *testing.T) {
   258  			for k, v := range test.env {
   259  				t.Setenv(k, v)
   260  			}
   261  			b, err := test.l.Payload()
   262  			switch {
   263  			case (err != nil) != (test.wantPayloadErr != nil):
   264  				t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr)
   265  			case (err != nil) && (test.wantPayloadErr != nil) && err.Error() != test.wantPayloadErr.Error():
   266  				t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr)
   267  			case err == nil:
   268  				if got, _, err := v1.SHA256(bytes.NewBuffer(b)); err != nil {
   269  					t.Errorf("v1.SHA256() = %v", err)
   270  				} else if want := digest; want != got {
   271  					t.Errorf("v1.SHA256() = %v, wanted %v", got, want)
   272  				}
   273  			}
   274  			if err != nil {
   275  				return
   276  			}
   277  
   278  			switch got, err := test.l.Base64Signature(); {
   279  			case (err != nil) != (test.wantSigErr != nil):
   280  				t.Errorf("Base64Signature() = %v, wanted %v", err, test.wantSigErr)
   281  			case (err != nil) && (test.wantSigErr != nil) && err.Error() != test.wantSigErr.Error():
   282  				t.Errorf("Base64Signature() = %v, wanted %v", err, test.wantSigErr)
   283  			case got != test.wantSig:
   284  				t.Errorf("Base64Signature() = %v, wanted %v", got, test.wantSig)
   285  			}
   286  
   287  			switch got, err := test.l.Cert(); {
   288  			case (err != nil) != (test.wantCertErr != nil):
   289  				t.Errorf("Cert() = %v, wanted %v", err, test.wantCertErr)
   290  			case (err != nil) && (test.wantCertErr != nil) && err.Error() != test.wantCertErr.Error():
   291  				t.Errorf("Cert() = %v, wanted %v", err, test.wantCertErr)
   292  			case (got != nil) != test.wantCert:
   293  				t.Errorf("Cert() = %v, wanted cert? %v", got, test.wantCert)
   294  			}
   295  
   296  			switch got, err := test.l.Chain(); {
   297  			case (err != nil) != (test.wantChainErr != nil):
   298  				t.Errorf("Chain() = %v, wanted %v", err, test.wantChainErr)
   299  			case (err != nil) && (test.wantChainErr != nil) && err.Error() != test.wantChainErr.Error():
   300  				t.Errorf("Chain() = %v, wanted %v", err, test.wantChainErr)
   301  			case len(got) != test.wantChain:
   302  				t.Errorf("Chain() = %v, wanted chain of length %d", got, test.wantChain)
   303  			}
   304  
   305  			switch got, err := test.l.Bundle(); {
   306  			case (err != nil) != (test.wantBundleErr != nil):
   307  				t.Errorf("Bundle() = %v, wanted %v", err, test.wantBundleErr)
   308  			case (err != nil) && (test.wantBundleErr != nil) && err.Error() != test.wantBundleErr.Error():
   309  				t.Errorf("Bundle() = %v, wanted %v", err, test.wantBundleErr)
   310  			case !cmp.Equal(got, test.wantBundle):
   311  				t.Errorf("Bundle() %s", cmp.Diff(got, test.wantBundle))
   312  			}
   313  		})
   314  	}
   315  }
   316  
   317  func TestSignatureWithTSAAnnotation(t *testing.T) {
   318  	layer, err := random.Layer(300 /* byteSize */, types.DockerLayer)
   319  	if err != nil {
   320  		t.Fatalf("random.Layer() = %v", err)
   321  	}
   322  	digest, err := layer.Digest()
   323  	if err != nil {
   324  		t.Fatalf("Digest() = %v", err)
   325  	}
   326  
   327  	tests := []struct {
   328  		name           string
   329  		l              *sigLayer
   330  		wantPayloadErr error
   331  		wantSig        string
   332  		wantSigErr     error
   333  		wantCert       bool
   334  		wantCertErr    error
   335  		wantChain      int
   336  		wantChainErr   error
   337  		wantBundle     *bundle.RFC3161Timestamp
   338  		wantBundleErr  error
   339  	}{{
   340  		name: "just payload and signature",
   341  		l: &sigLayer{
   342  			Layer: layer,
   343  			desc: v1.Descriptor{
   344  				Digest: digest,
   345  				Annotations: map[string]string{
   346  					sigkey: "blah",
   347  				},
   348  			},
   349  		},
   350  		wantSig: "blah",
   351  	}, {
   352  		name: "with empty other keys",
   353  		l: &sigLayer{
   354  			Layer: layer,
   355  			desc: v1.Descriptor{
   356  				Digest: digest,
   357  				Annotations: map[string]string{
   358  					sigkey:              "blah",
   359  					certkey:             "",
   360  					chainkey:            "",
   361  					RFC3161TimestampKey: "",
   362  				},
   363  			},
   364  		},
   365  		wantSig: "blah",
   366  	}, {
   367  		name: "missing signature",
   368  		l: &sigLayer{
   369  			Layer: layer,
   370  			desc: v1.Descriptor{
   371  				Digest: digest,
   372  			},
   373  		},
   374  		wantSigErr: fmt.Errorf("signature layer %s is missing %q annotation", digest, sigkey),
   375  	}, {
   376  		name: "min plus bad bundle",
   377  		l: &sigLayer{
   378  			Layer: layer,
   379  			desc: v1.Descriptor{
   380  				Digest: digest,
   381  				Annotations: map[string]string{
   382  					sigkey:              "blah",
   383  					RFC3161TimestampKey: `}`,
   384  				},
   385  			},
   386  		},
   387  		wantSig:       "blah",
   388  		wantBundleErr: errors.New(`unmarshaling RFC3161 timestamp bundle: invalid character '}' looking for beginning of value`),
   389  	}, {
   390  		name: "min plus bad cert",
   391  		l: &sigLayer{
   392  			Layer: layer,
   393  			desc: v1.Descriptor{
   394  				Digest: digest,
   395  				Annotations: map[string]string{
   396  					sigkey:  "blah",
   397  					certkey: `GARBAGE`,
   398  				},
   399  			},
   400  		},
   401  		wantSig:     "blah",
   402  		wantCertErr: errors.New(`error during PEM decoding`),
   403  	}, {
   404  		name: "min plus bad chain",
   405  		l: &sigLayer{
   406  			Layer: layer,
   407  			desc: v1.Descriptor{
   408  				Digest: digest,
   409  				Annotations: map[string]string{
   410  					sigkey:   "blah",
   411  					chainkey: `GARBAGE`,
   412  				},
   413  			},
   414  		},
   415  		wantSig:      "blah",
   416  		wantChainErr: errors.New(`error during PEM decoding`),
   417  	}, {
   418  		name: "min plus RFC3161 timestamp bundle",
   419  		l: &sigLayer{
   420  			Layer: layer,
   421  			desc: v1.Descriptor{
   422  				Digest: digest,
   423  				Annotations: map[string]string{
   424  					sigkey: "TSA blah",
   425  					// This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16.
   426  					// The Body has been removed for brevity
   427  					RFC3161TimestampKey: `{"SignedRFC3161Timestamp":"MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE="}`,
   428  				},
   429  			},
   430  		},
   431  		wantSig: "TSA blah",
   432  		wantBundle: &bundle.RFC3161Timestamp{
   433  			SignedRFC3161Timestamp: mustDecode("MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE="),
   434  		},
   435  	}}
   436  
   437  	for _, test := range tests {
   438  		t.Run(test.name, func(t *testing.T) {
   439  			b, err := test.l.Payload()
   440  			switch {
   441  			case (err != nil) != (test.wantPayloadErr != nil):
   442  				t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr)
   443  			case (err != nil) && (test.wantPayloadErr != nil) && err.Error() != test.wantPayloadErr.Error():
   444  				t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr)
   445  			case err == nil:
   446  				if got, _, err := v1.SHA256(bytes.NewBuffer(b)); err != nil {
   447  					t.Errorf("v1.SHA256() = %v", err)
   448  				} else if want := digest; want != got {
   449  					t.Errorf("v1.SHA256() = %v, wanted %v", got, want)
   450  				}
   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