Skip to content

Commit

Permalink
Add ec.oci.blob rego function
Browse files Browse the repository at this point in the history
Resolves #1070

Signed-off-by: Luiz Carvalho <lucarval@redhat.com>
  • Loading branch information
lcarva committed Oct 24, 2023
1 parent 976befd commit 352209e
Show file tree
Hide file tree
Showing 9 changed files with 470 additions and 1 deletion.
36 changes: 36 additions & 0 deletions acceptance/examples/fetch_blob.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package blobby

import future.keywords.contains
import future.keywords.if
import future.keywords.in

# METADATA
# custom:
# short_name: spam
deny contains result if {
content := ec.oci.blob(uri)
content != "spam"
result := {
"code": "blobby.spam_success",
"msg": sprintf("Unexpected content %q for blob at %q", [content, uri])
}
}

# METADATA
# custom:
# short_name: fetchable
deny contains result if {
not ec.oci.blob(uri)
result := {
"code": "blobby.fetchable",
"msg": sprintf("Cannot fetch blob at %q", [uri])
}
}

uri := value {
# Assume the blob is on the same repo as the image
repo := split(input.image.ref, "@")[0]
# The digest of the word "spam"
digest := "sha256:4e388ab32b10dc8dbc7e28144f552830adc74787c1e2c0824032078a79f227fb"
value := sprintf("%s@%s", [repo, digest])
}
17 changes: 17 additions & 0 deletions acceptance/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,22 @@ func createAndPushImageWithLayer(ctx context.Context, imageName string, files *g
return ctx, nil
}

func createAndPushLayer(ctx context.Context, content string, imageName string) (context.Context, error) {
l := s.NewLayer([]byte(content), types.OCIUncompressedLayer)

ref, err := registry.ImageReferenceInStubRegistry(ctx, imageName)
if err != nil {
return ctx, err
}

repo, err := name.NewRepository(ref.String())
if err != nil {
return ctx, err
}

return ctx, remote.WriteLayer(repo, l)
}

func labelImage(ctx context.Context, imageName string, labels *godog.Table) (context.Context, error) {
state := testenv.FetchState[imageState](ctx)

Expand Down Expand Up @@ -979,4 +995,5 @@ func AddStepsTo(sc *godog.ScenarioContext) {
sc.Step(`^an image named "([^"]*)" with signature from "([^"]*)"$`, steal("sig"))
sc.Step(`^an image named "([^"]*)" with attestation from "([^"]*)"$`, steal("att"))
sc.Step(`^all images relating to "([^"]*)" are copied to "([^"]*)"$`, copyAllImages)
sc.Step(`^an OCI blob with content "([^"]*)" in the repo "([^"]*)"$`, createAndPushLayer)
}
83 changes: 83 additions & 0 deletions features/__snapshots__/validate_image.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3160,4 +3160,87 @@ Error: 1 error occurred:



---

[fetch OCI blob:stdout - 1]
{
"success": true,
"components": [
{
"name": "Unnamed",
"containerImage": "${REGISTRY}/acceptance/fetch-oci-blob@sha256:${REGISTRY_acceptance/fetch-oci-blob:latest_DIGEST}",
"source": {},
"successes": [
{
"msg": "Pass",
"metadata": {
"code": "blobby.fetchable"
}
},
{
"msg": "Pass",
"metadata": {
"code": "blobby.spam"
}
},
{
"msg": "Pass",
"metadata": {
"code": "builtin.attestation.signature_check"
}
},
{
"msg": "Pass",
"metadata": {
"code": "builtin.attestation.syntax_check"
}
},
{
"msg": "Pass",
"metadata": {
"code": "builtin.image.signature_check"
}
}
],
"success": true,
"signatures": [
{
"keyid": "",
"sig": "${IMAGE_SIGNATURE_acceptance/fetch-oci-blob}"
}
],
"attestations": [
{
"type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://slsa.dev/provenance/v0.2",
"predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2",
"signatures": [
{
"keyid": "",
"sig": "${ATTESTATION_SIGNATURE_acceptance/fetch-oci-blob}"
}
]
}
]
}
],
"key": "${known_PUBLIC_KEY_JSON}",
"policy": {
"sources": [
{
"policy": [
"git::https://${GITHOST}/git/fetch-oci-blob-policy.git"
]
}
],
"rekorUrl": "${REKOR}",
"publicKey": "${known_PUBLIC_KEY}"
},
"ec-version": "${EC_VERSION}",
"effective-time": "${TIMESTAMP}"
}
---

[fetch OCI blob:stderr - 1]

---
26 changes: 26 additions & 0 deletions features/validate_image.feature
Original file line number Diff line number Diff line change
Expand Up @@ -870,3 +870,29 @@ Feature: evaluate enterprise contract
When ec command is run with "validate image --image ${REGISTRY}/acceptance/image --policy acceptance/ec-policy --public-key ${known_PUBLIC_KEY} --ignore-rekor --show-successes"
Then the exit status should be 1
Then the output should match the snapshot

Scenario: fetch OCI blob
Given a key pair named "known"
Given an image named "acceptance/fetch-oci-blob"
Given a valid image signature of "acceptance/fetch-oci-blob" image signed by the "known" key
Given a valid Rekor entry for image signature of "acceptance/fetch-oci-blob"
Given a valid attestation of "acceptance/fetch-oci-blob" signed by the "known" key
Given a valid Rekor entry for attestation of "acceptance/fetch-oci-blob"
Given an OCI blob with content "spam" in the repo "acceptance/fetch-oci-blob"
Given a git repository named "fetch-oci-blob-policy" with
| main.rego | examples/fetch_blob.rego |
Given policy configuration named "ec-policy" with specification
"""
{
"sources": [
{
"policy": [
"git::https://${GITHOST}/git/fetch-oci-blob-policy.git"
]
}
]
}
"""
When ec command is run with "validate image --image ${REGISTRY}/acceptance/fetch-oci-blob --policy acceptance/ec-policy --public-key ${known_PUBLIC_KEY} --rekor-url ${REKOR} --show-successes"
Then the exit status should be 0
Then the output should match the snapshot
120 changes: 120 additions & 0 deletions internal/evaluator/rego.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright The Enterprise Contract Contributors
//
// 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.
//
// SPDX-License-Identifier: Apache-2.0

// IMPORTANT: The rego functions in this file never return an error. Instead, they return no value
// when an error is encountered. If they did return an error, opa would exit abruptly and it would
// not produce a report of which policy rules succeeded/failed.

package evaluator

import (
"bytes"
"crypto/sha256"
"fmt"
"io"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/types"
log "github.com/sirupsen/logrus"

"github.com/enterprise-contract/ec-cli/internal/fetchers/oci"
)

const ociBlobName = "ec.oci.blob"

func registerOCIBlob() {
decl := rego.Function{
Name: ociBlobName,
Decl: types.NewFunction(
types.Args(
types.Named("ref", types.S).Description("OCI blob reference"),
),
types.Named("blob", types.S).Description("the OCI blob"),
),
Memoize: true,
Nondeterministic: true,
}

rego.RegisterBuiltin1(&decl, ociBlob)
}

const maxBytes = 10 * 1024 * 1024 // 10 MB

func ociBlob(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
uri, ok := a.Value.(ast.String)
if !ok {
return nil, nil
}

Check warning on line 64 in internal/evaluator/rego.go

View check run for this annotation

Codecov / codecov/patch

internal/evaluator/rego.go#L63-L64

Added lines #L63 - L64 were not covered by tests

ref, err := name.NewDigest(string(uri))
if err != nil {
log.Errorf("%s new digest: %s", ociBlobName, err)
return nil, nil
}

Check warning on line 70 in internal/evaluator/rego.go

View check run for this annotation

Codecov / codecov/patch

internal/evaluator/rego.go#L68-L70

Added lines #L68 - L70 were not covered by tests

opts := []remote.Option{
remote.WithTransport(remote.DefaultTransport),
remote.WithContext(bctx.Context),
remote.WithAuthFromKeychain(authn.DefaultKeychain),
}

rawLayer, err := oci.NewClient(bctx.Context).Layer(ref, opts...)
if err != nil {
log.Errorf("%s fetch layer: %s", ociBlobName, err)
return nil, nil
}

Check warning on line 82 in internal/evaluator/rego.go

View check run for this annotation

Codecov / codecov/patch

internal/evaluator/rego.go#L80-L82

Added lines #L80 - L82 were not covered by tests

layer, err := rawLayer.Uncompressed()
if err != nil {
log.Errorf("%s layer uncompressed: %s", ociBlobName, err)
return nil, nil
}

Check warning on line 88 in internal/evaluator/rego.go

View check run for this annotation

Codecov / codecov/patch

internal/evaluator/rego.go#L86-L88

Added lines #L86 - L88 were not covered by tests
defer layer.Close()

// TODO: Other algorithms are technically supported, e.g. sha512. However, support for those is
// not complete in the go-containerregistry library, e.g. name.NewDigest throws an error if
// sha256 is not used. This is good for now, but may need revisiting later.
hasher := sha256.New()
// Setup some safeguards. First, use LimitReader to avoid an unbounded amount of data from being
// read. Second, use TeeReader so we can compute the digest of the content read.
reader := io.TeeReader(io.LimitReader(layer, maxBytes), hasher)

var blob bytes.Buffer
if _, err := io.Copy(&blob, reader); err != nil {
log.Errorf("%s copy buffer: %s", ociBlobName, err)
return nil, nil
}

Check warning on line 103 in internal/evaluator/rego.go

View check run for this annotation

Codecov / codecov/patch

internal/evaluator/rego.go#L101-L103

Added lines #L101 - L103 were not covered by tests

sum := fmt.Sprintf("sha256:%x", hasher.Sum(nil))
// io.LimitReader truncates the layer if it exceeds its limits. This ensures unexpected behavior
// by not returning partial data.
if sum != ref.DigestStr() {
log.Errorf(
"%s computed digest, %q, not as expected, %q. Content may have been truncated at %d bytes",
ociBlobName, sum, ref.DigestStr(), maxBytes)
return nil, nil
}

Check warning on line 113 in internal/evaluator/rego.go

View check run for this annotation

Codecov / codecov/patch

internal/evaluator/rego.go#L109-L113

Added lines #L109 - L113 were not covered by tests

return ast.StringTerm(blob.String()), nil
}

func init() {
registerOCIBlob()
}
Loading

0 comments on commit 352209e

Please sign in to comment.