From 5fb05e15affed2cd35d77226568fae0e78b185dd Mon Sep 17 00:00:00 2001 From: Bob Callaway Date: Wed, 23 Jun 2021 15:01:16 -0400 Subject: [PATCH] Add Alpine Package type (#337) This adds support for the alpine package format used by Alpine Linux, which is the concatenation of three tgz files (signature, control data, and then the actual package files). Signed-off-by: Bob Callaway --- cmd/rekor-cli/app/pflags.go | 84 +++- cmd/rekor-cli/app/pflags_test.go | 39 ++ cmd/rekor-cli/app/upload.go | 5 + cmd/rekor-cli/app/verify.go | 20 + cmd/rekor-server/app/serve.go | 3 + go.mod | 1 + openapi.yaml | 21 +- pkg/generated/models/alpine.go | 210 ++++++++ pkg/generated/models/alpine_schema.go | 29 ++ pkg/generated/models/alpine_v001_schema.go | 450 ++++++++++++++++++ pkg/generated/models/proposed_entry.go | 6 + pkg/generated/models/rpm.go | 2 +- pkg/generated/restapi/embedded_spec.go | 274 ++++++++++- pkg/pki/x509/x509.go | 7 +- pkg/types/alpine/alpine.go | 57 +++ pkg/types/alpine/alpine_schema.json | 12 + pkg/types/alpine/alpine_test.go | 128 +++++ pkg/types/alpine/apk.go | 245 ++++++++++ pkg/types/alpine/apk_test.go | 50 ++ .../alpine/v0.0.1/alpine_v0_0_1_schema.json | 86 ++++ pkg/types/alpine/v0.0.1/entry.go | 340 +++++++++++++ pkg/types/alpine/v0.0.1/entry_test.go | 394 +++++++++++++++ pkg/types/rpm/v0.0.1/entry.go | 7 +- tests/alpine.json | 1 + tests/apk.go | 92 ++++ tests/e2e_test.go | 18 + tests/test_alpine.apk | Bin 0 -> 2874 bytes tests/test_alpine.pub | 9 + 28 files changed, 2578 insertions(+), 12 deletions(-) create mode 100644 pkg/generated/models/alpine.go create mode 100644 pkg/generated/models/alpine_schema.go create mode 100644 pkg/generated/models/alpine_v001_schema.go create mode 100644 pkg/types/alpine/alpine.go create mode 100644 pkg/types/alpine/alpine_schema.json create mode 100644 pkg/types/alpine/alpine_test.go create mode 100644 pkg/types/alpine/apk.go create mode 100644 pkg/types/alpine/apk_test.go create mode 100644 pkg/types/alpine/v0.0.1/alpine_v0_0_1_schema.json create mode 100644 pkg/types/alpine/v0.0.1/entry.go create mode 100644 pkg/types/alpine/v0.0.1/entry_test.go create mode 100644 tests/alpine.json create mode 100644 tests/apk.go create mode 100644 tests/test_alpine.apk create mode 100644 tests/test_alpine.pub diff --git a/cmd/rekor-cli/app/pflags.go b/cmd/rekor-cli/app/pflags.go index 25aeed5f9..3eb305144 100644 --- a/cmd/rekor-cli/app/pflags.go +++ b/cmd/rekor-cli/app/pflags.go @@ -36,6 +36,7 @@ import ( "github.com/go-playground/validator" "github.com/sigstore/rekor/pkg/generated/models" + alpine_v001 "github.com/sigstore/rekor/pkg/types/alpine/v0.0.1" intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" jar_v001 "github.com/sigstore/rekor/pkg/types/jar/v0.0.1" rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" @@ -335,6 +336,82 @@ func CreateRpmFromPFlags() (models.ProposedEntry, error) { return &returnVal, nil } +func CreateAlpineFromPFlags() (models.ProposedEntry, error) { + //TODO: how to select version of item to create + returnVal := models.Alpine{} + re := new(alpine_v001.V001Entry) + + apk := viper.GetString("entry") + if apk != "" { + var apkBytes []byte + apkURL, err := url.Parse(apk) + if err == nil && apkURL.IsAbs() { + /* #nosec G107 */ + apkResp, err := http.Get(apk) + if err != nil { + return nil, fmt.Errorf("error fetching 'alpine': %w", err) + } + defer apkResp.Body.Close() + apkBytes, err = ioutil.ReadAll(apkResp.Body) + if err != nil { + return nil, fmt.Errorf("error fetching 'alpine': %w", err) + } + } else { + apkBytes, err = ioutil.ReadFile(filepath.Clean(apk)) + if err != nil { + return nil, fmt.Errorf("error processing 'alpine' file: %w", err) + } + } + if err := json.Unmarshal(apkBytes, &returnVal); err != nil { + return nil, fmt.Errorf("error parsing alpine file: %w", err) + } + } else { + // we will need artifact, public-key, signature + re.AlpineModel = models.AlpineV001Schema{} + re.AlpineModel.Package = &models.AlpineV001SchemaPackage{} + + artifact := viper.GetString("artifact") + dataURL, err := url.Parse(artifact) + if err == nil && dataURL.IsAbs() { + re.AlpineModel.Package.URL = strfmt.URI(artifact) + } else { + artifactBytes, err := ioutil.ReadFile(filepath.Clean(artifact)) + if err != nil { + return nil, fmt.Errorf("error reading artifact file: %w", err) + } + re.AlpineModel.Package.Content = strfmt.Base64(artifactBytes) + } + + re.AlpineModel.PublicKey = &models.AlpineV001SchemaPublicKey{} + publicKey := viper.GetString("public-key") + keyURL, err := url.Parse(publicKey) + if err == nil && keyURL.IsAbs() { + re.AlpineModel.PublicKey.URL = strfmt.URI(publicKey) + } else { + keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKey)) + if err != nil { + return nil, fmt.Errorf("error reading public key file: %w", err) + } + re.AlpineModel.PublicKey.Content = strfmt.Base64(keyBytes) + } + + if err := re.Validate(); err != nil { + return nil, err + } + + if re.HasExternalEntities() { + if err := re.FetchExternalEntities(context.Background()); err != nil { + return nil, fmt.Errorf("error retrieving external entities: %v", err) + } + } + + returnVal.APIVersion = swag.String(re.APIVersion()) + returnVal.Spec = re.AlpineModel + } + + return &returnVal, nil +} + func CreateRekordFromPFlags() (models.ProposedEntry, error) { //TODO: how to select version of item to create returnVal := models.Rekord{} @@ -481,12 +558,17 @@ func (t *typeFlag) Set(s string) error { "jar": {}, "intoto": {}, "rfc3161": {}, + "alpine": {}, } if _, ok := set[s]; ok { t.value = s return nil } - return fmt.Errorf("value specified is invalid: [%s] supported values are: [rekord, rpm, jar, intoto, rfc3161]", s) + var types []string + for typeStr := range set { + types = append(types, typeStr) + } + return fmt.Errorf("value specified is invalid: [%s] supported values are: [%v]", s, strings.Join(types, ", ")) } type pkiFormatFlag struct { diff --git a/cmd/rekor-cli/app/pflags_test.go b/cmd/rekor-cli/app/pflags_test.go index ca314fa23..5f5f782aa 100644 --- a/cmd/rekor-cli/app/pflags_test.go +++ b/cmd/rekor-cli/app/pflags_test.go @@ -64,6 +64,12 @@ func TestArtifactPFlags(t *testing.T) { file, err = ioutil.ReadFile("../../../tests/test.rpm") case "/rpmPublicKey": file, err = ioutil.ReadFile("../../../tests/test_rpm_public_key.key") + case "/alpine": + file, err = ioutil.ReadFile("../../../tests/test_alpine.apk") + case "/alpinePublicKey": + file, err = ioutil.ReadFile("../../../tests/test_alpine.pub") + case "/alpineEntry": + file, err = ioutil.ReadFile("../../../tests/alpine.json") case "/not_found": err = errors.New("file not found") } @@ -110,6 +116,13 @@ func TestArtifactPFlags(t *testing.T) { expectParseSuccess: true, expectValidateSuccess: true, }, + { + caseDesc: "valid alpine URL", + entry: testServer.URL + "/alpineEntry", + typeStr: "alpine", + expectParseSuccess: true, + expectValidateSuccess: true, + }, { caseDesc: "valid rpm file, wrong type", typeStr: "rekord", @@ -145,6 +158,14 @@ func TestArtifactPFlags(t *testing.T) { expectParseSuccess: true, expectValidateSuccess: true, }, + { + caseDesc: "valid alpine - local artifact with required flags", + typeStr: "alpine", + artifact: "../../../tests/test_alpine.apk", + publicKey: "../../../tests/test_alpine.pub", + expectParseSuccess: true, + expectValidateSuccess: true, + }, { caseDesc: "nonexistant local artifact", artifact: "../../../tests/not_a_file", @@ -231,6 +252,14 @@ func TestArtifactPFlags(t *testing.T) { expectParseSuccess: true, expectValidateSuccess: true, }, + { + caseDesc: "valid alpine - remote artifact with required flags", + typeStr: "alpine", + artifact: testServer.URL + "/alpine", + publicKey: "../../../tests/test_alpine.pub", + expectParseSuccess: true, + expectValidateSuccess: true, + }, { caseDesc: "remote artifact with invalid URL", artifact: "hteeteep%**/test_file.txt", @@ -364,6 +393,16 @@ func TestArtifactPFlags(t *testing.T) { createFn = CreateRekordFromPFlags case "rpm": createFn = CreateRpmFromPFlags + case "jar": + createFn = CreateJarFromPFlags + case "intoto": + createFn = CreateIntotoFromPFlags + case "rfc3161": + createFn = CreateRFC3161FromPFlags + case "alpine": + createFn = CreateAlpineFromPFlags + default: + t.Fatalf("type %v not implemented", tc.typeStr) } if _, err := createFn(); err != nil { t.Errorf("unexpected result in '%v' building entry: %v", tc.caseDesc, err) diff --git a/cmd/rekor-cli/app/upload.go b/cmd/rekor-cli/app/upload.go index f7e84159d..4a1bc9439 100644 --- a/cmd/rekor-cli/app/upload.go +++ b/cmd/rekor-cli/app/upload.go @@ -100,6 +100,11 @@ var uploadCmd = &cobra.Command{ if err != nil { return nil, err } + case "alpine": + entry, err = CreateAlpineFromPFlags() + if err != nil { + return nil, err + } default: return nil, errors.New("unknown type specified") } diff --git a/cmd/rekor-cli/app/verify.go b/cmd/rekor-cli/app/verify.go index dae3d3fff..1c7728c20 100644 --- a/cmd/rekor-cli/app/verify.go +++ b/cmd/rekor-cli/app/verify.go @@ -116,6 +116,26 @@ var verifyCmd = &cobra.Command{ if err != nil { return nil, err } + case "jar": + entry, err = CreateJarFromPFlags() + if err != nil { + return nil, err + } + case "intoto": + entry, err = CreateIntotoFromPFlags() + if err != nil { + return nil, err + } + case "rfc3161": + entry, err = CreateRFC3161FromPFlags() + if err != nil { + return nil, err + } + case "alpine": + entry, err = CreateAlpineFromPFlags() + if err != nil { + return nil, err + } default: return nil, errors.New("invalid type specified") } diff --git a/cmd/rekor-server/app/serve.go b/cmd/rekor-server/app/serve.go index 70dcad763..278b926e8 100644 --- a/cmd/rekor-server/app/serve.go +++ b/cmd/rekor-server/app/serve.go @@ -28,6 +28,8 @@ import ( "github.com/sigstore/rekor/pkg/generated/restapi" "github.com/sigstore/rekor/pkg/generated/restapi/operations" "github.com/sigstore/rekor/pkg/log" + "github.com/sigstore/rekor/pkg/types/alpine" + alpine_v001 "github.com/sigstore/rekor/pkg/types/alpine/v0.0.1" "github.com/sigstore/rekor/pkg/types/intoto" intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" "github.com/sigstore/rekor/pkg/types/jar" @@ -79,6 +81,7 @@ var serveCmd = &cobra.Command{ jar.KIND: jar_v001.APIVERSION, intoto.KIND: intoto_v001.APIVERSION, rfc3161.KIND: rfc3161_v001.APIVERSION, + alpine.KIND: alpine_v001.APIVERSION, } for k, v := range pluggableTypeMap { diff --git a/go.mod b/go.mod index 0f4d79c40..5ad82708a 100644 --- a/go.mod +++ b/go.mod @@ -48,4 +48,5 @@ require ( google.golang.org/grpc v1.38.0 google.golang.org/protobuf v1.26.0 gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + gopkg.in/ini.v1 v1.62.0 ) diff --git a/openapi.yaml b/openapi.yaml index b2f6f5d98..0e2b04c81 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -314,7 +314,7 @@ definitions: rpm: type: object - description: RPM object + description: RPM package allOf: - $ref: '#/definitions/ProposedEntry' - properties: @@ -328,6 +328,24 @@ definitions: - apiVersion - spec additionalProperties: false + + alpine: + type: object + description: Alpine package + allOf: + - $ref: '#/definitions/ProposedEntry' + - properties: + apiVersion: + type: string + pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ + spec: + type: object + $ref: 'pkg/types/alpine/alpine_schema.json' + required: + - apiVersion + - spec + additionalProperties: false + intoto: type: object description: Intoto object @@ -344,6 +362,7 @@ definitions: - apiVersion - spec additionalProperties: false + jar: type: object description: Java Archive (JAR) diff --git a/pkg/generated/models/alpine.go b/pkg/generated/models/alpine.go new file mode 100644 index 000000000..5607679fd --- /dev/null +++ b/pkg/generated/models/alpine.go @@ -0,0 +1,210 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// Alpine Alpine package +// +// swagger:model alpine +type Alpine struct { + + // api version + // Required: true + // Pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ + APIVersion *string `json:"apiVersion"` + + // spec + // Required: true + Spec AlpineSchema `json:"spec"` +} + +// Kind gets the kind of this subtype +func (m *Alpine) Kind() string { + return "alpine" +} + +// SetKind sets the kind of this subtype +func (m *Alpine) SetKind(val string) { +} + +// UnmarshalJSON unmarshals this object with a polymorphic type from a JSON structure +func (m *Alpine) UnmarshalJSON(raw []byte) error { + var data struct { + + // api version + // Required: true + // Pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ + APIVersion *string `json:"apiVersion"` + + // spec + // Required: true + Spec AlpineSchema `json:"spec"` + } + buf := bytes.NewBuffer(raw) + dec := json.NewDecoder(buf) + dec.UseNumber() + + if err := dec.Decode(&data); err != nil { + return err + } + + var base struct { + /* Just the base type fields. Used for unmashalling polymorphic types.*/ + + Kind string `json:"kind"` + } + buf = bytes.NewBuffer(raw) + dec = json.NewDecoder(buf) + dec.UseNumber() + + if err := dec.Decode(&base); err != nil { + return err + } + + var result Alpine + + if base.Kind != result.Kind() { + /* Not the type we're looking for. */ + return errors.New(422, "invalid kind value: %q", base.Kind) + } + + result.APIVersion = data.APIVersion + result.Spec = data.Spec + + *m = result + + return nil +} + +// MarshalJSON marshals this object with a polymorphic type to a JSON structure +func (m Alpine) MarshalJSON() ([]byte, error) { + var b1, b2, b3 []byte + var err error + b1, err = json.Marshal(struct { + + // api version + // Required: true + // Pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ + APIVersion *string `json:"apiVersion"` + + // spec + // Required: true + Spec AlpineSchema `json:"spec"` + }{ + + APIVersion: m.APIVersion, + + Spec: m.Spec, + }) + if err != nil { + return nil, err + } + b2, err = json.Marshal(struct { + Kind string `json:"kind"` + }{ + + Kind: m.Kind(), + }) + if err != nil { + return nil, err + } + + return swag.ConcatJSON(b1, b2, b3), nil +} + +// Validate validates this alpine +func (m *Alpine) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAPIVersion(formats); err != nil { + res = append(res, err) + } + + if err := m.validateSpec(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *Alpine) validateAPIVersion(formats strfmt.Registry) error { + + if err := validate.Required("apiVersion", "body", m.APIVersion); err != nil { + return err + } + + if err := validate.Pattern("apiVersion", "body", *m.APIVersion, `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`); err != nil { + return err + } + + return nil +} + +func (m *Alpine) validateSpec(formats strfmt.Registry) error { + + if m.Spec == nil { + return errors.Required("spec", "body", nil) + } + + return nil +} + +// ContextValidate validate this alpine based on the context it is used +func (m *Alpine) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// MarshalBinary interface implementation +func (m *Alpine) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *Alpine) UnmarshalBinary(b []byte) error { + var res Alpine + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/generated/models/alpine_schema.go b/pkg/generated/models/alpine_schema.go new file mode 100644 index 000000000..49dd12b6b --- /dev/null +++ b/pkg/generated/models/alpine_schema.go @@ -0,0 +1,29 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +// AlpineSchema Alpine Package Schema +// +// Schema for Alpine package objects +// +// swagger:model alpineSchema +type AlpineSchema interface{} diff --git a/pkg/generated/models/alpine_v001_schema.go b/pkg/generated/models/alpine_v001_schema.go new file mode 100644 index 000000000..dbdd83851 --- /dev/null +++ b/pkg/generated/models/alpine_v001_schema.go @@ -0,0 +1,450 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// AlpineV001Schema Alpine v0.0.1 Schema +// +// Schema for Alpine Package entries +// +// swagger:model alpineV001Schema +type AlpineV001Schema struct { + + // Arbitrary content to be included in the verifiable entry in the transparency log + ExtraData interface{} `json:"extraData,omitempty"` + + // package + // Required: true + Package *AlpineV001SchemaPackage `json:"package"` + + // public key + // Required: true + PublicKey *AlpineV001SchemaPublicKey `json:"publicKey"` +} + +// Validate validates this alpine v001 schema +func (m *AlpineV001Schema) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validatePackage(formats); err != nil { + res = append(res, err) + } + + if err := m.validatePublicKey(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AlpineV001Schema) validatePackage(formats strfmt.Registry) error { + + if err := validate.Required("package", "body", m.Package); err != nil { + return err + } + + if m.Package != nil { + if err := m.Package.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("package") + } + return err + } + } + + return nil +} + +func (m *AlpineV001Schema) validatePublicKey(formats strfmt.Registry) error { + + if err := validate.Required("publicKey", "body", m.PublicKey); err != nil { + return err + } + + if m.PublicKey != nil { + if err := m.PublicKey.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("publicKey") + } + return err + } + } + + return nil +} + +// ContextValidate validate this alpine v001 schema based on the context it is used +func (m *AlpineV001Schema) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidatePackage(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidatePublicKey(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AlpineV001Schema) contextValidatePackage(ctx context.Context, formats strfmt.Registry) error { + + if m.Package != nil { + if err := m.Package.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("package") + } + return err + } + } + + return nil +} + +func (m *AlpineV001Schema) contextValidatePublicKey(ctx context.Context, formats strfmt.Registry) error { + + if m.PublicKey != nil { + if err := m.PublicKey.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("publicKey") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *AlpineV001Schema) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *AlpineV001Schema) UnmarshalBinary(b []byte) error { + var res AlpineV001Schema + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// AlpineV001SchemaPackage Information about the package associated with the entry +// +// swagger:model AlpineV001SchemaPackage +type AlpineV001SchemaPackage struct { + + // Specifies the package inline within the document + // Format: byte + Content strfmt.Base64 `json:"content,omitempty"` + + // hash + Hash *AlpineV001SchemaPackageHash `json:"hash,omitempty"` + + // Values of the .PKGINFO key / value pairs + Pkginfo map[string]string `json:"pkginfo,omitempty"` + + // Specifies the location of the package; if this is specified, a hash value must also be provided + // Format: uri + URL strfmt.URI `json:"url,omitempty"` +} + +// Validate validates this alpine v001 schema package +func (m *AlpineV001SchemaPackage) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateHash(formats); err != nil { + res = append(res, err) + } + + if err := m.validateURL(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AlpineV001SchemaPackage) validateHash(formats strfmt.Registry) error { + if swag.IsZero(m.Hash) { // not required + return nil + } + + if m.Hash != nil { + if err := m.Hash.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("package" + "." + "hash") + } + return err + } + } + + return nil +} + +func (m *AlpineV001SchemaPackage) validateURL(formats strfmt.Registry) error { + if swag.IsZero(m.URL) { // not required + return nil + } + + if err := validate.FormatOf("package"+"."+"url", "body", "uri", m.URL.String(), formats); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this alpine v001 schema package based on the context it is used +func (m *AlpineV001SchemaPackage) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateHash(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AlpineV001SchemaPackage) contextValidateHash(ctx context.Context, formats strfmt.Registry) error { + + if m.Hash != nil { + if err := m.Hash.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("package" + "." + "hash") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *AlpineV001SchemaPackage) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *AlpineV001SchemaPackage) UnmarshalBinary(b []byte) error { + var res AlpineV001SchemaPackage + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// AlpineV001SchemaPackageHash Specifies the hash algorithm and value for the package +// +// swagger:model AlpineV001SchemaPackageHash +type AlpineV001SchemaPackageHash struct { + + // The hashing function used to compute the hash value + // Required: true + // Enum: [sha256] + Algorithm *string `json:"algorithm"` + + // The hash value for the package + // Required: true + Value *string `json:"value"` +} + +// Validate validates this alpine v001 schema package hash +func (m *AlpineV001SchemaPackageHash) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAlgorithm(formats); err != nil { + res = append(res, err) + } + + if err := m.validateValue(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +var alpineV001SchemaPackageHashTypeAlgorithmPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + alpineV001SchemaPackageHashTypeAlgorithmPropEnum = append(alpineV001SchemaPackageHashTypeAlgorithmPropEnum, v) + } +} + +const ( + + // AlpineV001SchemaPackageHashAlgorithmSha256 captures enum value "sha256" + AlpineV001SchemaPackageHashAlgorithmSha256 string = "sha256" +) + +// prop value enum +func (m *AlpineV001SchemaPackageHash) validateAlgorithmEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, alpineV001SchemaPackageHashTypeAlgorithmPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *AlpineV001SchemaPackageHash) validateAlgorithm(formats strfmt.Registry) error { + + if err := validate.Required("package"+"."+"hash"+"."+"algorithm", "body", m.Algorithm); err != nil { + return err + } + + // value enum + if err := m.validateAlgorithmEnum("package"+"."+"hash"+"."+"algorithm", "body", *m.Algorithm); err != nil { + return err + } + + return nil +} + +func (m *AlpineV001SchemaPackageHash) validateValue(formats strfmt.Registry) error { + + if err := validate.Required("package"+"."+"hash"+"."+"value", "body", m.Value); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this alpine v001 schema package hash based on context it is used +func (m *AlpineV001SchemaPackageHash) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *AlpineV001SchemaPackageHash) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *AlpineV001SchemaPackageHash) UnmarshalBinary(b []byte) error { + var res AlpineV001SchemaPackageHash + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// AlpineV001SchemaPublicKey The public key that can verify the package signature +// +// swagger:model AlpineV001SchemaPublicKey +type AlpineV001SchemaPublicKey struct { + + // Specifies the content of the public key inline within the document + // Format: byte + Content strfmt.Base64 `json:"content,omitempty"` + + // Specifies the location of the public key + // Format: uri + URL strfmt.URI `json:"url,omitempty"` +} + +// Validate validates this alpine v001 schema public key +func (m *AlpineV001SchemaPublicKey) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateURL(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AlpineV001SchemaPublicKey) validateURL(formats strfmt.Registry) error { + if swag.IsZero(m.URL) { // not required + return nil + } + + if err := validate.FormatOf("publicKey"+"."+"url", "body", "uri", m.URL.String(), formats); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this alpine v001 schema public key based on context it is used +func (m *AlpineV001SchemaPublicKey) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *AlpineV001SchemaPublicKey) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *AlpineV001SchemaPublicKey) UnmarshalBinary(b []byte) error { + var res AlpineV001SchemaPublicKey + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/generated/models/proposed_entry.go b/pkg/generated/models/proposed_entry.go index 70812c512..94a5d6fab 100644 --- a/pkg/generated/models/proposed_entry.go +++ b/pkg/generated/models/proposed_entry.go @@ -115,6 +115,12 @@ func unmarshalProposedEntry(data []byte, consumer runtime.Consumer) (ProposedEnt return nil, err } return &result, nil + case "alpine": + var result Alpine + if err := consumer.Consume(buf2, &result); err != nil { + return nil, err + } + return &result, nil case "intoto": var result Intoto if err := consumer.Consume(buf2, &result); err != nil { diff --git a/pkg/generated/models/rpm.go b/pkg/generated/models/rpm.go index d4fab013d..8b1f10c77 100644 --- a/pkg/generated/models/rpm.go +++ b/pkg/generated/models/rpm.go @@ -32,7 +32,7 @@ import ( "github.com/go-openapi/validate" ) -// Rpm RPM object +// Rpm RPM package // // swagger:model rpm type Rpm struct { diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go index 04bae89f3..69c9dd862 100644 --- a/pkg/generated/restapi/embedded_spec.go +++ b/pkg/generated/restapi/embedded_spec.go @@ -625,6 +625,32 @@ func init() { } } }, + "alpine": { + "description": "Alpine package", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/ProposedEntry" + }, + { + "required": [ + "apiVersion", + "spec" + ], + "properties": { + "apiVersion": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + }, + "spec": { + "type": "object", + "$ref": "pkg/types/alpine/alpine_schema.json" + } + }, + "additionalProperties": false + } + ] + }, "intoto": { "description": "Intoto object", "type": "object", @@ -730,7 +756,7 @@ func init() { ] }, "rpm": { - "description": "RPM object", + "description": "RPM package", "type": "object", "allOf": [ { @@ -1225,6 +1251,111 @@ func init() { } }, "definitions": { + "AlpineV001SchemaPackage": { + "description": "Information about the package associated with the entry", + "type": "object", + "oneOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "content" + ] + } + ], + "properties": { + "content": { + "description": "Specifies the package inline within the document", + "type": "string", + "format": "byte" + }, + "hash": { + "description": "Specifies the hash algorithm and value for the package", + "type": "object", + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the package", + "type": "string" + } + } + }, + "pkginfo": { + "description": "Values of the .PKGINFO key / value pairs", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "url": { + "description": "Specifies the location of the package; if this is specified, a hash value must also be provided", + "type": "string", + "format": "uri" + } + } + }, + "AlpineV001SchemaPackageHash": { + "description": "Specifies the hash algorithm and value for the package", + "type": "object", + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the package", + "type": "string" + } + } + }, + "AlpineV001SchemaPublicKey": { + "description": "The public key that can verify the package signature", + "type": "object", + "oneOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "content" + ] + } + ], + "properties": { + "content": { + "description": "Specifies the content of the public key inline within the document", + "type": "string", + "format": "byte" + }, + "url": { + "description": "Specifies the location of the public key", + "type": "string", + "format": "uri" + } + } + }, "ConsistencyProof": { "type": "object", "required": [ @@ -1929,6 +2060,145 @@ func init() { } } }, + "alpine": { + "description": "Alpine package", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/ProposedEntry" + }, + { + "required": [ + "apiVersion", + "spec" + ], + "properties": { + "apiVersion": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + }, + "spec": { + "$ref": "#/definitions/alpineSchema" + } + }, + "additionalProperties": false + } + ] + }, + "alpineSchema": { + "description": "Schema for Alpine package objects", + "type": "object", + "title": "Alpine Package Schema", + "oneOf": [ + { + "$ref": "#/definitions/alpineV001Schema" + } + ], + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://rekor.sigstore.dev/types/alpine/alpine_schema.json" + }, + "alpineV001Schema": { + "description": "Schema for Alpine Package entries", + "type": "object", + "title": "Alpine v0.0.1 Schema", + "required": [ + "publicKey", + "package" + ], + "properties": { + "extraData": { + "description": "Arbitrary content to be included in the verifiable entry in the transparency log", + "type": "object", + "additionalProperties": true + }, + "package": { + "description": "Information about the package associated with the entry", + "type": "object", + "oneOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "content" + ] + } + ], + "properties": { + "content": { + "description": "Specifies the package inline within the document", + "type": "string", + "format": "byte" + }, + "hash": { + "description": "Specifies the hash algorithm and value for the package", + "type": "object", + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the package", + "type": "string" + } + } + }, + "pkginfo": { + "description": "Values of the .PKGINFO key / value pairs", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "url": { + "description": "Specifies the location of the package; if this is specified, a hash value must also be provided", + "type": "string", + "format": "uri" + } + } + }, + "publicKey": { + "description": "The public key that can verify the package signature", + "type": "object", + "oneOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "content" + ] + } + ], + "properties": { + "content": { + "description": "Specifies the content of the public key inline within the document", + "type": "string", + "format": "byte" + }, + "url": { + "description": "Specifies the location of the public key", + "type": "string", + "format": "uri" + } + } + } + }, + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://rekor.sigstore.dev/types/alpine/alpine_v0_0_1_schema.json" + }, "intoto": { "description": "Intoto object", "type": "object", @@ -2394,7 +2664,7 @@ func init() { "$id": "http://rekor.sigstore.dev/types/timestamp/timestamp_v0_0_1_schema.json" }, "rpm": { - "description": "RPM object", + "description": "RPM package", "type": "object", "allOf": [ { diff --git a/pkg/pki/x509/x509.go b/pkg/pki/x509/x509.go index 7bf2922fc..c7801e20f 100644 --- a/pkg/pki/x509/x509.go +++ b/pkg/pki/x509/x509.go @@ -60,6 +60,7 @@ func (s Signature) CanonicalValue() ([]byte, error) { // Verify implements the pki.Signature interface func (s Signature) Verify(r io.Reader, k interface{}) error { if len(s.signature) == 0 { + //lint:ignore ST1005 X509 is proper use of term return fmt.Errorf("X509 signature has not been initialized") } @@ -73,7 +74,7 @@ func (s Signature) Verify(r io.Reader, k interface{}) error { key, ok := k.(*PublicKey) if !ok { - return fmt.Errorf("Invalid public key type for: %v", k) + return fmt.Errorf("invalid public key type for: %v", k) } p := key.key @@ -174,6 +175,10 @@ func (k PublicKey) CanonicalValue() ([]byte, error) { return buf.Bytes(), nil } +func (k PublicKey) CryptoPubKey() crypto.PublicKey { + return k.key +} + // EmailAddresses implements the pki.PublicKey interface func (k PublicKey) EmailAddresses() []string { var names []string diff --git a/pkg/types/alpine/alpine.go b/pkg/types/alpine/alpine.go new file mode 100644 index 000000000..ee3e2ffe8 --- /dev/null +++ b/pkg/types/alpine/alpine.go @@ -0,0 +1,57 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package alpine + +import ( + "errors" + + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" +) + +const ( + KIND = "alpine" +) + +type BaseAlpineType struct { + types.RekorType +} + +func init() { + types.TypeMap.Store(KIND, New) +} + +func New() types.TypeImpl { + bat := BaseAlpineType{} + bat.Kind = KIND + bat.VersionMap = VersionMap + return &bat +} + +var VersionMap = types.NewSemVerEntryFactoryMap() + +func (brt *BaseAlpineType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl, error) { + if pe == nil { + return nil, errors.New("proposed entry cannot be nil") + } + + apk, ok := pe.(*models.Alpine) + if !ok { + return nil, errors.New("cannot unmarshal non-Alpine types") + } + + return brt.VersionedUnmarshal(apk, *apk.APIVersion) +} diff --git a/pkg/types/alpine/alpine_schema.json b/pkg/types/alpine/alpine_schema.json new file mode 100644 index 000000000..b66657eea --- /dev/null +++ b/pkg/types/alpine/alpine_schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://rekor.sigstore.dev/types/alpine/alpine_schema.json", + "title": "Alpine Package Schema", + "description": "Schema for Alpine package objects", + "type": "object", + "oneOf": [ + { + "$ref": "v0.0.1/alpine_v0_0_1_schema.json" + } + ] +} diff --git a/pkg/types/alpine/alpine_test.go b/pkg/types/alpine/alpine_test.go new file mode 100644 index 000000000..749116e96 --- /dev/null +++ b/pkg/types/alpine/alpine_test.go @@ -0,0 +1,128 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package alpine + +import ( + "context" + "errors" + "testing" + + "github.com/go-openapi/swag" + + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" +) + +type UnmarshalTester struct { + models.Alpine +} + +func (u UnmarshalTester) NewEntry() types.EntryImpl { + return &UnmarshalTester{} +} + +func (u UnmarshalTester) APIVersion() string { + return "2.0.1" +} + +func (u UnmarshalTester) IndexKeys() []string { + return []string{} +} + +func (u UnmarshalTester) Canonicalize(ctx context.Context) ([]byte, error) { + return nil, nil +} + +func (u UnmarshalTester) HasExternalEntities() bool { + return false +} + +func (u *UnmarshalTester) FetchExternalEntities(ctx context.Context) error { + return nil +} + +func (u UnmarshalTester) Unmarshal(pe models.ProposedEntry) error { + return nil +} + +func (u UnmarshalTester) Validate() error { + return nil +} + +func (u UnmarshalTester) Attestation() (string, []byte) { + return "", nil +} + +type UnmarshalFailsTester struct { + UnmarshalTester +} + +func (u UnmarshalFailsTester) NewEntry() types.EntryImpl { + return &UnmarshalFailsTester{} +} + +func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error { + return errors.New("error") +} + +func TestAlpineType(t *testing.T) { + // empty to start + if VersionMap.Count() != 0 { + t.Error("semver range was not blank at start of test") + } + + u := UnmarshalTester{} + // ensure semver range parser is working + invalidSemVerRange := "not a valid semver range" + err := VersionMap.SetEntryFactory(invalidSemVerRange, u.NewEntry) + if err == nil || VersionMap.Count() > 0 { + t.Error("invalid semver range was incorrectly added to SemVerToFacFnMap") + } + + // valid semver range can be parsed + err = VersionMap.SetEntryFactory(">= 1.2.3", u.NewEntry) + if err != nil || VersionMap.Count() != 1 { + t.Error("valid semver range was not added to SemVerToFacFnMap") + } + + u.Alpine.APIVersion = swag.String("2.0.1") + brt := New() + + // version requested matches implementation in map + if _, err := brt.UnmarshalEntry(&u.Alpine); err != nil { + t.Errorf("unexpected error in Unmarshal: %v", err) + } + + // version requested fails to match implementation in map + u.Alpine.APIVersion = swag.String("1.2.2") + if _, err := brt.UnmarshalEntry(&u.Alpine); err == nil { + t.Error("unexpected success in Unmarshal for non-matching version") + } + + // error in Unmarshal call is raised appropriately + u.Alpine.APIVersion = swag.String("2.2.0") + u2 := UnmarshalFailsTester{} + _ = VersionMap.SetEntryFactory(">= 1.2.3", u2.NewEntry) + if _, err := brt.UnmarshalEntry(&u.Alpine); err == nil { + t.Error("unexpected success in Unmarshal when error is thrown") + } + + // version requested fails to match implementation in map + u.Alpine.APIVersion = swag.String("not_a_version") + if _, err := brt.UnmarshalEntry(&u.Alpine); err == nil { + t.Error("unexpected success in Unmarshal for invalid version") + } +} diff --git a/pkg/types/alpine/apk.go b/pkg/types/alpine/apk.go new file mode 100644 index 000000000..2481693c0 --- /dev/null +++ b/pkg/types/alpine/apk.go @@ -0,0 +1,245 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package alpine + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/gzip" + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha1" // #nosec G505 + "crypto/sha256" + "encoding/hex" + "encoding/pem" + "fmt" + "hash" + "io" + "strings" + + "github.com/pkg/errors" + "gopkg.in/ini.v1" +) + +type Package struct { + Pkginfo map[string]string // KVP pairs + Signature []byte + Datahash []byte + controlSHA1Digest []byte +} + +type sha1Reader struct { + r *bufio.Reader + addToHash bool + hasher hash.Hash +} + +func newSHA1Reader(b *bufio.Reader) *sha1Reader { + // #nosec G401 + c := sha1Reader{ + r: b, + hasher: sha1.New(), + } + return &c +} + +func (s *sha1Reader) Read(p []byte) (int, error) { + n, err := s.r.Read(p) + if err == nil && n > 0 && s.addToHash { + s.hasher.Write(p) + } + return n, err +} + +func (s *sha1Reader) ReadByte() (byte, error) { + b, err := s.r.ReadByte() + if err == nil && s.addToHash { + s.hasher.Write([]byte{b}) + } + return b, err +} + +func (s sha1Reader) Sum() []byte { + return s.hasher.Sum(nil) +} + +func (s *sha1Reader) StartHashing() { + s.hasher.Reset() + s.addToHash = true +} + +func (s *sha1Reader) StopHashing() { + s.addToHash = false +} + +func (p *Package) Unmarshal(pkgReader io.Reader) error { + pkg := Package{} + // bufio.Reader is required if Multistream(false) is used + bufReader := bufio.NewReader(pkgReader) + sha1BufReader := newSHA1Reader(bufReader) + gzipReader, err := gzip.NewReader(sha1BufReader) + if err != nil { + return errors.Wrap(err, "create gzip reader") + } + defer func() { + _ = gzipReader.Close() + }() + + // APKs are concatenated gzip files so we want to know where the boundary is + gzipReader.Multistream(false) + + // GZIP headers/footers are left unmodified; Tar footers are removed on first two archives + // signature.tar.gz | control.tar.gz | data.tar.gz + sigBuf := bytes.Buffer{} + // #nosec G110 + if _, err := io.Copy(&sigBuf, gzipReader); err != nil { + return errors.Wrap(err, "reading signature.tar.gz") + } + + // the SHA1 sum used in the signature is over the entire file control.tar.gz so we need to + // intercept the buffered reading to compute the hash correctly + // + // we start sha1 hashing now since the Reset() call will begin reading control.tar.gz headers + sha1BufReader.StartHashing() + + // we reset the reader since we've found the end of signature.tar.gz + if err := gzipReader.Reset(sha1BufReader); err != nil && err != io.EOF { + return errors.Wrap(err, "resetting to control.tar.gz") + } + gzipReader.Multistream(false) + + controlTar := bytes.Buffer{} + // #nosec G110 + if _, err = io.Copy(&controlTar, gzipReader); err != nil { + return errors.Wrap(err, "reading control.tar.gz") + } + + // signature uses sha1 digest hardcoded in abuild-sign tool + pkg.controlSHA1Digest = sha1BufReader.Sum() + sha1BufReader.StopHashing() + + // the gzip reader is NOT reset again since that advances the underlying reader + // by reading the next GZIP header, which affects the datahash computation below + + sigReader := tar.NewReader(&sigBuf) + for { + header, err := sigReader.Next() + if err == io.EOF { + if pkg.Signature == nil { + return errors.New("no signature detected in alpine package") + } + break + } else if err != nil { + return errors.Wrap(err, "getting next entry in tar archive") + } + + if strings.HasPrefix(header.Name, ".SIGN") && pkg.Signature == nil { + sigBytes := make([]byte, header.Size) + if _, err = sigReader.Read(sigBytes); err != nil && err != io.EOF { + return errors.Wrap(err, "reading signature") + } + // we're not sure whether this is PEM encoded or not, so handle both cases + block, _ := pem.Decode(sigBytes) + if block == nil { + pkg.Signature = sigBytes + } else { + pkg.Signature = block.Bytes + } + } + } + + ctlReader := tar.NewReader(&controlTar) + for { + header, err := ctlReader.Next() + if err == io.EOF { + if pkg.Pkginfo == nil { + return errors.New(".PKGINFO file was not located") + } + break + } else if err != nil { + return errors.Wrap(err, "getting next entry in tar archive") + } + + if header.Name == ".PKGINFO" { + pkginfoContent := make([]byte, header.Size) + if _, err = ctlReader.Read(pkginfoContent); err != nil && err != io.EOF { + return errors.Wrap(err, "reading .PKGINFO") + } + + pkg.Pkginfo, err = parsePkginfo(pkginfoContent) + if err != nil { + return errors.Wrap(err, "parsing .PKGINFO") + } + pkg.Datahash, err = hex.DecodeString(pkg.Pkginfo["datahash"]) + if err != nil { + return errors.Wrap(err, "parsing datahash") + } + } + } + + // at this point, bufReader should point to first byte of data.tar.gz + // datahash value from .PKGINFO is sha256 sum of data.tar.gz + sha256 := sha256.New() + if _, err := io.Copy(sha256, bufReader); err != nil { + return errors.Wrap(err, "computing SHA256 sum of data.tar.gz") + } + computedSum := sha256.Sum(nil) + + if !bytes.Equal(computedSum, pkg.Datahash) { + return fmt.Errorf("checksum for data.tar.gz (%v) does not match value from .PKGINFO (%v)", hex.EncodeToString(computedSum), hex.EncodeToString(pkg.Datahash)) + } + *p = pkg + return nil +} + +// VerifySignature verifies the signature of the alpine package using the provided +// public key. It returns an error if verification fails, or nil if it is successful. +func (p Package) VerifySignature(pub crypto.PublicKey) error { + if p.Signature == nil { + return errors.New("no signature in alpine package object") + } + if p.controlSHA1Digest == nil { + return errors.New("no digest value for data.tar.gz known") + } + + switch pk := pub.(type) { + case *rsa.PublicKey: + return rsa.VerifyPKCS1v15(pk, crypto.SHA1, p.controlSHA1Digest, p.Signature) + case *ecdsa.PublicKey: + if !ecdsa.VerifyASN1(pk, p.controlSHA1Digest, p.Signature) { + return errors.New("failed to verify ECDSA signature") + } + default: + return errors.New("unknown key algorithm") + } + return nil +} + +// parsePkginfo parses the .PKGINFO file which is in a +// key[space]=[space]value\n +// format. it returns a map[string]string of the key/value pairs, or +// an error if parsing could not be completed successfully. +func parsePkginfo(input []byte) (map[string]string, error) { + cfg, err := ini.Load(input) + if err != nil { + return nil, err + } + + // .PKGINFO does not use sections, so using "" grabs the default values + return cfg.Section("").KeysHash(), nil +} diff --git a/pkg/types/alpine/apk_test.go b/pkg/types/alpine/apk_test.go new file mode 100644 index 000000000..08185c1a0 --- /dev/null +++ b/pkg/types/alpine/apk_test.go @@ -0,0 +1,50 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package alpine + +import ( + "os" + "testing" + + "github.com/sigstore/rekor/pkg/pki/x509" +) + +func TestAlpinePackage(t *testing.T) { + inputArchive, err := os.Open("../../../tests/test_alpine.apk") + if err != nil { + t.Fatalf("could not open archive %v", err) + } + + p := Package{} + err = p.Unmarshal(inputArchive) + if err != nil { + t.Fatalf("unmarshal error: %v", err) + } + + pubKey, err := os.Open("../../../tests/test_alpine.pub") + if err != nil { + t.Fatalf("could not open archive %v", err) + } + + pub, err := x509.NewPublicKey(pubKey) + if err != nil { + t.Fatalf("failed to parse public key: %v", err) + } + + if err = p.VerifySignature(pub.CryptoPubKey()); err != nil { + t.Fatalf("signature verification failed: %v", err) + } +} diff --git a/pkg/types/alpine/v0.0.1/alpine_v0_0_1_schema.json b/pkg/types/alpine/v0.0.1/alpine_v0_0_1_schema.json new file mode 100644 index 000000000..7cbcf530f --- /dev/null +++ b/pkg/types/alpine/v0.0.1/alpine_v0_0_1_schema.json @@ -0,0 +1,86 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://rekor.sigstore.dev/types/alpine/alpine_v0_0_1_schema.json", + "title": "Alpine v0.0.1 Schema", + "description": "Schema for Alpine Package entries", + "type": "object", + "properties": { + "publicKey" : { + "description": "The public key that can verify the package signature", + "type": "object", + "properties": { + "url": { + "description": "Specifies the location of the public key", + "type": "string", + "format": "uri" + }, + "content": { + "description": "Specifies the content of the public key inline within the document", + "type": "string", + "format": "byte" + } + }, + "oneOf": [ + { + "required": [ "url" ] + }, + { + "required": [ "content" ] + } + ] + }, + "package": { + "description": "Information about the package associated with the entry", + "type": "object", + "properties": { + "pkginfo": { + "description": "Values of the .PKGINFO key / value pairs", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "hash": { + "description": "Specifies the hash algorithm and value for the package", + "type": "object", + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ "sha256" ] + }, + "value": { + "description": "The hash value for the package", + "type": "string" + } + }, + "required": [ "algorithm", "value" ] + }, + "url": { + "description": "Specifies the location of the package; if this is specified, a hash value must also be provided", + "type": "string", + "format": "uri" + }, + "content": { + "description": "Specifies the package inline within the document", + "type": "string", + "format": "byte" + } + }, + "oneOf": [ + { + "required": [ "url" ] + }, + { + "required": [ "content" ] + } + ] + }, + "extraData": { + "description": "Arbitrary content to be included in the verifiable entry in the transparency log", + "type": "object", + "additionalProperties": true + } + }, + "required": [ "publicKey", "package" ] +} diff --git a/pkg/types/alpine/v0.0.1/entry.go b/pkg/types/alpine/v0.0.1/entry.go new file mode 100644 index 000000000..87480a833 --- /dev/null +++ b/pkg/types/alpine/v0.0.1/entry.go @@ -0,0 +1,340 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package alpine + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "strings" + + "github.com/asaskevich/govalidator" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "golang.org/x/sync/errgroup" + + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/log" + "github.com/sigstore/rekor/pkg/pki" + "github.com/sigstore/rekor/pkg/pki/x509" + "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/rekor/pkg/types/alpine" + "github.com/sigstore/rekor/pkg/util" +) + +const ( + APIVERSION = "0.0.1" +) + +func init() { + if err := alpine.VersionMap.SetEntryFactory(APIVERSION, NewEntry); err != nil { + log.Logger.Panic(err) + } +} + +type V001Entry struct { + AlpineModel models.AlpineV001Schema + fetchedExternalEntities bool + keyObj pki.PublicKey + apkObj *alpine.Package +} + +func (v V001Entry) APIVersion() string { + return APIVERSION +} + +func NewEntry() types.EntryImpl { + return &V001Entry{} +} + +func (v V001Entry) IndexKeys() []string { + var result []string + + if v.HasExternalEntities() { + if err := v.FetchExternalEntities(context.Background()); err != nil { + log.Logger.Error(err) + return result + } + } + + key, err := v.keyObj.CanonicalValue() + if err != nil { + log.Logger.Error(err) + } else { + keyHash := sha256.Sum256(key) + result = append(result, strings.ToLower(hex.EncodeToString(keyHash[:]))) + } + + result = append(result, v.keyObj.EmailAddresses()...) + + if v.AlpineModel.Package.Hash != nil { + hashKey := strings.ToLower(fmt.Sprintf("%s:%s", *v.AlpineModel.Package.Hash.Algorithm, *v.AlpineModel.Package.Hash.Value)) + result = append(result, hashKey) + } + + return result +} + +func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error { + apk, ok := pe.(*models.Alpine) + if !ok { + return errors.New("cannot unmarshal non Alpine v0.0.1 type") + } + + if err := types.DecodeEntry(apk.Spec, &v.AlpineModel); err != nil { + return err + } + + // field validation + if err := v.AlpineModel.Validate(strfmt.Default); err != nil { + return err + } + return nil + +} + +func (v V001Entry) HasExternalEntities() bool { + if v.fetchedExternalEntities { + return false + } + + if v.AlpineModel.Package != nil && v.AlpineModel.Package.URL.String() != "" { + return true + } + if v.AlpineModel.PublicKey != nil && v.AlpineModel.PublicKey.URL.String() != "" { + return true + } + return false +} + +func (v *V001Entry) FetchExternalEntities(ctx context.Context) error { + if v.fetchedExternalEntities { + return nil + } + + if err := v.Validate(); err != nil { + return err + } + + g, ctx := errgroup.WithContext(ctx) + + hashR, hashW := io.Pipe() + apkR, apkW := io.Pipe() + defer hashR.Close() + defer apkR.Close() + + closePipesOnError := func(err error) error { + pipeReaders := []*io.PipeReader{hashR, apkR} + pipeWriters := []*io.PipeWriter{hashW, apkW} + for idx := range pipeReaders { + if e := pipeReaders[idx].CloseWithError(err); e != nil { + log.Logger.Error(fmt.Errorf("error closing pipe: %w", e)) + } + if e := pipeWriters[idx].CloseWithError(err); e != nil { + log.Logger.Error(fmt.Errorf("error closing pipe: %w", e)) + } + } + return err + } + + oldSHA := "" + if v.AlpineModel.Package.Hash != nil && v.AlpineModel.Package.Hash.Value != nil { + oldSHA = swag.StringValue(v.AlpineModel.Package.Hash.Value) + } + artifactFactory := pki.NewArtifactFactory("x509") + + g.Go(func() error { + defer hashW.Close() + defer apkW.Close() + + dataReadCloser, err := util.FileOrURLReadCloser(ctx, v.AlpineModel.Package.URL.String(), v.AlpineModel.Package.Content) + if err != nil { + return closePipesOnError(err) + } + defer dataReadCloser.Close() + + /* #nosec G110 */ + if _, err := io.Copy(io.MultiWriter(hashW, apkW), dataReadCloser); err != nil { + return closePipesOnError(err) + } + return nil + }) + + hashResult := make(chan string) + + g.Go(func() error { + defer close(hashResult) + hasher := sha256.New() + + if _, err := io.Copy(hasher, hashR); err != nil { + return closePipesOnError(err) + } + + computedSHA := hex.EncodeToString(hasher.Sum(nil)) + if oldSHA != "" && computedSHA != oldSHA { + return closePipesOnError(fmt.Errorf("SHA mismatch: %s != %s", computedSHA, oldSHA)) + } + + select { + case <-ctx.Done(): + return ctx.Err() + case hashResult <- computedSHA: + return nil + } + }) + + keyResult := make(chan *x509.PublicKey) + + g.Go(func() error { + defer close(keyResult) + keyReadCloser, err := util.FileOrURLReadCloser(ctx, v.AlpineModel.PublicKey.URL.String(), + v.AlpineModel.PublicKey.Content) + if err != nil { + return closePipesOnError(err) + } + defer keyReadCloser.Close() + + v.keyObj, err = artifactFactory.NewPublicKey(keyReadCloser) + if err != nil { + return closePipesOnError(err) + } + + select { + case <-ctx.Done(): + return ctx.Err() + case keyResult <- v.keyObj.(*x509.PublicKey): + return nil + } + }) + + g.Go(func() error { + apk := alpine.Package{} + if err := apk.Unmarshal(apkR); err != nil { + return closePipesOnError(err) + } + + key := <-keyResult + if key == nil { + return closePipesOnError(errors.New("error processing public key")) + } + + if err := apk.VerifySignature(key.CryptoPubKey()); err != nil { + return closePipesOnError(err) + } + + v.apkObj = &apk + + select { + case <-ctx.Done(): + return ctx.Err() + default: + return nil + } + }) + + computedSHA := <-hashResult + + if err := g.Wait(); err != nil { + return err + } + + // if we get here, all goroutines succeeded without error + if oldSHA == "" { + v.AlpineModel.Package.Hash = &models.AlpineV001SchemaPackageHash{} + v.AlpineModel.Package.Hash.Algorithm = swag.String(models.AlpineV001SchemaPackageHashAlgorithmSha256) + v.AlpineModel.Package.Hash.Value = swag.String(computedSHA) + } + + v.fetchedExternalEntities = true + return nil +} + +func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) { + if err := v.FetchExternalEntities(ctx); err != nil { + return nil, err + } + if v.keyObj == nil { + return nil, errors.New("key object not initialized before canonicalization") + } + + canonicalEntry := models.AlpineV001Schema{} + canonicalEntry.ExtraData = v.AlpineModel.ExtraData + + var err error + + // need to canonicalize key content + canonicalEntry.PublicKey = &models.AlpineV001SchemaPublicKey{} + canonicalEntry.PublicKey.Content, err = v.keyObj.CanonicalValue() + if err != nil { + return nil, err + } + + canonicalEntry.Package = &models.AlpineV001SchemaPackage{} + canonicalEntry.Package.Hash = &models.AlpineV001SchemaPackageHash{} + canonicalEntry.Package.Hash.Algorithm = v.AlpineModel.Package.Hash.Algorithm + canonicalEntry.Package.Hash.Value = v.AlpineModel.Package.Hash.Value + // data content is not set deliberately + + // set .PKGINFO headers + canonicalEntry.Package.Pkginfo = v.apkObj.Pkginfo + + // ExtraData is copied through unfiltered + canonicalEntry.ExtraData = v.AlpineModel.ExtraData + + // wrap in valid object with kind and apiVersion set + apk := models.Alpine{} + apk.APIVersion = swag.String(APIVERSION) + apk.Spec = &canonicalEntry + + return json.Marshal(&apk) +} + +// Validate performs cross-field validation for fields in object +func (v V001Entry) Validate() error { + key := v.AlpineModel.PublicKey + if key == nil { + return errors.New("missing public key") + } + if len(key.Content) == 0 && key.URL.String() == "" { + return errors.New("one of 'content' or 'url' must be specified for publicKey") + } + + pkg := v.AlpineModel.Package + if pkg == nil { + return errors.New("missing package") + } + + if len(pkg.Content) == 0 && pkg.URL.String() == "" { + return errors.New("one of 'content' or 'url' must be specified for package") + } + + hash := pkg.Hash + if hash != nil { + if !govalidator.IsHash(swag.StringValue(hash.Value), swag.StringValue(hash.Algorithm)) { + return errors.New("invalid value for hash") + } + } + + return nil +} + +func (v V001Entry) Attestation() (string, []byte) { + return "", nil +} diff --git a/pkg/types/alpine/v0.0.1/entry_test.go b/pkg/types/alpine/v0.0.1/entry_test.go new file mode 100644 index 000000000..d7998a190 --- /dev/null +++ b/pkg/types/alpine/v0.0.1/entry_test.go @@ -0,0 +1,394 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package alpine + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "go.uber.org/goleak" + + "github.com/sigstore/rekor/pkg/generated/models" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +func TestNewEntryReturnType(t *testing.T) { + entry := NewEntry() + if reflect.TypeOf(entry) != reflect.ValueOf(&V001Entry{}).Type() { + t.Errorf("invalid type returned from NewEntry: %T", entry) + } +} + +func TestCrossFieldValidation(t *testing.T) { + type TestCase struct { + caseDesc string + entry V001Entry + hasExtEntities bool + expectUnmarshalSuccess bool + expectCanonicalizeSuccess bool + } + + keyBytes, _ := ioutil.ReadFile("../../../../tests/test_alpine.pub") + dataBytes, _ := ioutil.ReadFile("../../../../tests/test_alpine.apk") + + h := sha256.Sum256(dataBytes) + dataSHA := hex.EncodeToString(h[:]) + + testServer := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + file := &keyBytes + var err error + + switch r.URL.Path { + case "/key": + file = &keyBytes + case "/data": + file = &dataBytes + default: + err = errors.New("unknown URL") + } + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write(*file) + })) + defer testServer.Close() + + testCases := []TestCase{ + { + caseDesc: "empty obj", + entry: V001Entry{}, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "public key without url or content", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{}, + }, + }, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "public key without package", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + URL: strfmt.URI(testServer.URL + "/key"), + }, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "public key with empty package", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + URL: strfmt.URI(testServer.URL + "/key"), + }, + Package: &models.AlpineV001SchemaPackage{}, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "public key with data & url but no hash", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + URL: strfmt.URI(testServer.URL + "/key"), + }, + Package: &models.AlpineV001SchemaPackage{ + URL: strfmt.URI(testServer.URL + "/data"), + }, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: true, + }, + { + caseDesc: "public key with data & url and empty hash", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + URL: strfmt.URI(testServer.URL + "/key"), + }, + Package: &models.AlpineV001SchemaPackage{ + Hash: &models.AlpineV001SchemaPackageHash{}, + URL: strfmt.URI(testServer.URL + "/data"), + }, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "public key with data & url and hash missing value", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + URL: strfmt.URI(testServer.URL + "/key"), + }, + Package: &models.AlpineV001SchemaPackage{ + Hash: &models.AlpineV001SchemaPackageHash{ + Algorithm: swag.String(models.AlpineV001SchemaPackageHashAlgorithmSha256), + }, + URL: strfmt.URI(testServer.URL + "/data"), + }, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "public key with data & url with 404 error on key", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + URL: strfmt.URI(testServer.URL + "/404"), + }, + Package: &models.AlpineV001SchemaPackage{ + Hash: &models.AlpineV001SchemaPackageHash{ + Algorithm: swag.String(models.AlpineV001SchemaPackageHashAlgorithmSha256), + Value: swag.String(dataSHA), + }, + URL: strfmt.URI(testServer.URL + "/data"), + }, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: false, + }, + { + caseDesc: "public key with data & url with 404 error on data", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + URL: strfmt.URI(testServer.URL + "/key"), + }, + Package: &models.AlpineV001SchemaPackage{ + Hash: &models.AlpineV001SchemaPackageHash{ + Algorithm: swag.String(models.AlpineV001SchemaPackageHashAlgorithmSha256), + Value: swag.String(dataSHA), + }, + URL: strfmt.URI(testServer.URL + "/404"), + }, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: false, + }, + { + caseDesc: "public key with invalid key content & with data with content", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + Content: strfmt.Base64(dataBytes), + }, + Package: &models.AlpineV001SchemaPackage{ + Content: strfmt.Base64(dataBytes), + }, + }, + }, + hasExtEntities: false, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: false, + }, + { + caseDesc: "public key with data & url and incorrect hash value", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + URL: strfmt.URI(testServer.URL + "/key"), + }, + Package: &models.AlpineV001SchemaPackage{ + Hash: &models.AlpineV001SchemaPackageHash{ + Algorithm: swag.String(models.AlpineV001SchemaPackageHashAlgorithmSha256), + Value: swag.String("3030303030303030303030303030303030303030303030303030303030303030"), + }, + URL: strfmt.URI(testServer.URL + "/data"), + }, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: false, + }, + { + caseDesc: "public key with data & url and complete hash value", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + URL: strfmt.URI(testServer.URL + "/key"), + }, + Package: &models.AlpineV001SchemaPackage{ + Hash: &models.AlpineV001SchemaPackageHash{ + Algorithm: swag.String(models.AlpineV001SchemaPackageHashAlgorithmSha256), + Value: swag.String(dataSHA), + }, + URL: strfmt.URI(testServer.URL + "/data"), + }, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: true, + }, + { + caseDesc: "public key with url key & with data with url and complete hash value", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + URL: strfmt.URI(testServer.URL + "/key"), + }, + Package: &models.AlpineV001SchemaPackage{ + Hash: &models.AlpineV001SchemaPackageHash{ + Algorithm: swag.String(models.AlpineV001SchemaPackageHashAlgorithmSha256), + Value: swag.String(dataSHA), + }, + URL: strfmt.URI(testServer.URL + "/data"), + }, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: true, + }, + { + caseDesc: "public key with key content & with data with url and complete hash value", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + Content: strfmt.Base64(keyBytes), + }, + Package: &models.AlpineV001SchemaPackage{ + Hash: &models.AlpineV001SchemaPackageHash{ + Algorithm: swag.String(models.AlpineV001SchemaPackageHashAlgorithmSha256), + Value: swag.String(dataSHA), + }, + URL: strfmt.URI(testServer.URL + "/data"), + }, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: true, + }, + { + caseDesc: "public key with key content & with data with url and complete hash value", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + Content: strfmt.Base64(keyBytes), + }, + Package: &models.AlpineV001SchemaPackage{ + Hash: &models.AlpineV001SchemaPackageHash{ + Algorithm: swag.String(models.AlpineV001SchemaPackageHashAlgorithmSha256), + Value: swag.String(dataSHA), + }, + URL: strfmt.URI(testServer.URL + "/data"), + }, + }, + }, + hasExtEntities: true, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: true, + }, + { + caseDesc: "public key with key content & with data with content", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + Content: strfmt.Base64(keyBytes), + }, + Package: &models.AlpineV001SchemaPackage{ + Content: strfmt.Base64(dataBytes), + }, + }, + }, + hasExtEntities: false, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: true, + }, + { + caseDesc: "valid obj with extradata", + entry: V001Entry{ + AlpineModel: models.AlpineV001Schema{ + PublicKey: &models.AlpineV001SchemaPublicKey{ + Content: strfmt.Base64(keyBytes), + }, + Package: &models.AlpineV001SchemaPackage{ + Content: strfmt.Base64(dataBytes), + }, + ExtraData: []byte("{\"something\": \"here\""), + }, + }, + hasExtEntities: false, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: true, + }, + } + + for _, tc := range testCases { + if err := tc.entry.Validate(); (err == nil) != tc.expectUnmarshalSuccess { + t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err) + } + + v := &V001Entry{} + r := models.Alpine{ + APIVersion: swag.String(tc.entry.APIVersion()), + Spec: tc.entry.AlpineModel, + } + + unmarshalAndValidate := func() error { + if err := v.Unmarshal(&r); err != nil { + return err + } + return v.Validate() + } + if err := unmarshalAndValidate(); (err == nil) != tc.expectUnmarshalSuccess { + t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err) + } + + if tc.entry.HasExternalEntities() != tc.hasExtEntities { + t.Errorf("unexpected result from HasExternalEntities for '%v'", tc.caseDesc) + } + + if _, err := tc.entry.Canonicalize(context.TODO()); (err == nil) != tc.expectCanonicalizeSuccess { + t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err) + } + } +} diff --git a/pkg/types/rpm/v0.0.1/entry.go b/pkg/types/rpm/v0.0.1/entry.go index f8d31cd9a..e37178f59 100644 --- a/pkg/types/rpm/v0.0.1/entry.go +++ b/pkg/types/rpm/v0.0.1/entry.go @@ -323,12 +323,7 @@ func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) { rpm.APIVersion = swag.String(APIVERSION) rpm.Spec = &canonicalEntry - bytes, err := json.Marshal(&rpm) - if err != nil { - return nil, err - } - - return bytes, nil + return json.Marshal(&rpm) } // Validate performs cross-field validation for fields in object diff --git a/tests/alpine.json b/tests/alpine.json new file mode 100644 index 000000000..5d09985c3 --- /dev/null +++ b/tests/alpine.json @@ -0,0 +1 @@ +{"kind":"alpine","apiVersion":"0.0.1","spec":{"package":{"content":"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"},"publicKey":{"content":"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUExeUhKeFFnc0hRUkVjbFF1NE9oZQpxeFR4ZDF0SGNObnZuUVR1L1VyVGt5OHdXdmdYVCtqcHZlcm9lV1duem1zWWxESTkzZUxJMk9SYWt4YjNnQTJPClEwUnk0d3M4dmhheExRR0M3NHVRUjUrL3lZckx1VEt5ZEZ6dVBhUzFkSzE5cUpQWEI4R01kbUZPaWpuWFg0U0EKaml4dUhMZTFXVzdrWlZ0akw3bnVmdnBYa1dCR2pzZnJ2c2tkTkEvNU1meEFlQmJxUGdhcTBRTUVmeE1BbjYvUgpMNWtOZXBpL1ZyNFMzOVh2ZjJEeldrVExFSzhwY25qTmt0OS9hYWZoV3FGVlc3bTNIQ0FJSTZoL3FsUU5RS1NvCkd1SDM0UThHc0ZHMzBpelVFTlY5YXZZN2hTTHE3bmdnc3ZrbmxOQlp0RlVjbUdvUXJ0eDNGbXlZc0lDOC9SK0IKeXdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="}}} diff --git a/tests/apk.go b/tests/apk.go new file mode 100644 index 000000000..dd85dc49a --- /dev/null +++ b/tests/apk.go @@ -0,0 +1,92 @@ +// +build e2e + +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto" + crand "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "encoding/hex" + "io" + "io/ioutil" + "strings" + "testing" +) + +func createSignedApk(t *testing.T, artifactPath string) { + t.Helper() + + data := randomData(t, 100) + dataTarBuf := bytes.Buffer{} + dataTar := tar.NewWriter(&dataTarBuf) + dataTar.WriteHeader(&tar.Header{Name: "random.txt", Size: int64(len(data))}) + dataTar.Write(data) + dataTar.Close() + + dataTGZBuf := bytes.Buffer{} + dataGZ, _ := gzip.NewWriterLevel(&dataTGZBuf, gzip.BestCompression) + dataGZ.Write(dataTarBuf.Bytes()) + dataGZ.Close() + + datahash := sha256.Sum256(dataTGZBuf.Bytes()) + + ctlData := strings.Builder{} + ctlData.WriteString("name = " + randomRpmSuffix()) + ctlData.WriteRune('\n') + ctlData.WriteString("datahash = " + hex.EncodeToString(datahash[:])) + ctlData.WriteRune('\n') + ctlTarBuf := bytes.Buffer{} + ctlTar := tar.NewWriter(&ctlTarBuf) + ctlTar.WriteHeader(&tar.Header{Name: ".PKGINFO", Size: int64(ctlData.Len())}) + ctlTar.Write([]byte(ctlData.String())) + ctlTar.Flush() + // do not close so uncompressed stream appears as contiguous tar archive + + ctlTGZBuf := bytes.Buffer{} + ctlGZ, _ := gzip.NewWriterLevel(&ctlTGZBuf, gzip.BestCompression) + ctlGZ.Write(ctlTarBuf.Bytes()) + ctlGZ.Close() + + sha1sum := sha1.Sum(ctlTGZBuf.Bytes()) + sig, _ := rsa.SignPKCS1v15(crand.Reader, certPrivateKey, crypto.SHA1, sha1sum[:]) + + sigTarBuf := bytes.Buffer{} + sigTar := tar.NewWriter(&sigTarBuf) + sigTar.WriteHeader(&tar.Header{Name: ".SIGN.RSA.fixed.pub", Size: int64(len(sig))}) + sigTar.Write(sig) + sigTar.Flush() + // do not close so uncompressed stream appears as contiguous tar archive + + sigTGZBuf := bytes.Buffer{} + sigGZ, _ := gzip.NewWriterLevel(&sigTGZBuf, gzip.BestCompression) + sigGZ.Write(sigTarBuf.Bytes()) + sigGZ.Close() + + apkBuf := bytes.Buffer{} + if _, err := io.Copy(&apkBuf, io.MultiReader(&sigTGZBuf, &ctlTGZBuf, &dataTGZBuf)); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(artifactPath, apkBuf.Bytes(), 777); err != nil { + t.Fatal(err) + } +} diff --git a/tests/e2e_test.go b/tests/e2e_test.go index e6a2f4905..1126929f0 100644 --- a/tests/e2e_test.go +++ b/tests/e2e_test.go @@ -295,6 +295,24 @@ func TestJAR(t *testing.T) { outputContains(t, out, "Entry already exists") } +func TestAPK(t *testing.T) { + td := t.TempDir() + artifactPath := filepath.Join(td, "artifact.apk") + + createSignedApk(t, artifactPath) + + pubPath := filepath.Join(t.TempDir(), "pubKey.asc") + if err := ioutil.WriteFile(pubPath, []byte(pubKey), 0644); err != nil { + t.Fatal(err) + } + + // If we do it twice, it should already exist + out := runCli(t, "upload", "--artifact", artifactPath, "--type", "alpine", "--public-key", pubPath) + outputContains(t, out, "Created entry at") + out = runCli(t, "upload", "--artifact", artifactPath, "--type", "alpine", "--public-key", pubPath) + outputContains(t, out, "Entry already exists") +} + func TestIntoto(t *testing.T) { td := t.TempDir() attestationPath := filepath.Join(td, "attestation.json") diff --git a/tests/test_alpine.apk b/tests/test_alpine.apk new file mode 100644 index 0000000000000000000000000000000000000000..272ac3f874495975d4fa597373d930e898066818 GIT binary patch literal 2874 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 Y?b@#G+OF-|t}kf)8^BeR`~WBb02Q5_ng9R* literal 0 HcmV?d00001 diff --git a/tests/test_alpine.pub b/tests/test_alpine.pub new file mode 100644 index 000000000..bb4bdc80f --- /dev/null +++ b/tests/test_alpine.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1yHJxQgsHQREclQu4Ohe +qxTxd1tHcNnvnQTu/UrTky8wWvgXT+jpveroeWWnzmsYlDI93eLI2ORakxb3gA2O +Q0Ry4ws8vhaxLQGC74uQR5+/yYrLuTKydFzuPaS1dK19qJPXB8GMdmFOijnXX4SA +jixuHLe1WW7kZVtjL7nufvpXkWBGjsfrvskdNA/5MfxAeBbqPgaq0QMEfxMAn6/R +L5kNepi/Vr4S39Xvf2DzWkTLEK8pcnjNkt9/aafhWqFVW7m3HCAII6h/qlQNQKSo +GuH34Q8GsFG30izUENV9avY7hSLq7nggsvknlNBZtFUcmGoQrtx3FmyYsIC8/R+B +ywIDAQAB +-----END PUBLIC KEY-----