diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c337300f7..2b1e923cc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -380,7 +380,25 @@ jobs: # make the registry container accessible by name from inside the cluster docker network connect kind registry.local - kapp deploy -a kpack -y -f prerelease.yaml + cat < overlay.yaml + #@ load("@ytt:overlay", "overlay") + #@overlay/match by=overlay.subset({"metadata":{"name":"kpack-controller"}, "kind": "Deployment"}) + --- + spec: + template: + spec: + containers: + #@overlay/match by="name" + - name: controller + #@overlay/match-child-defaults missing_ok=True + env: + #@overlay/match by="name" + #@overlay/replace or_add=True + - name: EXPERIMENTAL_GENERATE_SLSA_ATTESTATION + value: "true" + EOF + + ytt -f prerelease.yaml -f overlay.yaml | kapp deploy -a kpack -y -f- export IMAGE_REGISTRY=${{ env.REGISTRY_URL }} export IMAGE_REGISTRY_USERNAME=${{ env.REGISTRY_USER }} diff --git a/cmd/build-init/main.go b/cmd/build-init/main.go index c21864e54..809b6fe52 100644 --- a/cmd/build-init/main.go +++ b/cmd/build-init/main.go @@ -72,7 +72,7 @@ const ( platformDir = "/platform" buildSecretsDir = "/var/build-secrets" registrySourcePullSecretsDir = "/registrySourcePullSecrets" - projectMetadataDir = "/projectMetadata" + projectMetadataDir = "/projectMetadata" // place to write project-metadata.toml which gets exported to image label by the lifecycle networkWaitLauncherDir = "/networkWait" networkWaitLauncherBinary = "network-wait-launcher.exe" ) @@ -223,7 +223,7 @@ func fetchSource(logger *log.Logger, keychain authn.Keychain) error { fetcher := blob.Fetcher{ Logger: logger, } - return fetcher.Fetch(appDir, *blobURL, *stripComponents) + return fetcher.Fetch(appDir, *blobURL, *stripComponents, projectMetadataDir) case *registryImage != "": registrySourcePullSecrets, err := dockercreds.ParseDockerConfigSecret(registrySourcePullSecretsDir) if err != nil { @@ -235,7 +235,7 @@ func fetchSource(logger *log.Logger, keychain authn.Keychain) error { Client: ®istry.Client{}, Keychain: authn.NewMultiKeychain(registrySourcePullSecrets, keychain), } - return fetcher.Fetch(appDir, *registryImage) + return fetcher.Fetch(appDir, *registryImage, projectMetadataDir) default: return errors.New("no git url, blob url, or registry image provided") } diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 3d75fe100..90a73f24c 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -59,6 +59,7 @@ import ( "github.com/pivotal/kpack/pkg/reconciler/sourceresolver" "github.com/pivotal/kpack/pkg/registry" "github.com/pivotal/kpack/pkg/secret" + "github.com/pivotal/kpack/pkg/slsa" ) const ( @@ -85,11 +86,14 @@ func main() { flag.StringVar(&images.CompletionWindowsImage, "completion-windows-image", os.Getenv("COMPLETION_WINDOWS_IMAGE"), "The image used to finish a build on windows") flag.StringVar(&images.BuildWaiterImage, "build-waiter-image", os.Getenv("BUILD_WAITER_IMAGE"), "The image used to initialize a build") + flag.StringVar(&cfg.SystemNamespace, "system-namespace", os.Getenv("SYSTEM_NAMESPACE"), "Namespace for the the controller, this will be used to lookup secrets for image signing and attestation.") + flag.StringVar(&cfg.SystemServiceAccount, "system-service-account", os.Getenv("SYSTEM_SERVICE_ACCOUNT"), "Service account for the the controller, this will be used to lookup secrets for image signing and attestation.") flag.BoolVar(&cfg.EnablePriorityClasses, "enable-priority-classes", flaghelpers.GetEnvBool("ENABLE_PRIORITY_CLASSES", false), "if set to true, enables different pod priority classes for normal builds and automated builds") flag.StringVar(&cfg.MaximumPlatformApiVersion, "maximum-platform-api-version", os.Getenv("MAXIMUM_PLATFORM_API_VERSION"), "The maximum allowed platform api version a build can utilize") flag.BoolVar(&cfg.SshTrustUnknownHosts, "insecure-ssh-trust-unknown-hosts", flaghelpers.GetEnvBool("INSECURE_SSH_TRUST_UNKNOWN_HOSTS", true), "if set to true, automatically trust unknown hosts when using git ssh source") flag.BoolVar(&featureFlags.InjectedSidecarSupport, "injected-sidecar-support", flaghelpers.GetEnvBool("INJECTED_SIDECAR_SUPPORT", false), "if set to true, all builds will execute in standard containers instead of init containers to support injected sidecars") + flag.BoolVar(&featureFlags.GenerateSlsaAttestation, "experimental-generate-slsa-attestation", flaghelpers.GetEnvBool("EXPERIMENTAL_GENERATE_SLSA_ATTESTATION", false), "if set to true, SLSA attestations will be generated for each build") flag.Parse() @@ -205,9 +209,24 @@ func main() { K8sClient: k8sClient, } - secretFetcher := &secret.Fetcher{Client: k8sClient} + slsaAttester := slsa.Attester{ + Version: cmd.Version, - buildController := build.NewController(ctx, options, k8sClient, buildInformer, podInformer, metadataRetriever, buildpodGenerator, podProgressLogger, keychainFactory, featureFlags.InjectedSidecarSupport) + LifecycleProvider: lifecycleProvider, + ImageReader: slsa.NewImageReader(®istry.Client{}), + + Images: images, + Features: featureFlags, + Config: cfg, + } + + secretFetcher := &secret.Fetcher{ + Client: k8sClient, + SystemNamespace: cfg.SystemNamespace, + SystemServiceAccountName: cfg.SystemServiceAccount, + } + + buildController := build.NewController(ctx, options, k8sClient, buildInformer, podInformer, metadataRetriever, buildpodGenerator, podProgressLogger, keychainFactory, &slsaAttester, secretFetcher, featureFlags) imageController := image.NewController(ctx, options, k8sClient, imageInformer, buildInformer, duckBuilderInformer, sourceResolverInformer, pvcInformer, cfg.EnablePriorityClasses) sourceResolverController := sourceresolver.NewController(ctx, options, sourceResolverInformer, gitResolver, blobResolver, registryResolver) builderController, builderResync := builder.NewController(ctx, options, builderInformer, builderCreator, keychainFactory, clusterStoreInformer, buildpackInformer, clusterBuildpackInformer, clusterStackInformer, secretFetcher) diff --git a/config/controller.yaml b/config/controller.yaml index a5d610a86..550efe6c4 100644 --- a/config/controller.yaml +++ b/config/controller.yaml @@ -98,6 +98,8 @@ spec: value: "false" - name: INJECTED_SIDECAR_SUPPORT value: "false" + - name: EXPERIMENTAL_GENERATE_SLSA_ATTESTATION + value: "false" - name: INSECURE_SSH_TRUST_UNKNOWN_HOSTS value: "true" - name: CONFIG_LOGGING_NAME @@ -110,6 +112,8 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace + - name: SYSTEM_SERVICE_ACCOUNT + value: controller - name: BUILD_INIT_IMAGE valueFrom: configMapKeyRef: diff --git a/config/controllerrole.yaml b/config/controllerrole.yaml index 61e5b2af0..b1e0bd264 100644 --- a/config/controllerrole.yaml +++ b/config/controllerrole.yaml @@ -46,6 +46,7 @@ rules: resources: - secrets - pods/log + - namespaces verbs: - get - apiGroups: diff --git a/docs/slsa.md b/docs/slsa.md new file mode 100644 index 000000000..7df66f671 --- /dev/null +++ b/docs/slsa.md @@ -0,0 +1,240 @@ +# SLSA attestations + +Kpack supports generating a [SLSA v1 provenance](https://slsa.dev/spec/v1.0/provenance) with each Build. These +attestations are written to the same registry as the app image and uses the same tag-based discovery mechanism as +[cosign](https://github.com/sigstore/cosign) for linking an image digest to an attestation image tag. + +If enabled, an attestation will be generated for every newly completed [Build](./build.md) in the cluster. Kpack will +search through the secrets attached to the Build's service account, as well as the kpack-controller's service account +for signing keys. If at least one signing key is found, the attestation will be signed by all the keys. Otherwise an +unsigned attestation will be generated. + +## Configuration + +SLSA attestation can be enabled or disabled at the cluster level using the `EXPERIMENTAL_GENERATE_SLSA_ATTESTATION` +environment variable in the [kpack-controller's deployment](../config/controller.yaml). + +## SLSA security level + +Reference: https://slsa.dev/spec/v1.0/levels + +By default, kpack provides `L0`, if SLSA attestation is enabled, it automatically achieves `L1`. For signed builds, +kpack achieves `L3` because: +- The build occurs on a Kubernetes cluster, usually this means it's on dedicated infrastructure but we won't judge you + for running your cluster on kind. (L2) +- The signing private keys are provided via Kubernetes Secret, which can use RBAC to ensure minimal access. (L2) +- Builds are run in pods which are isolated from each other via Kubernetes principles. (L3) +- The only place the private keys are used to sign the attestation become accessible on the build pod is during the + `completion` step, which is completely under the control of kpack. Even adding custom buildpacks to the Builder + wouldn't allow access to the secrets. (L3) + +## Provenance schema + +Consult the documentation for the individual builder ID. + +| Builder ID | Documentation | +|------------|---------------| +| `https://kpack.io/slsa/signed-app-build` | [slsa_build.md](./slsa_build.md) | +| `https://kpack.io/slsa/unsigned-app-build` | [slsa_build.md](./slsa_build.md) | + +## Attestation storage + +Attestations in kpack are attached to image digests and attests to the build environment of that particular image. As +such, the attestations are stored in a way that is predictable given an (app) image's digest. This is the same approach +that cosign uses and means the cosign CLI can be used to [verify kpack attestations](#verification-methods). + +### Cosign tag-based discovery + +Kpack attestations uses cosign's [tag-based +discovery](https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#tag-based-discovery) with the only +difference that the suffix is `.att` instead of `.sig` (this also how `cosign attest` works). For an image digest +`registry.com/my/repo@sha256:1234`, the corresponding attestation will be uploaded to +`registry.com/my/repo:sha256-1234.att`. + + +### Storage format + +The SLSA v1 _provenance_ is stored as a _predicate_ in an in-toto _statement_ which is base64 encoded and part of a DSSE +_envelope_. The envelope looks something like: + +```json +{ + "payloadType": "application/vnd.in-toto+json", + "payload": BASE64ENCODE({ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": APP_IMAGE, + "digest": { + "sha256": APP_IMAGE_DIGEST + } + } + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": SLSA V1 provenance..., + ], + }) + "signatures": [ + { + "keyid": ..., + "sig": ..., + }, + ] +} +``` + +The envelope is stored as uncompressed text in the first layer of the attestation image. The image (and the registry) +is treated as a blobstore and isn't intended to be a container image. That is, `docker pull $ATTESTATION_TAG` or +trying to run the image in any way will **not** work. + +If you want to access the attestation, you must use one of the tools that interact with the registry directly. + +All of the following examples assume you have [jq](https://jqlang.github.io/jq/) installed. Given an `IMAGE_DIGEST` +`registry.com/my/repo@sha256:1234`, the `ATTESTATION_TAG` would be `registry.com/my/repo:sha256-1234.att` + +The easiest way is to use [cosign](https://github.com/sigstore/cosign/blob/main/doc/cosign.md): +```bash +cosign download attestation $IMAGE_DIGEST | jq -r '.payload' | base64 --decode | jq +``` + +Another supported way is via [crane](https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md): +```bash +crane export $ATTESTATION_TAG | jq -r '.payload' | base64 --decode | jq +``` + +It's also accessible by [skopeo](https://github.com/containers/skopeo/blob/main/docs/skopeo.1.md), abeit with quite a +few more steps: +```bash +dir=$(mktemp -d) +skopeo copy docker://$ATTESTATION_TAG dir:$dir +sha=$(jq -r '.layers[0].digest | sub("^sha256:"; "")' $dir/manifest.json) +jq -r '.payload' $dir/$sha | base64 --decode | jq +rm -r $dir +``` + +## Signing keys + +Build specific signing keys can be attached to the Service Account used for the Build. Cluster-wide signing keys can be +attached to the Service Account used in the `kpack-controller` Deployment in the system namespace (ususally `kpack`). + +### PKCS#8 private key + +A PKCS#8 private key using RSA, ECDSA, or ED25519 and stored in PEM format can be used to sign attestations. The private +key must use the same format as the [Kubernetes SSH auth secret](https://kubernetes.io/docs/concepts/configuration/secret/#ssh-authentication-secrets) +and have the `kpack.io/slsa: ""` annotation. Private keys with passwords are currently not supported. + +``` yaml +apiVersion: v1 +kind: Secret +type: kubernetes.io/ssh-auth +metadata: + name: my-ecdsa-key + annotations: + kpack.io/slsa: "" +stringData: + ssh-privatekey: | + -----BEGIN PRIVATE KEY----- + + -----END PRIVATE KEY----- +``` + +### Cosign private key + +A [cosign generated secret](https://github.com/sigstore/cosign/blob/main/doc/cosign_generate-key-pair.md) may also be +used as long as it has the `kpack.io/slsa: ""` annotation. Private keys with passwords are currently not supported. + +```yaml +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: my-cosign-secret + annotations: + kpack.io/slsa: "" +data: + cosign.key: + cosign.password: + cosign.pub: +``` + +### Verification methods + +A single signature consists of a `keyid` and a `sig` field where the `keyid` is the name of the Kubernetes Secret used +to generate the signature and the `sig` is the base64 encoded signature. The attestation will contain an array of these +signatures: + +```json +{ + "payloadType": ..., + "payload": ..., + "signatures": [ + { + "keyid": "cosign-secret", + "sig": "MEQCID8QIkYOqxkPcE/bazsSDRj9vJSOXk9esFJSaj07jn2DAiB9/hrt8Ezd17UFYdaMSmMLzuF1oGSzK1vQ8jz5VSHNCQ==" + }, + { + "keyid": "rsa-secret", + "sig": "s8NjZ7b7l0lGkJBeREJ9pP7kehXZWSY46413r06SIdVJbDxwgRlmF3HhK8Ji629yJs1jVLUgusBvexAM3ck+ZSzXOoOmT2sgLlvSNatF0F4iOJVA4/MFFYHOZokpObDZ/XDKC9DP8sI++x8gLhOvcPs7p/PtGXXnEJzOoedrHGV17Q1OOLIDPGkYP/CA+u0OANaAbipmaUUq7gY+E9JVKuSxHG91N9qzzvhl+dAIkbSruxMkhHkdA72OpYohKZ+Q0h+ChPI7XLrKJBKj5fBB4oOCE2a6+trKeBAwWAnlZDCN8wOWj602slQSCHpSqO9oi/u7X9aLCfhUsCZ5luY3iQ==" + }, + { + "keyid": "ecdsa-secret", + "sig": "MEUCIQDEnkmqxb9ypLDIC+9oz7i5U22Tgq71YMVTf2tIuk+ubwIgZZfpAjLe8iW2Rp50PZz7DcUYvLGeG1NAMmGRlujy9S0=" + }, + { + "keyid": "ed25519-secret", + "sig": "WPGuhBYBlempQVC5BeULFeilJr3avQicH4MjruWsc8tUwL8dHgHxcONH6nNacRV9hKHO8wRJOSGs0Eot47aBDQ==" + } + ] +} +``` + +#### Cosign + +To verify a cosign key, you can use the `cosign verify-attestation` command. This command will go through all the +signatures and verify at least one of them is signed by the public key. If you have access to the Kubernetes Namespace +(`$SECRET_NAMESPACE`) and Secret (`$SECRET_NAME`) containing the public-private keypair, you can use: + +```bash +cosign verify-attestation --insecure-ignore-tlog=true --key k8s://$SECRET_NAMESPACE/$SECRET_NAME --type=slsaprovenance1 $APP_IMAGE_DIGEST +``` + +If you only have access to the file containing the public key (`$PUB_KEY_PATH`), you can use: + +```bash +cosign verify-attestation --insecure-ignore-tlog=true --key $PUB_KEY_PATH --type=slsaprovenance1 $APP_IMAGE_DIGEST +``` + +#### PKCS#8 + +If you want to verify attestations signed by a PKCS#8 key (RSA, ECDSA, ED25519): + +1. Grab and decode the base64 encoded payload from the attestation using one of the methods from [Storage format](#storage-format). +1. Compute the [DSSE PAE](https://github.com/secure-systems-lab/dsse/blob/v1.0.0/protocol.md) using `application/vnd.in-toto+json` as the type. + This basically means filling in `DSSEv1 28 application/vnd.in-toto+json $NUM_BYTES_IN_PAYLOAD $PAYLOAD` +1. Grab and decode the base64 encoded signature you want to verify from the attestation. +1. Use `openssl` to verify the signature is correct for the PAE. + +In practice this looks something like: + +```bash +# Get attestation +ATTESTATION="$(cosign download attestation $APP_IMAGE_DIGEST)" +# Parse payload +PAYLOAD="$(echo $ATTESTATION | jq -r '.payload' | base64 --decode)" +# Parse signature, note: if you used multiple signing keys you will need to figure out which signature is from the key +# you want. Kpack does not provide any guranatees on the ordering used for signing. +echo $ATTESTATION | jq -r '.signatures[0].sig' | base64 --decode > message.sig +# Compute the PAE as message +echo -n $PAYLOAD | awk '{printf "DSSEv1 28 application/vnd.in-toto+json %d %s", length($0), $0}' > message.txt +``` + +To use a RSA or ECDSA key stored in PKCS#8 format, it must be verified against the SHA256 digest of the PAE: + +``` +openssl dgst -sha256 -binary message.txt | openssl pkeyutl -verify -pubin -inkey $PUB_KEY_PATH -pkeyopt digest:sha256 -sigfile message.sig +``` + +To use an ED25519 key stored in PKCS#8 public key, it can be verified directly against the PAE: + +``` +openssl pkeyutl -verify -pubin -inkey $PUB_KEY_PATH -sigfile message.sig -rawin -in message.txt +``` diff --git a/docs/slsa_build.md b/docs/slsa_build.md new file mode 100644 index 000000000..6203b9db7 --- /dev/null +++ b/docs/slsa_build.md @@ -0,0 +1,273 @@ +# Build Type: kpack Build resource + +``` +"buildType": "https://github.com/buildpacks-community/kpack/blob/$RELEASE/docs/slsa.md" +``` + +The first kpack release that supports SLSA is `v0.13.0`. + +## Build Definition + +### External Parameters + +The external parameters section is identical to the `.spec` section of the Build resource. The full list of fields can +be found in the [Build resource's configuratin](./build.md#configuration), **only some of the most useful ones are +documented below for convenience**: + + + + + + + + + +
Parameter + Type + Description +
tags + array (string) + The list of tags that the built image was pushed to. +
serviceAccountName + string + The ServiceAcount that was used to do the build and credential lookup. +
builder.image + string + Image reference for the Cloud Native Buildpacks builder image that was used in the build. +
builder.imagePullSecrets + array (string) + A list of the names of additional Secrets that may be used to do credential lookup for image pushing. +
source + object + The source location that was used to pull the codebase, see the [Build resource docs](./build.md#source-config) for more details. +
env + array (object) + A list of name and value defining the environment variables used in the build. +
+ + +### Internal Parameters + +These are the configuration that is required to replicate the kpack installation. + + + + + + + + + + + + +
Parameter + Type + Description +
builderImage + The fully resolved digest of the builder image + The Build resource's .spec.builder.image +
buildInitImage + Image used by kpack to fetch the source code + The build-init-image ConfigMap in the kpack namespace +
buildInitWindowsImage + Image used by kpack to fetch the source code on Windows clusters + The build-init-windows-image Configmap in the kpack namespace +
buildWaterImage + Image used by kpack when injectedSidecarSupport is enabled + The build-waiter-image ConfigMap in the kpack namespace +
completionImage + Image used by kpack to do cosign and notary signing + The completion-image ConfigMapin the kpack namespace +
completionImageWindows + Image used by kpack to do cosign and notary signing on Windows clusters + The completion-image-windows ConfigMap in the kpack namespace +
rebaseImage + Image used by kpack to do rebasing of app images + The rebase-image ConfigMap in the kpack namespace +
lifecycleImage + The CNB lifecycle version that was used in the build + The lifecycle-image ConfigMap in the kpack namespace +
The rest + Feature flags and configuration values for the controller + The .spec.template.spec.containers[0].env section of the kpack-controller's deployment yaml +
+ +### Resolved Dependencies + +There will be 2 resolved dependencies: +- The source of the codebase. + - The `name` will always be `"source"` + - For `git` sources, the `uri` will be the git url and the `digest` will be `"sha1": git_sha` + - For `blob` sources, the `uri` will be the blob url and the `digest` will be `"sha256": sha256sum(blob)` + - For `registry` sources, the `uri` will be the image url and the `digest` will be `"sha256": image_digest` + +- Details about the `.spec.builder.image` + - The `name` will always be `"builder"` + - The `uri` will be the hostname + repository of the builder image + - The `digest` will be `"sha256": image_digest` + - The `annotations` will be the labels on the image + +## Run Details + +### Builder + +- The `id` will be either `https://kpack.io/slsa/signed-app-build` or `https://kpack.io/slsa/unsigned-app-build` +- The `version` field will contain `kpack` and `lifecycle` versions +- The `builderDependencies` will contain information about the ResourceVersion of the Kubernetes objects when the Build + was started + - The `name` will be the Kind of objects being recorded + - The `content` will be a base64 encoding of the Name and ResourceVersion of the object. + The following Kubernetes objects will be recorded: + - Build + - Namespace of the Build + - ServiceAccount of the Build + - An array of Secrets that were used during the build + +### Metadata + +- The `invocationID` will follow the format + `https://kpack.io/build///@` +- The `startedOn` will be the time the `prepare` step started on +- The `finishedOn` will be the time the `completion` step finished on + +### By Products + +Currently unused. + +## Examples + +
+example.json + +```json +{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v1", + "subject": [ + { + "name": "some.registry/test", + "digest": { + "sha256": "7b2373a79f5bc9c6f740b9fde14f8eb057c7a56fd4666efd271780d0119a127a" + } + } + ], + "predicate": { + "buildDefinition": { + "buildType": "https://github.com/buildpacks-community/kpack/blob/v0.0.0/docs/slsa.md", + "externalParameters": { + "tags": [ + "some.registry/test" + ], + "builder": { + "image": "index.docker.io/paketobuildpacks/builder-jammy-base" + }, + "serviceAccountName": "default", + "source": { + "git": { + "url": "https://github.com/paketo-buildpacks/samples.git", + "revision": "main" + }, + "subPath": "go/mod" + }, + "runImage": { + "image": "index.docker.io/paketobuildpacks/run-jammy-base@sha256:18c92f9d53d1b3b941624cb823df3c802782f8eb337ad3a229d14372df4cd27d" + }, + "resources": {} + }, + "internalParameters": { + "builderImage": "index.docker.io/paketobuildpacks/builder-jammy-base", + "systemNamespace": "kpack", + "systemServiceAccount": "controller", + "enablePriorityClasses": false, + "maximumPlatformApiVersion": "", + "sshTrustUnknownHosts": true, + "buildInitImage": "some.registry/build-init@sha256:cd8b6c8eecb79c0aee725ef00bac5d502df6fdfeaab8d6a2a5854f3a38445ac7", + "buildInitWindowsImage": "build-init-windows", + "buildWaiterImage": "some.registry/build-waiter@sha256:bcca4f1e691da17de25feff47f91b6cc6443d9deee3234676c95294cd30995c1", + "completionImage": "some.registry/completion@sha256:ea058f72bf3529b292751ed1d3a2ec6e0cfab1fdfc049863553181cb19682128", + "completionWindowsImage": "completion-windows", + "rebaseImage": "some.registry/rebase@sha256:8e4c673c48861b583ca6b7af2f72fa3b4a9397d53929431d78bf47b0f456da4b", + "injectedSidecarSupport": false, + "generateSlsaAttestation": true + }, + "resolvedDependencies": [ + { + "uri": "https://github.com/paketo-buildpacks/samples.git", + "digest": { + "sha1": "bb85b84dd957ae95e04045f0d74cd018d772d432" + }, + "name": "source" + }, + { + "uri": "index.docker.io/paketobuildpacks/builder-jammy-base", + "digest": { + "sha256": "f89bbfe854a23f992e42e206af0ed8e2e380115ded9de8f1bb734da3fd116c45" + }, + "name": "builder-image", + "annotations": { + "io.buildpacks.builder.metadata": "{\"description\":\"Ubuntu 22.04 Jammy Jellyfish base image with buildpacks for Java, Go, .NET Core, Node.js, Python, Apache HTTPD, NGINX and Procfile\",\"buildpacks\":[{\"id\":\"paketo-buildpacks/dotnet-core\",\"name\":\"Paketo Buildpack for .NET Core\",\"version\":\"0.42.3\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core\"},{\"id\":\"paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\",\"version\":\"3.6.6\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\"},{\"id\":\"paketo-buildpacks/dotnet-core-aspnet-runtime\",\"name\":\"Paketo Buildpack for ASP.NET Core Runtime\",\"version\":\"0.4.2\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-aspnet-runtime\"},{\"id\":\"paketo-buildpacks/dotnet-core-sdk\",\"name\":\"Paketo Buildpack for .NET Core SDK\",\"version\":\"0.14.3\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-sdk\"},{\"id\":\"paketo-buildpacks/dotnet-execute\",\"name\":\"Paketo Buildpack for .NET Execute\",\"version\":\"0.14.26\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-execute\"},{\"id\":\"paketo-buildpacks/dotnet-publish\",\"name\":\"Paketo Buildpack for .NET Publish\",\"version\":\"0.12.25\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-publish\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"name\":\"Paketo Buildpack for Environment Variables\",\"version\":\"4.5.6\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/icu\",\"name\":\"Paketo Buildpack for ICU\",\"version\":\"0.7.3\",\"homepage\":\"https://github.com/paketo-buildpacks/icu\"},{\"id\":\"paketo-buildpacks/image-labels\",\"name\":\"Paketo Buildpack for Image Labels\",\"version\":\"4.5.5\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/node-engine\",\"name\":\"Paketo Buildpack for Node Engine\",\"version\":\"3.0.1\",\"homepage\":\"https://github.com/paketo-buildpacks/node-engine\"},{\"id\":\"paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\",\"version\":\"5.6.7\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/vsdbg\",\"name\":\"Paketo Buildpack for Visual Studio Debugger\",\"version\":\"0.3.7\"},{\"id\":\"paketo-buildpacks/watchexec\",\"name\":\"Paketo Buildpack for Watchexec\",\"version\":\"2.8.6\",\"homepage\":\"https://github.com/paketo-buildpacks/watchexec\"},{\"id\":\"paketo-buildpacks/go\",\"name\":\"Paketo Buildpack for Go\",\"version\":\"4.6.2\",\"homepage\":\"https://github.com/paketo-buildpacks/go\"},{\"id\":\"paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\",\"version\":\"3.6.6\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"name\":\"Paketo Buildpack for Environment Variables\",\"version\":\"4.5.6\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/git\",\"name\":\"Paketo Buildpack for Git\",\"version\":\"1.0.7\",\"homepage\":\"https://github.com/paketo-buildpacks/git\"},{\"id\":\"paketo-buildpacks/go-build\",\"name\":\"Paketo Buildpack for Go Build\",\"version\":\"2.1.2\",\"homepage\":\"https://github.com/paketo-buildpacks/go-build\"},{\"id\":\"paketo-buildpacks/go-dist\",\"name\":\"Paketo Buildpack for Go Distribution\",\"version\":\"2.4.4\",\"homepage\":\"https://github.com/paketo-buildpacks/go-dist\"},{\"id\":\"paketo-buildpacks/go-mod-vendor\",\"name\":\"Paketo Buildpack for Go Mod Vendor\",\"version\":\"1.0.27\",\"homepage\":\"https://github.com/paketo-buildpacks/go-mod-vendor\"},{\"id\":\"paketo-buildpacks/image-labels\",\"name\":\"Paketo Buildpack for Image Labels\",\"version\":\"4.5.5\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\",\"version\":\"5.6.7\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/watchexec\",\"name\":\"Paketo Buildpack for Watchexec\",\"version\":\"2.8.6\",\"homepage\":\"https://github.com/paketo-buildpacks/watchexec\"},{\"id\":\"paketo-buildpacks/java-native-image\",\"name\":\"Paketo Buildpack for Java Native Image\",\"version\":\"8.25.0\",\"homepage\":\"https://paketo.io/docs/howto/java/#build-an-app-as-a-graalvm-native-image-application\"},{\"id\":\"paketo-buildpacks/bellsoft-liberica\",\"name\":\"Paketo Buildpack for BellSoft Liberica\",\"version\":\"10.4.4\",\"homepage\":\"https://github.com/paketo-buildpacks/bellsoft-liberica\"},{\"id\":\"paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\",\"version\":\"3.6.7\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\"},{\"id\":\"paketo-buildpacks/datadog\",\"name\":\"Paketo Buildpack for Datadog\",\"version\":\"4.8.0\",\"homepage\":\"https://github.com/paketo-buildpacks/datadog\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"name\":\"Paketo Buildpack for Environment Variables\",\"version\":\"4.5.6\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/executable-jar\",\"name\":\"Paketo Buildpack for Executable JAR\",\"version\":\"6.8.3\",\"homepage\":\"https://github.com/paketo-buildpacks/executable-jar\"},{\"id\":\"paketo-buildpacks/gradle\",\"name\":\"Paketo Buildpack for Gradle\",\"version\":\"7.7.1\",\"homepage\":\"https://github.com/paketo-buildpacks/gradle\"},{\"id\":\"paketo-buildpacks/image-labels\",\"name\":\"Paketo Buildpack for Image Labels\",\"version\":\"4.5.5\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/leiningen\",\"name\":\"Paketo Buildpack for Leiningen\",\"version\":\"4.6.9\",\"homepage\":\"https://github.com/paketo-buildpacks/leiningen\"},{\"id\":\"paketo-buildpacks/maven\",\"name\":\"Paketo Buildpack for Maven\",\"version\":\"6.15.12\",\"homepage\":\"https://github.com/paketo-buildpacks/maven\"},{\"id\":\"paketo-buildpacks/native-image\",\"name\":\"Paketo Buildpack for Native Image\",\"version\":\"5.12.7\",\"homepage\":\"https://github.com/paketo-buildpacks/native-image\"},{\"id\":\"paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\",\"version\":\"5.6.8\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/sbt\",\"name\":\"Paketo Buildpack for SBT\",\"version\":\"6.12.11\",\"homepage\":\"https://github.com/paketo-buildpacks/sbt\"},{\"id\":\"paketo-buildpacks/spring-boot\",\"name\":\"Paketo Buildpack for Spring Boot\",\"version\":\"5.27.8\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot\"},{\"id\":\"paketo-buildpacks/syft\",\"name\":\"Paketo Buildpack for Syft\",\"version\":\"1.42.0\",\"homepage\":\"https://github.com/paketo-buildpacks/syft\"},{\"id\":\"paketo-buildpacks/upx\",\"name\":\"Paketo Buildpack for UPX\",\"version\":\"3.4.7\",\"homepage\":\"https://github.com/paketo-buildpacks/upx\"},{\"id\":\"paketo-buildpacks/java\",\"name\":\"Paketo Buildpack for Java\",\"version\":\"10.6.0\",\"homepage\":\"https://paketo.io/docs/howto/java\"},{\"id\":\"paketo-buildpacks/apache-tomcat\",\"name\":\"Paketo Buildpack for Apache Tomcat\",\"version\":\"7.14.2\",\"homepage\":\"https://github.com/paketo-buildpacks/apache-tomcat\"},{\"id\":\"paketo-buildpacks/apache-tomee\",\"name\":\"Paketo Buildpack for Apache Tomee\",\"version\":\"1.8.0\",\"homepage\":\"https://github.com/paketo-buildpacks/apache-tomee\"},{\"id\":\"paketo-buildpacks/azure-application-insights\",\"name\":\"Paketo Buildpack for Azure Application Insights\",\"version\":\"5.17.3\",\"homepage\":\"https://github.com/paketo-buildpacks/azure-application-insights\"},{\"id\":\"paketo-buildpacks/bellsoft-liberica\",\"name\":\"Paketo Buildpack for BellSoft Liberica\",\"version\":\"10.4.4\",\"homepage\":\"https://github.com/paketo-buildpacks/bellsoft-liberica\"},{\"id\":\"paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\",\"version\":\"3.6.7\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\"},{\"id\":\"paketo-buildpacks/clojure-tools\",\"name\":\"Paketo Buildpack for Clojure Tools\",\"version\":\"2.8.14\",\"homepage\":\"https://github.com/paketo-buildpacks/clojure-tools\"},{\"id\":\"paketo-buildpacks/datadog\",\"name\":\"Paketo Buildpack for Datadog\",\"version\":\"4.8.0\",\"homepage\":\"https://github.com/paketo-buildpacks/datadog\"},{\"id\":\"paketo-buildpacks/dist-zip\",\"name\":\"Paketo Buildpack for DistZip\",\"version\":\"5.6.8\",\"homepage\":\"https://github.com/paketo-buildpacks/dist-zip\"},{\"id\":\"paketo-buildpacks/encrypt-at-rest\",\"name\":\"Paketo Buildpack for Encrypt-at-Rest\",\"version\":\"4.5.13\",\"homepage\":\"https://github.com/paketo-buildpacks/encrypt-at-rest\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"name\":\"Paketo Buildpack for Environment Variables\",\"version\":\"4.5.6\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/executable-jar\",\"name\":\"Paketo Buildpack for Executable JAR\",\"version\":\"6.8.3\",\"homepage\":\"https://github.com/paketo-buildpacks/executable-jar\"},{\"id\":\"paketo-buildpacks/google-stackdriver\",\"name\":\"Paketo Buildpack for Google Stackdriver\",\"version\":\"8.0.4\",\"homepage\":\"https://github.com/paketo-buildpacks/google-stackdriver\"},{\"id\":\"paketo-buildpacks/gradle\",\"name\":\"Paketo Buildpack for Gradle\",\"version\":\"7.7.1\",\"homepage\":\"https://github.com/paketo-buildpacks/gradle\"},{\"id\":\"paketo-buildpacks/image-labels\",\"name\":\"Paketo Buildpack for Image Labels\",\"version\":\"4.5.5\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/jattach\",\"name\":\"Paketo Buildpack for JAttach\",\"version\":\"1.4.9\",\"homepage\":\"https://github.com/paketo-buildpacks/jattach\"},{\"id\":\"paketo-buildpacks/java-memory-assistant\",\"name\":\"Paketo Buildpack for Java Memory Assistant\",\"version\":\"1.4.9\",\"homepage\":\"https://github.com/paketo-buildpacks/java-memory-assistant\"},{\"id\":\"paketo-buildpacks/leiningen\",\"name\":\"Paketo Buildpack for Leiningen\",\"version\":\"4.6.9\",\"homepage\":\"https://github.com/paketo-buildpacks/leiningen\"},{\"id\":\"paketo-buildpacks/liberty\",\"name\":\"Paketo Buildpack for Liberty\",\"version\":\"3.8.12\",\"homepage\":\"https://github.com/paketo-buildpacks/liberty\"},{\"id\":\"paketo-buildpacks/maven\",\"name\":\"Paketo Buildpack for Maven\",\"version\":\"6.15.12\",\"homepage\":\"https://github.com/paketo-buildpacks/maven\"},{\"id\":\"paketo-buildpacks/node-engine\",\"name\":\"Paketo Buildpack for Node Engine\",\"version\":\"3.1.0\",\"homepage\":\"https://github.com/paketo-buildpacks/node-engine\"},{\"id\":\"paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\",\"version\":\"5.6.8\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/sbt\",\"name\":\"Paketo Buildpack for SBT\",\"version\":\"6.12.11\",\"homepage\":\"https://github.com/paketo-buildpacks/sbt\"},{\"id\":\"paketo-buildpacks/spring-boot\",\"name\":\"Paketo Buildpack for Spring Boot\",\"version\":\"5.27.8\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot\"},{\"id\":\"paketo-buildpacks/syft\",\"name\":\"Paketo Buildpack for Syft\",\"version\":\"1.42.0\",\"homepage\":\"https://github.com/paketo-buildpacks/syft\"},{\"id\":\"paketo-buildpacks/watchexec\",\"name\":\"Paketo Buildpack for Watchexec\",\"version\":\"2.8.7\",\"homepage\":\"https://github.com/paketo-buildpacks/watchexec\"},{\"id\":\"paketo-buildpacks/yarn\",\"name\":\"Paketo Buildpack for Yarn\",\"version\":\"1.2.0\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn\"},{\"id\":\"paketo-buildpacks/nodejs\",\"name\":\"Paketo Buildpack for Node.js\",\"version\":\"2.0.0\",\"homepage\":\"https://github.com/paketo-buildpacks/nodejs\"},{\"id\":\"paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\",\"version\":\"3.6.6\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\"},{\"id\":\"paketo-buildpacks/datadog\",\"name\":\"Paketo Buildpack for Datadog\",\"version\":\"3.6.0\",\"homepage\":\"https://github.com/paketo-buildpacks/datadog\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"name\":\"Paketo Buildpack for Environment Variables\",\"version\":\"4.5.6\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/image-labels\",\"name\":\"Paketo Buildpack for Image Labels\",\"version\":\"4.5.5\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/node-engine\",\"name\":\"Paketo Buildpack for Node Engine\",\"version\":\"3.0.1\",\"homepage\":\"https://github.com/paketo-buildpacks/node-engine\"},{\"id\":\"paketo-buildpacks/node-run-script\",\"name\":\"Paketo Buildpack for Node Run Script\",\"version\":\"1.0.14\",\"homepage\":\"https://github.com/paketo-buildpacks/node-run-script\"},{\"id\":\"paketo-buildpacks/node-start\",\"name\":\"Paketo Buildpack for Node Start\",\"version\":\"1.1.3\",\"homepage\":\"https://github.com/paketo-buildpacks/node-start\"},{\"id\":\"paketo-buildpacks/npm-install\",\"name\":\"Paketo Buildpack for NPM Install\",\"version\":\"1.3.1\",\"homepage\":\"https://github.com/paketo-buildpacks/npm-install\"},{\"id\":\"paketo-buildpacks/npm-start\",\"name\":\"Paketo Buildpack for NPM Start\",\"version\":\"1.0.15\",\"homepage\":\"https://github.com/paketo-buildpacks/npm-start\"},{\"id\":\"paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\",\"version\":\"5.6.7\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/watchexec\",\"name\":\"Paketo Buildpack for Watchexec\",\"version\":\"2.8.6\",\"homepage\":\"https://github.com/paketo-buildpacks/watchexec\"},{\"id\":\"paketo-buildpacks/yarn\",\"name\":\"Paketo Buildpack for Yarn\",\"version\":\"1.2.0\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn\"},{\"id\":\"paketo-buildpacks/yarn-install\",\"name\":\"Paketo Buildpack for Yarn Install\",\"version\":\"1.2.2\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn-install\"},{\"id\":\"paketo-buildpacks/yarn-start\",\"name\":\"Paketo Buildpack for Yarn Start\",\"version\":\"1.1.3\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn-start\"},{\"id\":\"paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\",\"version\":\"5.6.8\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/python\",\"name\":\"Paketo Buildpack for Python\",\"version\":\"2.14.0\"},{\"id\":\"paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\",\"version\":\"3.6.3\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\"},{\"id\":\"paketo-buildpacks/conda-env-update\",\"name\":\"Paketo Buildpack for Conda Env Update\",\"version\":\"0.7.12\",\"homepage\":\"https://github.com/paketo-buildpacks/conda-env-update\"},{\"id\":\"paketo-buildpacks/cpython\",\"name\":\"Paketo Buildpack for CPython\",\"version\":\"1.9.0\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"name\":\"Paketo Buildpack for Environment Variables\",\"version\":\"4.5.3\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/image-labels\",\"name\":\"Paketo Buildpack for Image Labels\",\"version\":\"4.5.2\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/miniconda\",\"name\":\"Paketo Buildpack for Miniconda\",\"version\":\"0.8.5\"},{\"id\":\"paketo-buildpacks/pip\",\"name\":\"Paketo Buildpack for Pip\",\"version\":\"0.18.0\"},{\"id\":\"paketo-buildpacks/pip-install\",\"name\":\"Paketo Buildpack for Pip Install\",\"version\":\"0.6.0\"},{\"id\":\"paketo-buildpacks/pipenv\",\"name\":\"Paketo Buildpack for Pipenv\",\"version\":\"1.19.0\"},{\"id\":\"paketo-buildpacks/pipenv-install\",\"name\":\"Paketo Buildpack for Pipenv Install\",\"version\":\"0.6.18\"},{\"id\":\"paketo-buildpacks/poetry\",\"name\":\"Paketo Buildpack for Poetry\",\"version\":\"0.6.5\"},{\"id\":\"paketo-buildpacks/poetry-install\",\"name\":\"Paketo Buildpack for Poetry Install\",\"version\":\"0.3.17\"},{\"id\":\"paketo-buildpacks/poetry-run\",\"name\":\"Paketo Buildpack for Poetry Run\",\"version\":\"0.4.21\"},{\"id\":\"paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\",\"version\":\"5.6.4\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/python-start\",\"name\":\"Paketo Buildpack for Python Start\",\"version\":\"0.14.14\",\"homepage\":\"https://github.com/paketo-buildpacks/python-start\"},{\"id\":\"paketo-buildpacks/watchexec\",\"name\":\"Paketo Buildpack for Watchexec\",\"version\":\"2.8.3\",\"homepage\":\"https://github.com/paketo-buildpacks/watchexec\"},{\"id\":\"paketo-buildpacks/ruby\",\"name\":\"Paketo Buildpack for Ruby\",\"version\":\"0.42.1\",\"homepage\":\"https://github.com/paketo-buildpacks/ruby\"},{\"id\":\"paketo-buildpacks/bundle-install\",\"name\":\"Paketo Buildpack for Bundle Install\",\"version\":\"0.8.1\",\"homepage\":\"https://github.com/paketo-buildpacks/bundle-install\"},{\"id\":\"paketo-buildpacks/bundler\",\"name\":\"Paketo Buildpack for Bundler\",\"version\":\"0.8.1\",\"homepage\":\"https://github.com/paketo-buildpacks/bundler\"},{\"id\":\"paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\",\"version\":\"3.6.7\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"name\":\"Paketo Buildpack for Environment Variables\",\"version\":\"4.5.6\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/image-labels\",\"name\":\"Paketo Buildpack for Image Labels\",\"version\":\"4.5.5\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/mri\",\"name\":\"Paketo Buildpack for MRI\",\"version\":\"0.14.10\",\"homepage\":\"https://github.com/paketo-buildpacks/mri\"},{\"id\":\"paketo-buildpacks/node-engine\",\"name\":\"Paketo Buildpack for Node Engine\",\"version\":\"3.0.1\",\"homepage\":\"https://github.com/paketo-buildpacks/node-engine\"},{\"id\":\"paketo-buildpacks/passenger\",\"name\":\"Paketo Buildpack for Passenger\",\"version\":\"0.13.3\",\"homepage\":\"https://github.com/paketo-buildpacks/passenger\"},{\"id\":\"paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\",\"version\":\"5.6.8\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/puma\",\"name\":\"Paketo Buildpack for Puma\",\"version\":\"0.4.37\",\"homepage\":\"https://github.com/paketo-buildpacks/puma\"},{\"id\":\"paketo-buildpacks/rackup\",\"name\":\"Paketo Buildpack for Rackup\",\"version\":\"0.4.36\",\"homepage\":\"https://github.com/paketo-buildpacks/rackup\"},{\"id\":\"paketo-buildpacks/rails-assets\",\"name\":\"Paketo Buildpack for Rails Assets\",\"version\":\"0.10.4\",\"homepage\":\"https://github.com/paketo-buildpacks/rails-assets\"},{\"id\":\"paketo-buildpacks/rake\",\"name\":\"Paketo Buildpack for Rake\",\"version\":\"0.4.36\",\"homepage\":\"https://github.com/paketo-buildpacks/rake\"},{\"id\":\"paketo-buildpacks/thin\",\"name\":\"Paketo Buildpack for Thin\",\"version\":\"0.5.36\",\"homepage\":\"https://github.com/paketo-buildpacks/thin\"},{\"id\":\"paketo-buildpacks/unicorn\",\"name\":\"Paketo Buildpack for Unicorn\",\"version\":\"0.4.36\",\"homepage\":\"https://github.com/paketo-buildpacks/unicorn\"},{\"id\":\"paketo-buildpacks/yarn\",\"name\":\"Paketo Buildpack for Yarn\",\"version\":\"1.2.0\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn\"},{\"id\":\"paketo-buildpacks/yarn-install\",\"name\":\"Paketo Buildpack for Yarn Install\",\"version\":\"1.2.2\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn-install\"},{\"id\":\"paketo-buildpacks/web-servers\",\"name\":\"Paketo Buildpack for Web Servers\",\"version\":\"0.19.1\",\"homepage\":\"https://github.com/paketo-buildpacks/web-servers\"},{\"id\":\"paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\",\"version\":\"3.6.5\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"name\":\"Paketo Buildpack for Environment Variables\",\"version\":\"4.5.5\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/httpd\",\"name\":\"Paketo Buildpack for Apache HTTP Server\",\"version\":\"0.7.14\",\"homepage\":\"https://github.com/paketo-buildpacks/httpd\"},{\"id\":\"paketo-buildpacks/image-labels\",\"name\":\"Paketo Buildpack for Image Labels\",\"version\":\"4.5.4\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/nginx\",\"name\":\"Paketo Buildpack for Nginx Server\",\"version\":\"0.15.6\",\"homepage\":\"https://github.com/paketo-buildpacks/nginx\"},{\"id\":\"paketo-buildpacks/node-engine\",\"name\":\"Paketo Buildpack for Node Engine\",\"version\":\"2.0.0\",\"homepage\":\"https://github.com/paketo-buildpacks/node-engine\"},{\"id\":\"paketo-buildpacks/node-run-script\",\"name\":\"Paketo Buildpack for Node Run Script\",\"version\":\"1.0.13\",\"homepage\":\"https://github.com/paketo-buildpacks/node-run-script\"},{\"id\":\"paketo-buildpacks/npm-install\",\"name\":\"Paketo Buildpack for NPM Install\",\"version\":\"1.3.0\",\"homepage\":\"https://github.com/paketo-buildpacks/npm-install\"},{\"id\":\"paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\",\"version\":\"5.6.6\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/source-removal\",\"name\":\"Paketo Buildpack for Source Removal\",\"version\":\"0.2.1\"},{\"id\":\"paketo-buildpacks/watchexec\",\"name\":\"Paketo Buildpack for Watchexec\",\"version\":\"2.8.5\",\"homepage\":\"https://github.com/paketo-buildpacks/watchexec\"},{\"id\":\"paketo-buildpacks/yarn\",\"name\":\"Paketo Buildpack for Yarn\",\"version\":\"1.1.11\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn\"},{\"id\":\"paketo-buildpacks/yarn-install\",\"name\":\"Paketo Buildpack for Yarn Install\",\"version\":\"1.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn-install\"}],\"extensions\":null,\"stack\":{\"runImage\":{\"image\":\"index.docker.io/paketobuildpacks/run-jammy-base:latest\",\"mirrors\":[]}},\"lifecycle\":{\"version\":\"0.18.3\",\"api\":{\"buildpack\":\"0.7\",\"platform\":\"0.7\"},\"apis\":{\"buildpack\":{\"deprecated\":[],\"supported\":[\"0.7\",\"0.8\",\"0.9\",\"0.10\"]},\"platform\":{\"deprecated\":[],\"supported\":[\"0.7\",\"0.8\",\"0.9\",\"0.10\",\"0.11\",\"0.12\"]}}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"0.31.0+git-3a994bd.build-5086\"},\"images\":[{\"image\":\"index.docker.io/paketobuildpacks/run-jammy-base:latest\",\"mirrors\":[]}]}", + "io.buildpacks.buildpack.layers": "{\"paketo-buildpacks/apache-tomcat\":{\"7.14.2\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:ce1b165e67eaa0fca17d2018209683525320817d4b6ebfc8018ae2304bfa29c6\",\"homepage\":\"https://github.com/paketo-buildpacks/apache-tomcat\",\"name\":\"Paketo Buildpack for Apache Tomcat\"}},\"paketo-buildpacks/apache-tomee\":{\"1.8.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:e1efe0e0fe75007d1b985bc55d4b2ca25a71cdf06ce0a942c9bb0d11c8181ea8\",\"homepage\":\"https://github.com/paketo-buildpacks/apache-tomee\",\"name\":\"Paketo Buildpack for Apache Tomee\"}},\"paketo-buildpacks/azure-application-insights\":{\"5.17.3\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:e5237d5d688156b0e68f1b0022afff8cb7a43bd8618fec19820a14675d7225fa\",\"homepage\":\"https://github.com/paketo-buildpacks/azure-application-insights\",\"name\":\"Paketo Buildpack for Azure Application Insights\"}},\"paketo-buildpacks/bellsoft-liberica\":{\"10.4.4\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:07de586e9118a13b87e3bb0326af1de2d04254f2e7e7e4ca99d4730de69c44b1\",\"homepage\":\"https://github.com/paketo-buildpacks/bellsoft-liberica\",\"name\":\"Paketo Buildpack for BellSoft Liberica\"}},\"paketo-buildpacks/bundle-install\":{\"0.8.1\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:27847f75caee85243a98a9e937270ec17559d1e5a63ad682b03bf123bdcca2bf\",\"homepage\":\"https://github.com/paketo-buildpacks/bundle-install\",\"name\":\"Paketo Buildpack for Bundle Install\"}},\"paketo-buildpacks/bundler\":{\"0.8.1\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"}],\"layerDiffID\":\"sha256:62d4fe09302f024a37c821da944feec4680949dd1bd098278ab864d70895fd97\",\"homepage\":\"https://github.com/paketo-buildpacks/bundler\",\"name\":\"Paketo Buildpack for Bundler\"}},\"paketo-buildpacks/ca-certificates\":{\"3.6.3\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:b60541d13f4fa3fddd3783a1cb77de4c502e590a34c0265becc0d1819ea0f664\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\"},\"3.6.5\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:d7b5fe48f8aec15ae831663443e85d27cc39f6e7f677f3574d22efc12ab1b91d\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\"},\"3.6.6\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:e987b55729a6c11809fd034067ca16b6713bc108deafb7720b4c8042c04bfb6d\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\"},\"3.6.7\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:09958ee88d9b92eb8cac51041ca0b7e26ea4729d6c21cb4c4b0e73d1a03d97f2\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\",\"name\":\"Paketo Buildpack for CA Certificates\"}},\"paketo-buildpacks/clojure-tools\":{\"2.8.14\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:1b0ba4a1fbe2f120bc41c43c65698e6ece432946a93ecf5dca1ab901e6f5a1be\",\"homepage\":\"https://github.com/paketo-buildpacks/clojure-tools\",\"name\":\"Paketo Buildpack for Clojure Tools\"}},\"paketo-buildpacks/conda-env-update\":{\"0.7.12\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:e8f09369eccaf0ce87f8f4868129ff29911f8311ae9650a5f73c90c3abb46ef5\",\"homepage\":\"https://github.com/paketo-buildpacks/conda-env-update\",\"name\":\"Paketo Buildpack for Conda Env Update\"}},\"paketo-buildpacks/cpython\":{\"1.9.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:5757cf2c2d7f21c2253c556582fb1c802eaabe53d10d86afe92bd1be2c9b3636\",\"name\":\"Paketo Buildpack for CPython\"}},\"paketo-buildpacks/datadog\":{\"3.6.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:b351bc73239608a09c7523656594df065fd2a6aacaee90254945d9e3070122ca\",\"homepage\":\"https://github.com/paketo-buildpacks/datadog\",\"name\":\"Paketo Buildpack for Datadog\"},\"4.8.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:e85973d2cb9437b6fdce9da62c3c200b11df38522dcded9d4786cd998f1c2ee6\",\"homepage\":\"https://github.com/paketo-buildpacks/datadog\",\"name\":\"Paketo Buildpack for Datadog\"}},\"paketo-buildpacks/dist-zip\":{\"5.6.8\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:b5beba4f9e9f9235a1982e02e9441f4974e94bb0a4a218de5a77abdfbd88f723\",\"homepage\":\"https://github.com/paketo-buildpacks/dist-zip\",\"name\":\"Paketo Buildpack for DistZip\"}},\"paketo-buildpacks/dotnet-core\":{\"0.42.3\":{\"api\":\"0.8\",\"order\":[{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/vsdbg\",\"version\":\"0.3.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/dotnet-core-sdk\",\"version\":\"0.14.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/icu\",\"version\":\"0.7.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"3.0.1\",\"optional\":true},{\"id\":\"paketo-buildpacks/dotnet-publish\",\"version\":\"0.12.25\",\"optional\":true},{\"id\":\"paketo-buildpacks/dotnet-core-aspnet-runtime\",\"version\":\"0.4.2\",\"optional\":true},{\"id\":\"paketo-buildpacks/dotnet-execute\",\"version\":\"0.14.26\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]}],\"layerDiffID\":\"sha256:221c5a3d0d6de36c52462c58a73c67c8e6ec5d61e81e5e1960fc2587480157b0\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core\",\"name\":\"Paketo Buildpack for .NET Core\"}},\"paketo-buildpacks/dotnet-core-aspnet-runtime\":{\"0.4.2\":{\"api\":\"0.8\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"}],\"layerDiffID\":\"sha256:49961a424533b2e65d3026686d01640a32fe17f7cfc1a99494f504ba0aebdcb5\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-aspnet-runtime\",\"name\":\"Paketo Buildpack for ASP.NET Core Runtime\"}},\"paketo-buildpacks/dotnet-core-sdk\":{\"0.14.3\":{\"api\":\"0.8\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"}],\"layerDiffID\":\"sha256:28963f03b59fc92c20606176dcaebcd218e4918a79b4bb9dd4a36c67df0a2f5b\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-sdk\",\"name\":\"Paketo Buildpack for .NET Core SDK\"}},\"paketo-buildpacks/dotnet-execute\":{\"0.14.26\":{\"api\":\"0.8\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:69a7b7c3c54b5466f263ba3affe9e30b730185be7fea40f7508ca3a444ba4ece\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-execute\",\"name\":\"Paketo Buildpack for .NET Execute\"}},\"paketo-buildpacks/dotnet-publish\":{\"0.12.25\":{\"api\":\"0.8\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:4253a9340935dde582d03afe3e78e853b35f0a95676b8d97db06213c382abfcc\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-publish\",\"name\":\"Paketo Buildpack for .NET Publish\"}},\"paketo-buildpacks/encrypt-at-rest\":{\"4.5.13\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:667867d05b5a669b278a09ea1769b651f73e7d2aed541071d3ae4f017260d9cd\",\"homepage\":\"https://github.com/paketo-buildpacks/encrypt-at-rest\",\"name\":\"Paketo Buildpack for Encrypt-at-Rest\"}},\"paketo-buildpacks/environment-variables\":{\"4.5.3\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:e0b403660a34970e9f33fbfca76e5f60f900243797be86ce381746b6197771d3\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\",\"name\":\"Paketo Buildpack for Environment Variables\"},\"4.5.5\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:35a7169bca9ac0760bf19acda272c88cdf9b4fade0f96c5ff263ab8b862d789e\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\",\"name\":\"Paketo Buildpack for Environment Variables\"},\"4.5.6\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:491136e66ebd688914cbba8f67d6e84178245ba87c01f22f762b6ef966cbe718\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\",\"name\":\"Paketo Buildpack for Environment Variables\"}},\"paketo-buildpacks/executable-jar\":{\"6.8.3\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:07713a584e9c73cf2397531b4e3d669ff2d4578e1706ca5395dbfe683804e627\",\"homepage\":\"https://github.com/paketo-buildpacks/executable-jar\",\"name\":\"Paketo Buildpack for Executable JAR\"}},\"paketo-buildpacks/git\":{\"1.0.7\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:2bb5a7078a44f3cc8cc3e498a7df865355d0b9b91ba41c38d20858911602e1e5\",\"homepage\":\"https://github.com/paketo-buildpacks/git\",\"name\":\"Paketo Buildpack for Git\"}},\"paketo-buildpacks/go\":{\"4.6.2\":{\"api\":\"0.7\",\"order\":[{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/go-dist\",\"version\":\"2.4.4\"},{\"id\":\"paketo-buildpacks/git\",\"version\":\"1.0.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/go-mod-vendor\",\"version\":\"1.0.27\"},{\"id\":\"paketo-buildpacks/go-build\",\"version\":\"2.1.2\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/go-dist\",\"version\":\"2.4.4\"},{\"id\":\"paketo-buildpacks/git\",\"version\":\"1.0.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/go-build\",\"version\":\"2.1.2\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]}],\"layerDiffID\":\"sha256:ec527faf320cf1753d9b080a862257115c177f2f0eba1decab2d199196fa8f6a\",\"homepage\":\"https://github.com/paketo-buildpacks/go\",\"name\":\"Paketo Buildpack for Go\"}},\"paketo-buildpacks/go-build\":{\"2.1.2\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:76b1619f81820ed875e544a5fe47eb25e515352c4f7804c7dbe8723966e528c6\",\"homepage\":\"https://github.com/paketo-buildpacks/go-build\",\"name\":\"Paketo Buildpack for Go Build\"}},\"paketo-buildpacks/go-dist\":{\"2.4.4\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:b4b8b2d4b5f8bd0d39648b52f8fc7c54d88d433cc846ea07d6e5082a2cbca62a\",\"homepage\":\"https://github.com/paketo-buildpacks/go-dist\",\"name\":\"Paketo Buildpack for Go Distribution\"}},\"paketo-buildpacks/go-mod-vendor\":{\"1.0.27\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:27c065e31f83b584d59233a66103125e4b7171a8d5acc54e57c8bb1964d2f7c6\",\"homepage\":\"https://github.com/paketo-buildpacks/go-mod-vendor\",\"name\":\"Paketo Buildpack for Go Mod Vendor\"}},\"paketo-buildpacks/google-stackdriver\":{\"8.0.4\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:bb061969c1853e50cb7411bd0946c87d32871631e9c7c3cc03eb3739e7f8d350\",\"homepage\":\"https://github.com/paketo-buildpacks/google-stackdriver\",\"name\":\"Paketo Buildpack for Google Stackdriver\"}},\"paketo-buildpacks/gradle\":{\"7.7.1\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:5ad6a561c7da2aee1cc382514511fb5c0d37c1309f5ae0f092bfb3b0a6b111dc\",\"homepage\":\"https://github.com/paketo-buildpacks/gradle\",\"name\":\"Paketo Buildpack for Gradle\"}},\"paketo-buildpacks/httpd\":{\"0.7.14\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"}],\"layerDiffID\":\"sha256:72902c2a282fa529a145efae4838d0ac39818a6b9f77109307ce6512634e8f23\",\"homepage\":\"https://github.com/paketo-buildpacks/httpd\",\"name\":\"Paketo Buildpack for Apache HTTP Server\"}},\"paketo-buildpacks/icu\":{\"0.7.3\":{\"api\":\"0.8\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"}],\"layerDiffID\":\"sha256:e94fea0c37edd8d65cc4966b247b8ee9ffc0305c21a99f132fd8f75dfb9cbe83\",\"homepage\":\"https://github.com/paketo-buildpacks/icu\",\"name\":\"Paketo Buildpack for ICU\"}},\"paketo-buildpacks/image-labels\":{\"4.5.2\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:3e44c3f0963ed0bb2d0c64ecfaff2d706db8c156ae77924156befc9d4517c754\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\",\"name\":\"Paketo Buildpack for Image Labels\"},\"4.5.4\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:23f2551fe30d7889e2706b4099406cb1d0d2bd0baf0c495e2cbda6c3e901e48b\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\",\"name\":\"Paketo Buildpack for Image Labels\"},\"4.5.5\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:8c583593b549c46a53a13afe8c4a01200970cbcb8ee35526697d079232570456\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\",\"name\":\"Paketo Buildpack for Image Labels\"}},\"paketo-buildpacks/jattach\":{\"1.4.9\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:edb7e84d3ccbcdbe00586d0fcd187f6515809f38b35937727b59b15d33eb00fe\",\"homepage\":\"https://github.com/paketo-buildpacks/jattach\",\"name\":\"Paketo Buildpack for JAttach\"}},\"paketo-buildpacks/java\":{\"10.6.0\":{\"api\":\"0.7\",\"order\":[{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/bellsoft-liberica\",\"version\":\"10.4.4\"},{\"id\":\"paketo-buildpacks/yarn\",\"version\":\"1.2.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"3.1.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/syft\",\"version\":\"1.42.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/leiningen\",\"version\":\"4.6.9\",\"optional\":true},{\"id\":\"paketo-buildpacks/clojure-tools\",\"version\":\"2.8.14\",\"optional\":true},{\"id\":\"paketo-buildpacks/gradle\",\"version\":\"7.7.1\",\"optional\":true},{\"id\":\"paketo-buildpacks/maven\",\"version\":\"6.15.12\",\"optional\":true},{\"id\":\"paketo-buildpacks/sbt\",\"version\":\"6.12.11\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/executable-jar\",\"version\":\"6.8.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/apache-tomcat\",\"version\":\"7.14.2\",\"optional\":true},{\"id\":\"paketo-buildpacks/apache-tomee\",\"version\":\"1.8.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/liberty\",\"version\":\"3.8.12\",\"optional\":true},{\"id\":\"paketo-buildpacks/dist-zip\",\"version\":\"5.6.8\",\"optional\":true},{\"id\":\"paketo-buildpacks/spring-boot\",\"version\":\"5.27.8\",\"optional\":true},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.8\",\"optional\":true},{\"id\":\"paketo-buildpacks/jattach\",\"version\":\"1.4.9\",\"optional\":true},{\"id\":\"paketo-buildpacks/azure-application-insights\",\"version\":\"5.17.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/google-stackdriver\",\"version\":\"8.0.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/datadog\",\"version\":\"4.8.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/java-memory-assistant\",\"version\":\"1.4.9\",\"optional\":true},{\"id\":\"paketo-buildpacks/encrypt-at-rest\",\"version\":\"4.5.13\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]}],\"layerDiffID\":\"sha256:51aa45594fafc2ce0736447d99279482ae7201c90a78065f8c5f28133e936a5b\",\"homepage\":\"https://paketo.io/docs/howto/java\",\"name\":\"Paketo Buildpack for Java\"}},\"paketo-buildpacks/java-memory-assistant\":{\"1.4.9\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:b1059cc190c5c858b3ef697e123743ab2981d48b0de2b177f621c21795d1f4cc\",\"homepage\":\"https://github.com/paketo-buildpacks/java-memory-assistant\",\"name\":\"Paketo Buildpack for Java Memory Assistant\"}},\"paketo-buildpacks/java-native-image\":{\"8.25.0\":{\"api\":\"0.7\",\"order\":[{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/upx\",\"version\":\"3.4.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/bellsoft-liberica\",\"version\":\"10.4.4\"},{\"id\":\"paketo-buildpacks/syft\",\"version\":\"1.42.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/leiningen\",\"version\":\"4.6.9\",\"optional\":true},{\"id\":\"paketo-buildpacks/gradle\",\"version\":\"7.7.1\",\"optional\":true},{\"id\":\"paketo-buildpacks/maven\",\"version\":\"6.15.12\",\"optional\":true},{\"id\":\"paketo-buildpacks/sbt\",\"version\":\"6.12.11\",\"optional\":true},{\"id\":\"paketo-buildpacks/executable-jar\",\"version\":\"6.8.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/spring-boot\",\"version\":\"5.27.8\",\"optional\":true},{\"id\":\"paketo-buildpacks/datadog\",\"version\":\"4.8.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/native-image\",\"version\":\"5.12.7\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.8\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]}],\"layerDiffID\":\"sha256:2e6d9877eacfbdeda18cafbad95550207e82f96e5aa230056b9d750f648e21b0\",\"homepage\":\"https://paketo.io/docs/howto/java/#build-an-app-as-a-graalvm-native-image-application\",\"name\":\"Paketo Buildpack for Java Native Image\"}},\"paketo-buildpacks/leiningen\":{\"4.6.9\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:7f08c1f7191e025630591734cc2518ee31ece6f91b184f8379428cab1d11e4f3\",\"homepage\":\"https://github.com/paketo-buildpacks/leiningen\",\"name\":\"Paketo Buildpack for Leiningen\"}},\"paketo-buildpacks/liberty\":{\"3.8.12\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:39affe80dabb2dc69dcc3d50e82aa2251c631086c9f93bf84f736fa6951bba30\",\"homepage\":\"https://github.com/paketo-buildpacks/liberty\",\"name\":\"Paketo Buildpack for Liberty\"}},\"paketo-buildpacks/maven\":{\"6.15.12\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:556e2a8dfa041a33fc99b6bf252158f8ae6c9f987b24d8d51b60b5f47dde63db\",\"homepage\":\"https://github.com/paketo-buildpacks/maven\",\"name\":\"Paketo Buildpack for Maven\"}},\"paketo-buildpacks/miniconda\":{\"0.8.5\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:ddfaf04fb79c40ef178e264699a6e20723b9cebf4e6f42147629e99239bec874\",\"name\":\"Paketo Buildpack for Miniconda\"}},\"paketo-buildpacks/mri\":{\"0.14.10\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"}],\"layerDiffID\":\"sha256:67045efe8b4986fee6a84df0bc03956a7f9636b43df722eefe077806cc179759\",\"homepage\":\"https://github.com/paketo-buildpacks/mri\",\"name\":\"Paketo Buildpack for MRI\"}},\"paketo-buildpacks/native-image\":{\"5.12.7\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:6703f08ed36fa7863406b2a33a881f74648e9659aaa712fadb5bf40009a4558c\",\"homepage\":\"https://github.com/paketo-buildpacks/native-image\",\"name\":\"Paketo Buildpack for Native Image\"}},\"paketo-buildpacks/nginx\":{\"0.15.6\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"}],\"layerDiffID\":\"sha256:7a890b446c99ed0c593a9670e7d7c645de98855dc63288c64f04549c3622e7ec\",\"homepage\":\"https://github.com/paketo-buildpacks/nginx\",\"name\":\"Paketo Buildpack for Nginx Server\"}},\"paketo-buildpacks/node-engine\":{\"2.0.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:762574589a15aa16b3f268c5c648d64631fd529181d287150dbb6a93c6a74714\",\"homepage\":\"https://github.com/paketo-buildpacks/node-engine\",\"name\":\"Paketo Buildpack for Node Engine\"},\"3.0.1\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:ab2f69aa057c098d00d41e989b4dae371f763e75e456c9a655e6289a0c3a060c\",\"homepage\":\"https://github.com/paketo-buildpacks/node-engine\",\"name\":\"Paketo Buildpack for Node Engine\"},\"3.1.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:c48b2d0da119d1163fd360bc613b74a8cedd7ab0bb7faba9acf49bf233c18937\",\"homepage\":\"https://github.com/paketo-buildpacks/node-engine\",\"name\":\"Paketo Buildpack for Node Engine\"}},\"paketo-buildpacks/node-run-script\":{\"1.0.13\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:14704ae575228cecd0daf36e21239c71c6ae9065b4d00ea7c1ead4001da552c4\",\"homepage\":\"https://github.com/paketo-buildpacks/node-run-script\",\"name\":\"Paketo Buildpack for Node Run Script\"},\"1.0.14\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:831f58054738183092d1a3fac71c0b39b16a6649b032529f078e7f18f900e415\",\"homepage\":\"https://github.com/paketo-buildpacks/node-run-script\",\"name\":\"Paketo Buildpack for Node Run Script\"}},\"paketo-buildpacks/node-start\":{\"1.1.3\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:cf5619cb7b7168ec95f4bb572d8c79416fdb33421a2ff2dce55d4476265fed0a\",\"homepage\":\"https://github.com/paketo-buildpacks/node-start\",\"name\":\"Paketo Buildpack for Node Start\"}},\"paketo-buildpacks/nodejs\":{\"2.0.0\":{\"api\":\"0.7\",\"order\":[{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"3.0.1\"},{\"id\":\"paketo-buildpacks/yarn\",\"version\":\"1.2.0\"},{\"id\":\"paketo-buildpacks/yarn-install\",\"version\":\"1.2.2\"},{\"id\":\"paketo-buildpacks/node-run-script\",\"version\":\"1.0.14\",\"optional\":true},{\"id\":\"paketo-buildpacks/node-start\",\"version\":\"1.1.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/yarn-start\",\"version\":\"1.1.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/datadog\",\"version\":\"3.6.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"3.0.1\"},{\"id\":\"paketo-buildpacks/npm-install\",\"version\":\"1.3.1\"},{\"id\":\"paketo-buildpacks/node-run-script\",\"version\":\"1.0.14\",\"optional\":true},{\"id\":\"paketo-buildpacks/node-start\",\"version\":\"1.1.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/npm-start\",\"version\":\"1.0.15\",\"optional\":true},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/datadog\",\"version\":\"3.6.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"3.0.1\"},{\"id\":\"paketo-buildpacks/node-start\",\"version\":\"1.1.3\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/datadog\",\"version\":\"3.6.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]}],\"layerDiffID\":\"sha256:efa2f539b939405e5b27af54bcd3cf6e74165b5a3ab70cc4424c2a8e4e88c619\",\"homepage\":\"https://github.com/paketo-buildpacks/nodejs\",\"name\":\"Paketo Buildpack for Node.js\"}},\"paketo-buildpacks/npm-install\":{\"1.3.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:bd7446c321e344099f19f232f61b4f43a650aa88e5891100579f2a7b7e577f17\",\"homepage\":\"https://github.com/paketo-buildpacks/npm-install\",\"name\":\"Paketo Buildpack for NPM Install\"},\"1.3.1\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:22d2108f6ba3dc7aeb52bb971367cdef215f24bc2ed07401499c111162001bd1\",\"homepage\":\"https://github.com/paketo-buildpacks/npm-install\",\"name\":\"Paketo Buildpack for NPM Install\"}},\"paketo-buildpacks/npm-start\":{\"1.0.15\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:a3713bbd7bf2d8c4093a766214251db9660cf72020d645072a73dce9ff017c19\",\"homepage\":\"https://github.com/paketo-buildpacks/npm-start\",\"name\":\"Paketo Buildpack for NPM Start\"}},\"paketo-buildpacks/passenger\":{\"0.13.3\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"}],\"layerDiffID\":\"sha256:5b48a97e4cb1db0c2cae697fe88c67d1cc3653284e2112cc4c6d170537e3e220\",\"homepage\":\"https://github.com/paketo-buildpacks/passenger\",\"name\":\"Paketo Buildpack for Passenger\"}},\"paketo-buildpacks/pip\":{\"0.18.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:427ee111c07faab6e8b9ee58fdd2447cc19de7f1daa99eeb234d61b38e706868\",\"name\":\"Paketo Buildpack for Pip\"}},\"paketo-buildpacks/pip-install\":{\"0.6.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:5a61b8c6a643a5ab8801eb74d53288276f70b0fad35422ae3dbe31129a86f10d\",\"name\":\"Paketo Buildpack for Pip Install\"}},\"paketo-buildpacks/pipenv\":{\"1.19.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:0f1253493befe7e470e5adf66e8f3f81fc95fb7a11e8eaa4ef27edd6a4620232\",\"name\":\"Paketo Buildpack for Pipenv\"}},\"paketo-buildpacks/pipenv-install\":{\"0.6.18\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:8b9dac6ff7b7b833eed3aa511de4c7a0ae4dcb58d22873aee20f84187466fbe9\",\"name\":\"Paketo Buildpack for Pipenv Install\"}},\"paketo-buildpacks/poetry\":{\"0.6.5\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:fbad9e9552228860a9e0b81109861cb3b94377485a0d9e603aec4d01bb8d2409\",\"name\":\"Paketo Buildpack for Poetry\"}},\"paketo-buildpacks/poetry-install\":{\"0.3.17\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:b945b2a8add8dd4f225975cd962e56ffdd9b26a3af90b9d23fbe7b725625ba2f\",\"name\":\"Paketo Buildpack for Poetry Install\"}},\"paketo-buildpacks/poetry-run\":{\"0.4.21\":{\"api\":\"0.8\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:6945394fc466897bba06908c5ab1b0ed68d71e64820b40dfc9d58740275882c5\",\"name\":\"Paketo Buildpack for Poetry Run\"}},\"paketo-buildpacks/procfile\":{\"5.6.4\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:549522d4bb1bbe20f18b678d3db1c71cb6013c26a8ce444998175fa8a75fde94\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\"},\"5.6.6\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:d8fc272d6a7d4016559553a56e8140f0f871284f47978820c6d2c480fcf356c2\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\"},\"5.6.7\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:8c16fe2c06e19c4e70d50c3321e88ef66a9bcd896a498313a91e8cf7cda42d4f\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\"},\"5.6.8\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:7ed29173481c523f069fd9d4ca8a168733a161a2c45660423afb1bd9ada3545a\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\",\"name\":\"Paketo Buildpack for Procfile\"}},\"paketo-buildpacks/puma\":{\"0.4.37\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:45d931eca5343b9199f150195c62ff76ed05ba9bcedaa09d22f1aad3dfc5e969\",\"homepage\":\"https://github.com/paketo-buildpacks/puma\",\"name\":\"Paketo Buildpack for Puma\"}},\"paketo-buildpacks/python\":{\"2.14.0\":{\"api\":\"0.8\",\"order\":[{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/cpython\",\"version\":\"1.9.0\"},{\"id\":\"paketo-buildpacks/pip\",\"version\":\"0.18.0\"},{\"id\":\"paketo-buildpacks/pipenv\",\"version\":\"1.19.0\"},{\"id\":\"paketo-buildpacks/pipenv-install\",\"version\":\"0.6.18\"},{\"id\":\"paketo-buildpacks/python-start\",\"version\":\"0.14.14\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.2\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/cpython\",\"version\":\"1.9.0\"},{\"id\":\"paketo-buildpacks/pip\",\"version\":\"0.18.0\"},{\"id\":\"paketo-buildpacks/pip-install\",\"version\":\"0.6.0\"},{\"id\":\"paketo-buildpacks/python-start\",\"version\":\"0.14.14\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.2\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/miniconda\",\"version\":\"0.8.5\"},{\"id\":\"paketo-buildpacks/conda-env-update\",\"version\":\"0.7.12\"},{\"id\":\"paketo-buildpacks/python-start\",\"version\":\"0.14.14\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.2\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/cpython\",\"version\":\"1.9.0\"},{\"id\":\"paketo-buildpacks/pip\",\"version\":\"0.18.0\"},{\"id\":\"paketo-buildpacks/poetry\",\"version\":\"0.6.5\"},{\"id\":\"paketo-buildpacks/poetry-install\",\"version\":\"0.3.17\"},{\"id\":\"paketo-buildpacks/poetry-run\",\"version\":\"0.4.21\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.2\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/cpython\",\"version\":\"1.9.0\"},{\"id\":\"paketo-buildpacks/pip\",\"version\":\"0.18.0\"},{\"id\":\"paketo-buildpacks/poetry\",\"version\":\"0.6.5\"},{\"id\":\"paketo-buildpacks/poetry-install\",\"version\":\"0.3.17\"},{\"id\":\"paketo-buildpacks/python-start\",\"version\":\"0.14.14\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.2\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/cpython\",\"version\":\"1.9.0\"},{\"id\":\"paketo-buildpacks/python-start\",\"version\":\"0.14.14\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.3\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.2\",\"optional\":true}]}],\"layerDiffID\":\"sha256:442267367faf5de598f76a55eda8246802498bd8603b33e64fa478c1865a313d\",\"name\":\"Paketo Buildpack for Python\"}},\"paketo-buildpacks/python-start\":{\"0.14.14\":{\"api\":\"0.8\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:cc5f2a129069824d2b544daa7953a5897f25514a7f1288bdab7b118a22c96e1d\",\"homepage\":\"https://github.com/paketo-buildpacks/python-start\",\"name\":\"Paketo Buildpack for Python Start\"}},\"paketo-buildpacks/rackup\":{\"0.4.36\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:f8d8e1ebbbb5fa9329c9812197932021e1e69816b97d8e330142ac067be46f9a\",\"homepage\":\"https://github.com/paketo-buildpacks/rackup\",\"name\":\"Paketo Buildpack for Rackup\"}},\"paketo-buildpacks/rails-assets\":{\"0.10.4\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:2d3c01f357322eb3ec9e097c7483f5f93b0be210d32f265ad0b77f02b633c1f9\",\"homepage\":\"https://github.com/paketo-buildpacks/rails-assets\",\"name\":\"Paketo Buildpack for Rails Assets\"}},\"paketo-buildpacks/rake\":{\"0.4.36\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:5c991f23f064e635c66acfd3789b53c58c4a6c30c0a3fd5af99bbada3b0d1a36\",\"homepage\":\"https://github.com/paketo-buildpacks/rake\",\"name\":\"Paketo Buildpack for Rake\"}},\"paketo-buildpacks/ruby\":{\"0.42.1\":{\"api\":\"0.7\",\"order\":[{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/mri\",\"version\":\"0.14.10\"},{\"id\":\"paketo-buildpacks/bundler\",\"version\":\"0.8.1\"},{\"id\":\"paketo-buildpacks/bundle-install\",\"version\":\"0.8.1\"},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"3.0.1\",\"optional\":true},{\"id\":\"paketo-buildpacks/yarn\",\"version\":\"1.2.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/yarn-install\",\"version\":\"1.2.2\",\"optional\":true},{\"id\":\"paketo-buildpacks/rails-assets\",\"version\":\"0.10.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/puma\",\"version\":\"0.4.37\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.8\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/mri\",\"version\":\"0.14.10\"},{\"id\":\"paketo-buildpacks/bundler\",\"version\":\"0.8.1\"},{\"id\":\"paketo-buildpacks/bundle-install\",\"version\":\"0.8.1\"},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"3.0.1\",\"optional\":true},{\"id\":\"paketo-buildpacks/yarn\",\"version\":\"1.2.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/yarn-install\",\"version\":\"1.2.2\",\"optional\":true},{\"id\":\"paketo-buildpacks/rails-assets\",\"version\":\"0.10.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/thin\",\"version\":\"0.5.36\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.8\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/mri\",\"version\":\"0.14.10\"},{\"id\":\"paketo-buildpacks/bundler\",\"version\":\"0.8.1\"},{\"id\":\"paketo-buildpacks/bundle-install\",\"version\":\"0.8.1\"},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"3.0.1\",\"optional\":true},{\"id\":\"paketo-buildpacks/yarn\",\"version\":\"1.2.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/yarn-install\",\"version\":\"1.2.2\",\"optional\":true},{\"id\":\"paketo-buildpacks/rails-assets\",\"version\":\"0.10.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/unicorn\",\"version\":\"0.4.36\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.8\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/mri\",\"version\":\"0.14.10\"},{\"id\":\"paketo-buildpacks/bundler\",\"version\":\"0.8.1\"},{\"id\":\"paketo-buildpacks/bundle-install\",\"version\":\"0.8.1\"},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"3.0.1\",\"optional\":true},{\"id\":\"paketo-buildpacks/yarn\",\"version\":\"1.2.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/yarn-install\",\"version\":\"1.2.2\",\"optional\":true},{\"id\":\"paketo-buildpacks/rails-assets\",\"version\":\"0.10.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/passenger\",\"version\":\"0.13.3\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.8\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/mri\",\"version\":\"0.14.10\"},{\"id\":\"paketo-buildpacks/bundler\",\"version\":\"0.8.1\"},{\"id\":\"paketo-buildpacks/bundle-install\",\"version\":\"0.8.1\"},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"3.0.1\",\"optional\":true},{\"id\":\"paketo-buildpacks/yarn\",\"version\":\"1.2.0\",\"optional\":true},{\"id\":\"paketo-buildpacks/yarn-install\",\"version\":\"1.2.2\",\"optional\":true},{\"id\":\"paketo-buildpacks/rails-assets\",\"version\":\"0.10.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/rackup\",\"version\":\"0.4.36\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.8\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.7\",\"optional\":true},{\"id\":\"paketo-buildpacks/mri\",\"version\":\"0.14.10\"},{\"id\":\"paketo-buildpacks/bundler\",\"version\":\"0.8.1\",\"optional\":true},{\"id\":\"paketo-buildpacks/bundle-install\",\"version\":\"0.8.1\",\"optional\":true},{\"id\":\"paketo-buildpacks/rake\",\"version\":\"0.4.36\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.8\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.5\",\"optional\":true}]}],\"layerDiffID\":\"sha256:8fead10057cb6c2c3a15c046b88d7f5097cc255ceaa05a703feb474379f8b892\",\"homepage\":\"https://github.com/paketo-buildpacks/ruby\",\"name\":\"Paketo Buildpack for Ruby\"}},\"paketo-buildpacks/sbt\":{\"6.12.11\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:6b5c1ceaea0a04661e2954e38e3857e8361604bc81201d9dd75679f18776a76e\",\"homepage\":\"https://github.com/paketo-buildpacks/sbt\",\"name\":\"Paketo Buildpack for SBT\"}},\"paketo-buildpacks/source-removal\":{\"0.2.1\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:c9b2b4846699e199bf3baa9f694c2914da8404ab2278da08576a53ecd11cd608\",\"name\":\"Paketo Buildpack for Source Removal\"}},\"paketo-buildpacks/spring-boot\":{\"5.27.8\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:bd7eb5047ec41baea3935dd124854d8091eefac3498327fa0bd826ed70d4b8c7\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot\",\"name\":\"Paketo Buildpack for Spring Boot\"}},\"paketo-buildpacks/syft\":{\"1.42.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:0ce0006100b3fd286676a59b52c93688c1529ba9a91373fd0a247f7ddaaaf146\",\"homepage\":\"https://github.com/paketo-buildpacks/syft\",\"name\":\"Paketo Buildpack for Syft\"}},\"paketo-buildpacks/thin\":{\"0.5.36\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:607f576518edd2c0e2c1eebdcfc8ad158758171a3cbf66c0a42a961fc08ab5dc\",\"homepage\":\"https://github.com/paketo-buildpacks/thin\",\"name\":\"Paketo Buildpack for Thin\"}},\"paketo-buildpacks/unicorn\":{\"0.4.36\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:9fc259606ccec69e30ca25169bf6a9f80ac76d197cdd1ada148873859a3453dc\",\"homepage\":\"https://github.com/paketo-buildpacks/unicorn\",\"name\":\"Paketo Buildpack for Unicorn\"}},\"paketo-buildpacks/upx\":{\"3.4.7\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:37db1ffe6394c2f8a243aee1429789ef65ae7ddc83327714a7b2845d68a90086\",\"homepage\":\"https://github.com/paketo-buildpacks/upx\",\"name\":\"Paketo Buildpack for UPX\"}},\"paketo-buildpacks/vsdbg\":{\"0.3.7\":{\"api\":\"0.8\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"}],\"layerDiffID\":\"sha256:7907d2b0f5b2e44de7179ed3f78bf8039914be808e769ea2ffaefaf79f73a864\",\"name\":\"Paketo Buildpack for Visual Studio Debugger\"}},\"paketo-buildpacks/watchexec\":{\"2.8.3\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:7091071dbf9c60845a16f05a7c8edab69cc2c26aa13ba32c096f1f2097b91e72\",\"homepage\":\"https://github.com/paketo-buildpacks/watchexec\",\"name\":\"Paketo Buildpack for Watchexec\"},\"2.8.5\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:11f60937526ebc92492d6a33591e0b2099c585ba920a78da56b476afcf8c7ac3\",\"homepage\":\"https://github.com/paketo-buildpacks/watchexec\",\"name\":\"Paketo Buildpack for Watchexec\"},\"2.8.6\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:59ed9760c40281504b679fd3c24acba2fd5df71f4deb25caeb6f2d4611cd0b34\",\"homepage\":\"https://github.com/paketo-buildpacks/watchexec\",\"name\":\"Paketo Buildpack for Watchexec\"},\"2.8.7\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.paketo.stacks.tiny\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:0c5056a64de25640f4a2b9fde21186b5425007c233a1c402c52dcc96a2e2146d\",\"homepage\":\"https://github.com/paketo-buildpacks/watchexec\",\"name\":\"Paketo Buildpack for Watchexec\"}},\"paketo-buildpacks/web-servers\":{\"0.19.1\":{\"api\":\"0.7\",\"order\":[{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"2.0.0\"},{\"id\":\"paketo-buildpacks/yarn\",\"version\":\"1.1.11\"},{\"id\":\"paketo-buildpacks/yarn-install\",\"version\":\"1.2.1\"},{\"id\":\"paketo-buildpacks/node-run-script\",\"version\":\"1.0.13\"},{\"id\":\"paketo-buildpacks/nginx\",\"version\":\"0.15.6\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/source-removal\",\"version\":\"0.2.1\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"2.0.0\"},{\"id\":\"paketo-buildpacks/npm-install\",\"version\":\"1.3.0\"},{\"id\":\"paketo-buildpacks/node-run-script\",\"version\":\"1.0.13\"},{\"id\":\"paketo-buildpacks/nginx\",\"version\":\"0.15.6\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/source-removal\",\"version\":\"0.2.1\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"2.0.0\"},{\"id\":\"paketo-buildpacks/yarn\",\"version\":\"1.1.11\"},{\"id\":\"paketo-buildpacks/yarn-install\",\"version\":\"1.2.1\"},{\"id\":\"paketo-buildpacks/node-run-script\",\"version\":\"1.0.13\"},{\"id\":\"paketo-buildpacks/httpd\",\"version\":\"0.7.14\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/source-removal\",\"version\":\"0.2.1\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/node-engine\",\"version\":\"2.0.0\"},{\"id\":\"paketo-buildpacks/npm-install\",\"version\":\"1.3.0\"},{\"id\":\"paketo-buildpacks/node-run-script\",\"version\":\"1.0.13\"},{\"id\":\"paketo-buildpacks/httpd\",\"version\":\"0.7.14\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/source-removal\",\"version\":\"0.2.1\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/nginx\",\"version\":\"0.15.6\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/source-removal\",\"version\":\"0.2.1\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"3.6.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/watchexec\",\"version\":\"2.8.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/httpd\",\"version\":\"0.7.14\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.6\",\"optional\":true},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"4.5.5\",\"optional\":true},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"4.5.4\",\"optional\":true},{\"id\":\"paketo-buildpacks/source-removal\",\"version\":\"0.2.1\"}]}],\"layerDiffID\":\"sha256:60b2632955c147fd465b110c148052c946dcc0af762ae3f2875da59c87e08516\",\"homepage\":\"https://github.com/paketo-buildpacks/web-servers\",\"name\":\"Paketo Buildpack for Web Servers\"}},\"paketo-buildpacks/yarn\":{\"1.1.11\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:a7184382daf5436fe33ca5a0d69053fad35d1e13eb1047b6b908041ba9ca995e\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn\",\"name\":\"Paketo Buildpack for Yarn\"},\"1.2.0\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"io.buildpacks.stacks.jammy\"},{\"id\":\"*\"}],\"layerDiffID\":\"sha256:ec468a227ffad6f302bab3acf85e994c7e7d35d413e0682ad2cd9d3c8ab17d80\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn\",\"name\":\"Paketo Buildpack for Yarn\"}},\"paketo-buildpacks/yarn-install\":{\"1.2.1\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:00eba1517d53a91e912fc8d03c7335ca58477b7412b458041e674ae1f60bf204\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn-install\",\"name\":\"Paketo Buildpack for Yarn Install\"},\"1.2.2\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:e23c3fb0f408926d15a29044d91de04e61dc35068fb3552604f410cfe68be161\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn-install\",\"name\":\"Paketo Buildpack for Yarn Install\"}},\"paketo-buildpacks/yarn-start\":{\"1.1.3\":{\"api\":\"0.7\",\"stacks\":[{\"id\":\"*\"}],\"layerDiffID\":\"sha256:c19038830466a1990b14ed00f7b0464b4a7ad57c8c663398513f17524f97db95\",\"homepage\":\"https://github.com/paketo-buildpacks/yarn-start\",\"name\":\"Paketo Buildpack for Yarn Start\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"paketo-buildpacks/ruby\",\"version\":\"0.42.1\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/dotnet-core\",\"version\":\"0.42.3\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/go\",\"version\":\"4.6.2\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/java-native-image\",\"version\":\"8.25.0\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/java\",\"version\":\"10.6.0\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/web-servers\",\"version\":\"0.19.1\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/nodejs\",\"version\":\"2.0.0\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/python\",\"version\":\"2.14.0\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.8\"}]}]", + "io.buildpacks.buildpack.order-extensions": "null", + "io.buildpacks.extension.layers": "{}", + "io.buildpacks.stack.description": "ubuntu:jammy with compilers and shell utilities", + "io.buildpacks.stack.distro.name": "ubuntu", + "io.buildpacks.stack.distro.version": "22.04", + "io.buildpacks.stack.homepage": "https://github.com/paketo-buildpacks/jammy-base-stack", + "io.buildpacks.stack.id": "io.buildpacks.stacks.jammy", + "io.buildpacks.stack.maintainer": "Paketo Buildpacks", + "io.buildpacks.stack.metadata": "{}", + "io.buildpacks.stack.mixins": "null", + "io.buildpacks.stack.released": "2023-12-20T02:34:30Z", + "org.opencontainers.image.ref.name": "ubuntu", + "org.opencontainers.image.version": "22.04" + } + } + ] + }, + "runDetails": { + "builder": { + "id": "https://kpack.io/slsa/signed-build", + "version": { + "kpack": "0.0.0", + "lifecycle": "0.17.2" + }, + "builderDependencies": [ + { + "name": "Namespace", + "mediaType": "application/json", + "content": "eyJuYW1lIjoiZGVmYXVsdCIsInJlc291cmNlVmVyc2lvbiI6IjE5NSJ9" + }, + { + "name": "Build", + "mediaType": "application/json", + "content": "eyJuYW1lIjoidGVzdCIsInJlc291cmNlVmVyc2lvbiI6IjI4MTI2NTgyMSJ9" + }, + { + "name": "Pod", + "mediaType": "application/json", + "content": "eyJuYW1lIjoidGVzdC1idWlsZC1wb2QiLCJyZXNvdXJjZVZlcnNpb24iOiIyODEyNjU4MzIifQ==" + }, + { + "name": "ServiceAccount", + "mediaType": "application/json", + "content": "eyJuYW1lIjoiZGVmYXVsdCIsInJlc291cmNlVmVyc2lvbiI6IjI4MDYxNTI2OCJ9" + }, + { + "name": "Secrets", + "mediaType": "application/json", + "content": "W3sibmFtZSI6ImdjciIsInJlc291cmNlVmVyc2lvbiI6IjIxNjk5ODg4OSJ9LHsibmFtZSI6ImNvc2lnbiIsInJlc291cmNlVmVyc2lvbiI6IjI2MTIwNTY5OSJ9LHsibmFtZSI6InJzYSIsInJlc291cmNlVmVyc2lvbiI6IjI4MDYxNTkyMiJ9LHsibmFtZSI6ImVjZHNhIiwicmVzb3VyY2VWZXJzaW9uIjoiMjgwNjE2MDIyIn0seyJuYW1lIjoiZWQyNTUxOSIsInJlc291cmNlVmVyc2lvbiI6IjI4MDYxNjA4NSJ9XQ==" + } + ] + }, + "metadata": { + "invocationID": "https://kpack.io/default/test/test-build-pod@gke-default-pool-0582cba3-l21a", + "startedOn": "2024-01-10T14:55:57-05:00", + "finishedOn": "2024-01-10T14:57:57-05:00" + } + } + } +} +``` +
diff --git a/go.mod b/go.mod index a98fb38a9..871637b16 100644 --- a/go.mod +++ b/go.mod @@ -12,12 +12,15 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/go-containerregistry v0.17.0 github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20230822174451-190ad0e4d556 + github.com/in-toto/in-toto-golang v0.9.0 github.com/matthewmcnew/archtest v0.0.0-20191014222827-a111193b50ad github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b github.com/pkg/errors v0.9.1 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/sclevine/spec v1.4.0 + github.com/secure-systems-lab/go-securesystemslib v0.7.0 github.com/sigstore/cosign/v2 v2.2.2 + github.com/sigstore/sigstore v1.7.6 github.com/sirupsen/logrus v1.9.3 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 @@ -174,7 +177,6 @@ require ( github.com/hashicorp/hcl v1.0.1-vault-5 // indirect github.com/heroku/color v0.0.6 // indirect github.com/imdario/mergo v0.3.16 // indirect - github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect @@ -224,13 +226,11 @@ require ( github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sassoftware/relic v7.2.1+incompatible // indirect - github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect github.com/segmentio/ksuid v1.0.4 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/fulcio v1.4.3 // indirect github.com/sigstore/rekor v1.3.4 // indirect - github.com/sigstore/sigstore v1.7.6 // indirect github.com/sigstore/timestamp-authority v1.2.0 // indirect github.com/skeema/knownhosts v1.2.1 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect diff --git a/pkg/apis/build/v1alpha2/build_types.go b/pkg/apis/build/v1alpha2/build_types.go index 62816278a..b7254e1b8 100644 --- a/pkg/apis/build/v1alpha2/build_types.go +++ b/pkg/apis/build/v1alpha2/build_types.go @@ -71,13 +71,13 @@ type BuildSpec struct { Cosign *CosignConfig `json:"cosign,omitempty"` DefaultProcess string `json:"defaultProcess,omitempty"` // +listType - Tolerations []corev1.Toleration `json:"tolerations,omitempty"` - NodeSelector map[string]string `json:"nodeSelector,omitempty"` - Affinity *corev1.Affinity `json:"affinity,omitempty"` - RuntimeClassName *string `json:"runtimeClassName,omitempty"` - SchedulerName string `json:"schedulerName,omitempty"` - PriorityClassName string `json:"priorityClassName,omitempty"` - CreationTime string `json:"creationTime,omitempty"` + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + Affinity *corev1.Affinity `json:"affinity,omitempty"` + RuntimeClassName *string `json:"runtimeClassName,omitempty"` + SchedulerName string `json:"schedulerName,omitempty"` + PriorityClassName string `json:"priorityClassName,omitempty"` + CreationTime string `json:"creationTime,omitempty"` } func (bs *BuildSpec) RegistryCacheTag() string { @@ -129,12 +129,13 @@ type BuildStack struct { // +k8s:openapi-gen=true type BuildStatus struct { - corev1alpha1.Status `json:",inline"` - BuildMetadata corev1alpha1.BuildpackMetadataList `json:"buildMetadata,omitempty"` - Stack corev1alpha1.BuildStack `json:"stack,omitempty"` - LatestImage string `json:"latestImage,omitempty"` - LatestCacheImage string `json:"latestCacheImage,omitempty"` - PodName string `json:"podName,omitempty"` + corev1alpha1.Status `json:",inline"` + BuildMetadata corev1alpha1.BuildpackMetadataList `json:"buildMetadata,omitempty"` + Stack corev1alpha1.BuildStack `json:"stack,omitempty"` + LatestImage string `json:"latestImage,omitempty"` + LatestCacheImage string `json:"latestCacheImage,omitempty"` + LatestAttestationImage string `json:"latestAttestationImage,omitempty"` + PodName string `json:"podName,omitempty"` // +listType StepStates []corev1.ContainerState `json:"stepStates,omitempty"` // +listType diff --git a/pkg/blob/fetch.go b/pkg/blob/fetch.go index 3f2c0add5..c10feafb5 100644 --- a/pkg/blob/fetch.go +++ b/pkg/blob/fetch.go @@ -1,12 +1,17 @@ package blob import ( + "crypto/sha256" + "fmt" "io" + "io/fs" "log" "net/http" "net/url" "os" + "path" + "github.com/BurntSushi/toml" "github.com/pkg/errors" "github.com/pivotal/kpack/pkg/archive" @@ -18,7 +23,7 @@ type Fetcher struct { Logger *log.Logger } -func (f *Fetcher) Fetch(dir string, blobURL string, stripComponents int) error { +func (f *Fetcher) Fetch(dir string, blobURL string, stripComponents int, metadataDir string) error { u, err := url.Parse(blobURL) if err != nil { return err @@ -36,9 +41,15 @@ func (f *Fetcher) Fetch(dir string, blobURL string, stripComponents int) error { return err } + checksum, err := sha256sum(file) + if err != nil { + return err + } + switch mediaType { case "application/zip": - info, err := file.Stat() + var info fs.FileInfo + info, err = file.Stat() if err != nil { return err } @@ -57,6 +68,27 @@ func (f *Fetcher) Fetch(dir string, blobURL string, stripComponents int) error { return err } + projectMetadataFile, err := os.Create(path.Join(metadataDir, "project-metadata.toml")) + if err != nil { + return errors.Wrapf(err, "invalid metadata destination '%s/project-metadata.toml' for blob: %s", metadataDir, blobURL) + } + defer projectMetadataFile.Close() + + projectMd := Project{ + Source: Source{ + Type: "blob", + Metadata: Metadata{ + Url: blobURL, + }, + Version: Version{ + SHA256: checksum, + }, + }, + } + if err := toml.NewEncoder(projectMetadataFile).Encode(projectMd); err != nil { + return errors.Wrapf(err, "invalid metadata destination '%s/project-metadata.toml' for blob: %s", metadataDir, blobURL) + } + f.Logger.Printf("Successfully downloaded %s%s in path %q", u.Host, u.Path, dir) return nil @@ -105,3 +137,36 @@ func classifyFile(reader io.ReadSeeker) (string, error) { return http.DetectContentType(buf), nil } + +func sha256sum(reader io.ReadSeeker) (string, error) { + hash := sha256.New() + _, err := io.Copy(hash, reader) + if err != nil { + return "", err + } + + _, err = reader.Seek(0, 0) + if err != nil { + return "", err + } + + return fmt.Sprintf("%x", hash.Sum(nil)), nil +} + +type Project struct { + Source Source `toml:"source"` +} + +type Source struct { + Type string `toml:"type"` + Metadata Metadata `toml:"metadata"` + Version Version `toml:"version"` +} + +type Metadata struct { + Url string `toml:"url"` +} + +type Version struct { + SHA256 string `toml:"sha256sum"` +} diff --git a/pkg/blob/fetch_test.go b/pkg/blob/fetch_test.go index 2688babf2..bf4fa618f 100644 --- a/pkg/blob/fetch_test.go +++ b/pkg/blob/fetch_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "os" + "path" "path/filepath" "syscall" "testing" @@ -29,13 +30,17 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { fetcher = &blob.Fetcher{ Logger: log.New(output, "", 0), } - dir string + dir string + metadataDir string ) it.Before(func() { var err error dir, err = os.MkdirTemp("", "fetch_test") require.NoError(t, err) + + metadataDir, err = os.MkdirTemp("", "fetch_test") + require.NoError(t, err) }) it.After(func() { @@ -45,7 +50,7 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { for _, f := range []string{"test.zip", "test.tar", "test.tar.gz"} { testFile := f it("unpacks "+testFile, func() { - err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, testFile), 0) + err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, testFile), 0, metadataDir) require.NoError(t, err) files, err := os.ReadDir(dir) @@ -74,7 +79,7 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { // Set no umask to test file mode oldMask := syscall.Umask(0) defer syscall.Umask(oldMask) - err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "fat-zip.zip"), 0) + err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "fat-zip.zip"), 0, metadataDir) require.NoError(t, err) files, err := os.ReadDir(dir) @@ -91,7 +96,7 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { }) it("sets the correct file mode", func() { - err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "test-exe.tar"), 0) + err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "test-exe.tar"), 0, metadataDir) require.NoError(t, err) files, err := os.ReadDir(dir) @@ -115,9 +120,27 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { require.Equal(t, 0755, int(info.Mode())) }) + it("records project-metadata.toml", func() { + err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "test.tar"), 0, metadataDir) + require.NoError(t, err) + + p := path.Join(metadataDir, "project-metadata.toml") + contents, err := os.ReadFile(p) + require.NoError(t, err) + + expectedFile := fmt.Sprintf(`[source] + type = "blob" + [source.metadata] + url = "%v/test.tar" + [source.version] + sha256sum = "e54f870c2d76e5a1e577b9ff6c8c56f42b539fff83cf86cccf6b16ce6e177a4e" +`, server.URL) + + require.Equal(t, expectedFile, string(contents)) + }) for _, archiveFile := range []string{"parent.tar", "parent.tar.gz", "parent.zip"} { it("strips parent components from "+archiveFile, func() { - err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, archiveFile), 1) + err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, archiveFile), 1, metadataDir) require.NoError(t, err) files, err := os.ReadDir(dir) @@ -132,17 +155,17 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { it("errors when url is inaccessible", func() { url := fmt.Sprintf("%s/%s", server.URL, "invalid.zip") - err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "invalid.zip"), 0) + err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "invalid.zip"), 0, metadataDir) require.EqualError(t, err, fmt.Sprintf("failed to get blob %s", url)) }) it("errors when the blob file type is unexpected", func() { - err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "test.txt"), 0) + err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "test.txt"), 0, metadataDir) require.EqualError(t, err, "unexpected blob file type, must be one of .zip, .tar.gz, .tar, .jar") }) it("errors when the blob content type is unexpected", func() { - err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "test.html"), 0) + err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "test.html"), 0, metadataDir) require.EqualError(t, err, "unexpected blob file type, must be one of .zip, .tar.gz, .tar, .jar") }) } diff --git a/pkg/config/config.go b/pkg/config/config.go index 1cf6cb201..f5aca33f6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,13 +3,17 @@ package config import "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" type Config struct { + SystemNamespace string `json:"systemNamespace"` + SystemServiceAccount string `json:"systemServiceAccount"` + EnablePriorityClasses bool `json:"enablePriorityClasses"` MaximumPlatformApiVersion string `json:"maximumPlatformApiVersion"` SshTrustUnknownHosts bool `json:"sshTrustUnknownHosts"` } type FeatureFlags struct { - InjectedSidecarSupport bool `json:"injectedSidecarSupport"` + InjectedSidecarSupport bool `json:"injectedSidecarSupport"` + GenerateSlsaAttestation bool `json:"generateSlsaAttestation"` } type Images struct { diff --git a/pkg/config/lifecycle_provider.go b/pkg/config/lifecycle_provider.go index a28334b2c..2a75ab5c6 100644 --- a/pkg/config/lifecycle_provider.go +++ b/pkg/config/lifecycle_provider.go @@ -2,13 +2,13 @@ package config import ( "context" - "knative.dev/pkg/system" "sync/atomic" "github.com/google/go-containerregistry/pkg/authn" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + "knative.dev/pkg/system" "github.com/pivotal/kpack/pkg/cnb" "github.com/pivotal/kpack/pkg/registry" @@ -41,6 +41,14 @@ func NewLifecycleProvider(client RegistryClient, keychainFactory registry.Keycha } } +func (l *LifecycleProvider) Metadata() (cnb.LifecycleMetadata, error) { + lifecycle, err := l.lifecycle() + if err != nil { + return cnb.LifecycleMetadata{}, err + } + return lifecycle.metadata, nil +} + func (l *LifecycleProvider) LayerForOS(os string) (v1.Layer, cnb.LifecycleMetadata, error) { lifecycle, err := l.lifecycle() if err != nil { diff --git a/pkg/config/lifecycle_provider_test.go b/pkg/config/lifecycle_provider_test.go index 4eec8296e..49a2fd601 100644 --- a/pkg/config/lifecycle_provider_test.go +++ b/pkg/config/lifecycle_provider_test.go @@ -180,6 +180,12 @@ func testProvider(t *testing.T, when spec.G, it spec.S) { _, _, err := p.LayerForOS("kpack-invalid-test-os") require.EqualError(t, err, "unrecognized os kpack-invalid-test-os") }) + + it("can return the metadata by itself", func() { + readMetadata, err := p.Metadata() + require.NoError(t, err) + require.Equal(t, readMetadata, lifecycleMetadata) + }) }) } diff --git a/pkg/cosign/image_signer.go b/pkg/cosign/image_signer.go index f873043c2..17984a6ae 100644 --- a/pkg/cosign/image_signer.go +++ b/pkg/cosign/image_signer.go @@ -6,18 +6,21 @@ import ( "os" "github.com/buildpacks/lifecycle/platform/files" - "io/ioutil" - - cosignutil "github.com/pivotal/kpack/pkg/cosign/util" - "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" "github.com/pkg/errors" cosignoptions "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" cosignremote "github.com/sigstore/cosign/v2/pkg/oci/remote" corev1 "k8s.io/api/core/v1" + + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + "github.com/pivotal/kpack/pkg/secret" +) + +const ( + CosignRepositoryEnv = "COSIGN_REPOSITORY" + CosignDockerMediaTypesEnv = "COSIGN_DOCKER_MEDIA_TYPES" ) type SignFunc func(*cosignoptions.RootOptions, cosignoptions.KeyOpts, cosignoptions.SignOptions, []string) error @@ -74,7 +77,7 @@ func (s *ImageSigner) sign(ro *cosignoptions.RootOptions, refImage, digest, secr cosignPasswordFile := fmt.Sprintf("%s/%s/cosign.password", secretLocation, cosignSecret) ko := cosignoptions.KeyOpts{KeyRef: cosignKeyFile, PassFunc: func(bool) ([]byte, error) { - content, err := ioutil.ReadFile(cosignPasswordFile) + content, err := os.ReadFile(cosignPasswordFile) // When password file is not available, default empty password is used if err != nil { return []byte(""), nil @@ -84,17 +87,17 @@ func (s *ImageSigner) sign(ro *cosignoptions.RootOptions, refImage, digest, secr }} if cosignRepository, ok := cosignRepositories[cosignSecret]; ok { - if err := os.Setenv(cosignutil.CosignRepositoryEnv, fmt.Sprintf("%s", cosignRepository)); err != nil { - return errors.Errorf("failed setting %s env variable: %v", cosignutil.CosignRepositoryEnv, err) + if err := os.Setenv(CosignRepositoryEnv, fmt.Sprintf("%s", cosignRepository)); err != nil { + return errors.Errorf("failed setting %s env variable: %v", CosignRepositoryEnv, err) } - defer os.Unsetenv(cosignutil.CosignRepositoryEnv) + defer os.Unsetenv(CosignRepositoryEnv) } if cosignDockerMediaType, ok := cosignDockerMediaTypes[cosignSecret]; ok { - if err := os.Setenv(cosignutil.CosignDockerMediaTypesEnv, fmt.Sprintf("%s", cosignDockerMediaType)); err != nil { + if err := os.Setenv(CosignDockerMediaTypesEnv, fmt.Sprintf("%s", cosignDockerMediaType)); err != nil { return errors.Errorf("failed setting COSIGN_DOCKER_MEDIA_TYPES env variable: %v", err) } - defer os.Unsetenv(cosignutil.CosignDockerMediaTypesEnv) + defer os.Unsetenv(CosignDockerMediaTypesEnv) } var cosignAnnotations []string @@ -130,14 +133,14 @@ func (s *ImageSigner) SignBuilder( builderKeychain authn.Keychain, ) ([]v1alpha2.CosignSignature, error) { signaturePaths := make([]v1alpha2.CosignSignature, 0) - cosignSecrets := filterCosignSecrets(serviceAccountSecrets) + cosignSecrets := secret.FilterCosignSigningSecrets(serviceAccountSecrets) for _, cosignSecret := range cosignSecrets { keyRef := fmt.Sprintf("k8s://%s/%s", cosignSecret.Namespace, cosignSecret.Name) keyOpts := cosignoptions.KeyOpts{ KeyRef: keyRef, PassFunc: func(bool) ([]byte, error) { - if password, ok := cosignSecret.Data[cosignutil.SecretDataCosignPassword]; ok { + if password, ok := cosignSecret.Data[secret.CosignSecretPassword]; ok { return password, nil } @@ -145,15 +148,15 @@ func (s *ImageSigner) SignBuilder( }, } - if cosignRepository, ok := cosignSecret.Annotations[cosignutil.RepositoryAnnotationPrefix]; ok { - if err := os.Setenv(cosignutil.CosignRepositoryEnv, cosignRepository); err != nil { - return nil, fmt.Errorf("failed setting %s env variable: %w", cosignutil.CosignRepositoryEnv, err) + if cosignRepository, ok := cosignSecret.Annotations[secret.CosignRepositoryAnnotation]; ok { + if err := os.Setenv(CosignRepositoryEnv, cosignRepository); err != nil { + return nil, fmt.Errorf("failed setting %s env variable: %w", CosignRepositoryEnv, err) } } - if cosignDockerMediaType, ok := cosignSecret.Annotations[cosignutil.DockerMediaTypesAnnotationPrefix]; ok { - if err := os.Setenv(cosignutil.CosignDockerMediaTypesEnv, cosignDockerMediaType); err != nil { - return nil, fmt.Errorf("failed setting %s env variable: %w", cosignutil.CosignDockerMediaTypesEnv, err) + if cosignDockerMediaType, ok := cosignSecret.Annotations[secret.CosignDockerMediaTypesAnnotation]; ok { + if err := os.Setenv(CosignDockerMediaTypesEnv, cosignDockerMediaType); err != nil { + return nil, fmt.Errorf("failed setting %s env variable: %w", CosignDockerMediaTypesEnv, err) } } @@ -210,17 +213,17 @@ func (s *ImageSigner) SignBuilder( }, ) - if _, found := os.LookupEnv(cosignutil.CosignDockerMediaTypesEnv); found { - err = os.Unsetenv(cosignutil.CosignDockerMediaTypesEnv) + if _, found := os.LookupEnv(CosignDockerMediaTypesEnv); found { + err = os.Unsetenv(CosignDockerMediaTypesEnv) if err != nil { - return nil, fmt.Errorf("failed to cleanup environment variable %s: %w", cosignutil.CosignDockerMediaTypesEnv, err) + return nil, fmt.Errorf("failed to cleanup environment variable %s: %w", CosignDockerMediaTypesEnv, err) } } - if _, found := os.LookupEnv(cosignutil.CosignRepositoryEnv); found { - err = os.Unsetenv(cosignutil.CosignRepositoryEnv) + if _, found := os.LookupEnv(CosignRepositoryEnv); found { + err = os.Unsetenv(CosignRepositoryEnv) if err != nil { - return nil, fmt.Errorf("failed to cleanup environment variable %s: %w", cosignutil.CosignRepositoryEnv, err) + return nil, fmt.Errorf("failed to cleanup environment variable %s: %w", CosignRepositoryEnv, err) } } } @@ -228,22 +231,6 @@ func (s *ImageSigner) SignBuilder( return signaturePaths, nil } -func filterCosignSecrets(serviceAccountSecrets []*corev1.Secret) []*corev1.Secret { - cosignSecrets := make([]*corev1.Secret, 0) - - for _, cosignSecret := range serviceAccountSecrets { - _, passwordOk := cosignSecret.Data[cosignutil.SecretDataCosignPassword] - _, keyOk := cosignSecret.Data[cosignutil.SecretDataCosignKey] - - if passwordOk && keyOk { - cosignSecrets = append(cosignSecrets, cosignSecret) - } - } - - // successful - return cosignSecrets -} - func findCosignSecrets(secretLocation string) ([]string, error) { var result []string diff --git a/pkg/cosign/image_signer_test.go b/pkg/cosign/image_signer_test.go index 24a023601..e228de93b 100644 --- a/pkg/cosign/image_signer_test.go +++ b/pkg/cosign/image_signer_test.go @@ -4,7 +4,7 @@ import ( "bufio" "context" "fmt" - "io/ioutil" + "io" "log" "net/http/httptest" "net/url" @@ -14,9 +14,6 @@ import ( "strings" "testing" - cosigntesting "github.com/pivotal/kpack/pkg/cosign/testing" - cosignutil "github.com/pivotal/kpack/pkg/cosign/util" - "github.com/BurntSushi/toml" "github.com/buildpacks/lifecycle/platform/files" "github.com/google/go-containerregistry/pkg/authn" @@ -25,8 +22,6 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/remote" - registry2 "github.com/pivotal/kpack/pkg/registry" - "github.com/pivotal/kpack/pkg/registry/registryfakes" "github.com/sclevine/spec" "github.com/sigstore/cosign/v2/cmd/cosign/cli/download" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" @@ -37,6 +32,11 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + cosigntesting "github.com/pivotal/kpack/pkg/cosign/testing" + registry2 "github.com/pivotal/kpack/pkg/registry" + "github.com/pivotal/kpack/pkg/registry/registryfakes" + "github.com/pivotal/kpack/pkg/secret" ) var fetchSignatureFunc = func(_ name.Reference, options ...ociremote.Option) (name.Tag, error) { @@ -93,16 +93,16 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { // Override secretLocation for test secretLocation = createCosignKeyFiles(t) - secretKey1 = path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignKey) - publicKey1 = path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignPublicKey) - publicKey2 = path.Join(secretLocation, "secret-name-2", cosignutil.SecretDataCosignPublicKey) - passwordFile1 = path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignPassword) - passwordFile2 = path.Join(secretLocation, "secret-name-2", cosignutil.SecretDataCosignPassword) + secretKey1 = path.Join(secretLocation, "secret-name-1", secret.CosignSecretPrivateKey) + publicKey1 = path.Join(secretLocation, "secret-name-1", secret.CosignSecretPublicKey) + publicKey2 = path.Join(secretLocation, "secret-name-2", secret.CosignSecretPublicKey) + passwordFile1 = path.Join(secretLocation, "secret-name-1", secret.CosignSecretPassword) + passwordFile2 = path.Join(secretLocation, "secret-name-2", secret.CosignSecretPassword) report = createReportToml(t, expectedImageName, imageDigest) - os.Unsetenv(cosignutil.CosignRepositoryEnv) - os.Unsetenv(cosignutil.CosignDockerMediaTypesEnv) + os.Unsetenv(CosignRepositoryEnv) + os.Unsetenv(CosignDockerMediaTypesEnv) }) it("signs images", func() { @@ -277,15 +277,15 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { cliSignCmdCallCount := 0 - assert.Empty(t, len(os.Getenv(cosignutil.CosignRepositoryEnv))) + assert.Empty(t, len(os.Getenv(CosignRepositoryEnv))) cliSignCmd := func( ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string, ) error { t.Helper() if strings.Contains(ko.KeyRef, "secret-name-2") { - assert.Equal(t, altImageName, os.Getenv(cosignutil.CosignRepositoryEnv)) + assert.Equal(t, altImageName, os.Getenv(CosignRepositoryEnv)) } else { - assertUnset(t, cosignutil.CosignRepositoryEnv) + assertUnset(t, CosignRepositoryEnv) } cliSignCmdCallCount++ @@ -306,7 +306,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { assert.Nil(t, err) assert.Equal(t, 2, cliSignCmdCallCount) - assertUnset(t, cosignutil.CosignRepositoryEnv) + assertUnset(t, CosignRepositoryEnv) err = cosigntesting.Verify(t, publicKey1, expectedImageName, nil) assert.Nil(t, err) @@ -317,8 +317,8 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { // Required to set COSIGN_REPOSITORY env variable to validate signature // on a registry that does not contain the image - os.Setenv(cosignutil.CosignRepositoryEnv, altImageName) - defer os.Unsetenv(cosignutil.CosignRepositoryEnv) + os.Setenv(CosignRepositoryEnv, altImageName) + defer os.Unsetenv(CosignRepositoryEnv) err = cosigntesting.Verify(t, publicKey1, expectedImageName, nil) assert.Error(t, err) err = cosigntesting.Verify(t, publicKey2, expectedImageName, nil) @@ -328,15 +328,15 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { it("sets COSIGN_DOCKER_MEDIA_TYPES environment variable", func() { cliSignCmdCallCount := 0 - assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) + assertUnset(t, CosignDockerMediaTypesEnv) cliSignCmd := func( ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string, ) error { t.Helper() if strings.Contains(ko.KeyRef, "secret-name-1") { - assert.Equal(t, "1", os.Getenv(cosignutil.CosignDockerMediaTypesEnv)) + assert.Equal(t, "1", os.Getenv(CosignDockerMediaTypesEnv)) } else { - assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) + assertUnset(t, CosignDockerMediaTypesEnv) } cliSignCmdCallCount++ @@ -352,20 +352,20 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { assert.Nil(t, err) assert.Equal(t, 2, cliSignCmdCallCount) - assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) + assertUnset(t, CosignDockerMediaTypesEnv) }) it("sets both COSIGN_REPOSITORY and COSIGN_DOCKER_MEDIA_TYPES environment variable", func() { cliSignCmdCallCount := 0 - assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) - assertUnset(t, cosignutil.CosignRepositoryEnv) + assertUnset(t, CosignDockerMediaTypesEnv) + assertUnset(t, CosignRepositoryEnv) cliSignCmd := func( ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string, ) error { t.Helper() - assert.Equal(t, "1", os.Getenv(cosignutil.CosignDockerMediaTypesEnv)) - assert.Equal(t, "registry.example.com/fakeproject", os.Getenv(cosignutil.CosignRepositoryEnv)) + assert.Equal(t, "1", os.Getenv(CosignDockerMediaTypesEnv)) + assert.Equal(t, "registry.example.com/fakeproject", os.Getenv(CosignRepositoryEnv)) cliSignCmdCallCount++ return nil } @@ -385,8 +385,8 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { assert.Nil(t, err) assert.Equal(t, 2, cliSignCmdCallCount) - assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) - assertUnset(t, cosignutil.CosignRepositoryEnv) + assertUnset(t, CosignDockerMediaTypesEnv) + assertUnset(t, CosignRepositoryEnv) }) }) @@ -462,8 +462,8 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { password := "" keypair(t, secretLocation, "secret-name-1", password) - privKeyPath := path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignKey) - pubKeyPath := path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignPublicKey) + privKeyPath := path.Join(secretLocation, "secret-name-1", secret.CosignSecretPrivateKey) + pubKeyPath := path.Join(secretLocation, "secret-name-1", secret.CosignSecretPublicKey) ctx := context.Background() // Verify+download should fail at first @@ -583,12 +583,12 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { signFunc: func(rootOptions *options.RootOptions, opts options.KeyOpts, signOptions options.SignOptions, i []string) error { t.Helper() - value, found := os.LookupEnv(cosignutil.CosignRepositoryEnv) + value, found := os.LookupEnv(CosignRepositoryEnv) require.True(t, found) require.NotNil(t, value) assert.Equal(t, signaturesPath, value) - value, found = os.LookupEnv(cosignutil.CosignDockerMediaTypesEnv) + value, found = os.LookupEnv(CosignDockerMediaTypesEnv) require.True(t, found) require.NotNil(t, value) assert.Equal(t, dockerMediaTypesValue, value) @@ -701,7 +701,8 @@ func assertUnset(t *testing.T, envName string, msg ...string) { } func fakeRegistry(t *testing.T) (string, func()) { - r := httptest.NewServer(registry.New()) + sinkLogger := log.New(io.Discard, "", 0) + r := httptest.NewServer(registry.New(registry.Logger(sinkLogger))) u, err := url.Parse(r.URL) assert.Nil(t, err) @@ -752,15 +753,15 @@ func keypair(t *testing.T, dirPath, secretName, password string) { err = os.Mkdir(filepath.Join(dirPath, secretName), 0700) assert.Nil(t, err) - privKeyPath := filepath.Join(dirPath, secretName, cosignutil.SecretDataCosignKey) - err = ioutil.WriteFile(privKeyPath, keys.PrivateBytes, 0600) + privKeyPath := filepath.Join(dirPath, secretName, secret.CosignSecretPrivateKey) + err = os.WriteFile(privKeyPath, keys.PrivateBytes, 0600) assert.Nil(t, err) - pubKeyPath := filepath.Join(dirPath, secretName, cosignutil.SecretDataCosignPublicKey) - err = ioutil.WriteFile(pubKeyPath, keys.PublicBytes, 0600) + pubKeyPath := filepath.Join(dirPath, secretName, secret.CosignSecretPublicKey) + err = os.WriteFile(pubKeyPath, keys.PublicBytes, 0600) assert.Nil(t, err) - passwordPath := filepath.Join(dirPath, secretName, cosignutil.SecretDataCosignPassword) + passwordPath := filepath.Join(dirPath, secretName, secret.CosignSecretPassword) passwordBytes, _ := passFunc(true) err = os.WriteFile(passwordPath, passwordBytes, 0600) assert.Nil(t, err) diff --git a/pkg/cosign/testing/test_util.go b/pkg/cosign/testing/test_util.go index 90dc2c854..245fd1cea 100644 --- a/pkg/cosign/testing/test_util.go +++ b/pkg/cosign/testing/test_util.go @@ -5,14 +5,14 @@ import ( "crypto" "testing" - cosignutil "github.com/pivotal/kpack/pkg/cosign/util" - cosignVerify "github.com/sigstore/cosign/v2/cmd/cosign/cli/verify" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/signature" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pivotal/kpack/pkg/secret" ) func GenerateFakeKeyPair(t *testing.T, secretName string, secretNamespace string, password string, annotations map[string]string) corev1.Secret { @@ -26,9 +26,9 @@ func GenerateFakeKeyPair(t *testing.T, secretName string, secretNamespace string require.NoError(t, err) data := map[string][]byte{ - cosignutil.SecretDataCosignPublicKey: keys.PublicBytes, - cosignutil.SecretDataCosignKey: keys.PrivateBytes, - cosignutil.SecretDataCosignPassword: []byte(password), + secret.CosignSecretPublicKey: keys.PublicBytes, + secret.CosignSecretPrivateKey: keys.PrivateBytes, + secret.CosignSecretPassword: []byte(password), } secret := corev1.Secret{ diff --git a/pkg/cosign/util/constants.go b/pkg/cosign/util/constants.go deleted file mode 100644 index 96efabd5f..000000000 --- a/pkg/cosign/util/constants.go +++ /dev/null @@ -1,12 +0,0 @@ -package cosignutil - -const ( - CosignRepositoryEnv = "COSIGN_REPOSITORY" - CosignDockerMediaTypesEnv = "COSIGN_DOCKER_MEDIA_TYPES" - - SecretDataCosignKey = "cosign.key" - SecretDataCosignPassword = "cosign.password" - SecretDataCosignPublicKey = "cosign.pub" - DockerMediaTypesAnnotationPrefix = "kpack.io/cosign.docker-media-types" - RepositoryAnnotationPrefix = "kpack.io/cosign.repository" -) diff --git a/pkg/git/fetch.go b/pkg/git/fetch.go index 8020d95d0..c1e198284 100644 --- a/pkg/git/fetch.go +++ b/pkg/git/fetch.go @@ -97,14 +97,14 @@ func (f Fetcher) Fetch(dir, gitURL, gitRevision, metadataDir string) error { } defer projectMetadataFile.Close() - projectMd := project{ - Source: source{ + projectMd := Project{ + Source: Source{ Type: "git", - Metadata: metadata{ + Metadata: Metadata{ Repository: gitURL, Revision: gitRevision, }, - Version: version{ + Version: Version{ Commit: hash.String(), }, }, @@ -117,21 +117,21 @@ func (f Fetcher) Fetch(dir, gitURL, gitRevision, metadataDir string) error { return nil } -type project struct { - Source source `toml:"source"` +type Project struct { + Source Source `toml:"source"` } -type source struct { +type Source struct { Type string `toml:"type"` - Metadata metadata `toml:"metadata"` - Version version `toml:"version"` + Metadata Metadata `toml:"metadata"` + Version Version `toml:"version"` } -type metadata struct { +type Metadata struct { Repository string `toml:"repository"` Revision string `toml:"revision"` } -type version struct { +type Version struct { Commit string `toml:"commit"` } diff --git a/pkg/git/fetch_test.go b/pkg/git/fetch_test.go index 2494688a3..aeb9fa975 100644 --- a/pkg/git/fetch_test.go +++ b/pkg/git/fetch_test.go @@ -25,8 +25,10 @@ func testGitCheckout(t *testing.T, when spec.G, it spec.S) { Logger: log.New(outputBuffer, "", 0), Keychain: fakeGitKeychain{}, } - var testDir string - var metadataDir string + var ( + testDir string + metadataDir string + ) it.Before(func() { var err error @@ -54,7 +56,7 @@ func testGitCheckout(t *testing.T, when spec.G, it spec.S) { p := path.Join(metadataDir, "project-metadata.toml") require.FileExists(t, p) - var projectMetadata project + var projectMetadata Project _, err = toml.DecodeFile(p, &projectMetadata) require.NoError(t, err) require.Equal(t, "git", projectMetadata.Source.Type) @@ -102,21 +104,39 @@ func testGitCheckout(t *testing.T, when spec.G, it spec.S) { require.False(t, isExecutableByAny(fileInfo.Mode())) }) + it("records project-metadata.toml", func() { + err := fetcher.Fetch(testDir, "https://github.com/git-fixtures/basic", "b029517f6300c2da0f4b651b8642506cd6aaf45d", metadataDir) + require.NoError(t, err) + + p := path.Join(metadataDir, "project-metadata.toml") + contents, err := os.ReadFile(p) + require.NoError(t, err) + + expectedFile := `[source] + type = "git" + [source.metadata] + repository = "https://github.com/git-fixtures/basic" + revision = "b029517f6300c2da0f4b651b8642506cd6aaf45d" + [source.version] + commit = "b029517f6300c2da0f4b651b8642506cd6aaf45d" +` + require.Equal(t, expectedFile, string(contents)) + }) + it("returns invalid credentials to fetch error on authentication required", func() { err := fetcher.Fetch(testDir, "git@bitbucket.com:org/repo", "main", metadataDir) require.ErrorContains(t, err, "unable to fetch references for repository") }) - it("initializes submodules", func() { - fetcher.InitializeSubmodules = true - err := fetcher.Fetch(testDir, "https://github.com/git-fixtures/submodule", "master", metadataDir) - require.NoError(t, err) - - _, err = os.Lstat(path.Join(testDir, "basic", ".gitignore")) + it("initializes submodules", func() { + fetcher.InitializeSubmodules = true + err := fetcher.Fetch(testDir, "https://github.com/git-fixtures/submodule", "master", metadataDir) require.NoError(t, err) + _, err = os.Lstat(path.Join(testDir, "basic", ".gitignore")) + require.NoError(t, err) - }) + }) }) } diff --git a/pkg/reconciler/build/build.go b/pkg/reconciler/build/build.go index b02cfc0c5..033fa8dd7 100644 --- a/pkg/reconciler/build/build.go +++ b/pkg/reconciler/build/build.go @@ -3,18 +3,11 @@ package build import ( "context" "encoding/json" + "fmt" + "github.com/google/go-containerregistry/pkg/authn" - buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" - corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" - "github.com/pivotal/kpack/pkg/buildchange" - "github.com/pivotal/kpack/pkg/buildpod" - "github.com/pivotal/kpack/pkg/client/clientset/versioned" - buildinformers "github.com/pivotal/kpack/pkg/client/informers/externalversions/build/v1alpha2" - buildlisters "github.com/pivotal/kpack/pkg/client/listers/build/v1alpha2" - "github.com/pivotal/kpack/pkg/cnb" - "github.com/pivotal/kpack/pkg/reconciler" - "github.com/pivotal/kpack/pkg/registry" - "github.com/pkg/errors" + ggcrv1 "github.com/google/go-containerregistry/pkg/v1" + intoto "github.com/in-toto/in-toto-golang/in_toto" "go.uber.org/zap" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -27,6 +20,20 @@ import ( "k8s.io/client-go/tools/cache" "knative.dev/pkg/controller" "knative.dev/pkg/logging/logkey" + + buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" + "github.com/pivotal/kpack/pkg/buildchange" + "github.com/pivotal/kpack/pkg/buildpod" + "github.com/pivotal/kpack/pkg/client/clientset/versioned" + buildinformers "github.com/pivotal/kpack/pkg/client/informers/externalversions/build/v1alpha2" + buildlisters "github.com/pivotal/kpack/pkg/client/listers/build/v1alpha2" + "github.com/pivotal/kpack/pkg/cnb" + "github.com/pivotal/kpack/pkg/config" + "github.com/pivotal/kpack/pkg/reconciler" + "github.com/pivotal/kpack/pkg/registry" + "github.com/pivotal/kpack/pkg/secret" + "github.com/pivotal/kpack/pkg/slsa" ) const ( @@ -49,17 +56,41 @@ type PodProgressLogger interface { GetTerminationMessage(pod *corev1.Pod, s *corev1.ContainerStatus) (string, error) } -func NewController(ctx context.Context, opt reconciler.Options, k8sClient k8sclient.Interface, informer buildinformers.BuildInformer, podInformer corev1Informers.PodInformer, metadataRetriever MetadataRetriever, podGenerator PodGenerator, podProgressLogger *buildchange.ProgressLogger, keychainFactory registry.KeychainFactory, injectedSidecarSupport bool) *controller.Impl { +//go:generate counterfeiter . SLSAAttester +type SLSAAttester interface { + AttestBuild(build *buildapi.Build, buildMetadata *cnb.BuildMetadata, pod *corev1.Pod, builderAndAppKeychain authn.Keychain, builderID slsa.BuilderID, depFns ...slsa.BuilderDependencyFn) (intoto.Statement, error) + Sign(ctx context.Context, stmt intoto.Statement, signers ...slsa.Signer) ([]byte, error) + Write(ctx context.Context, digestStr string, payload []byte, keychain authn.Keychain) (ggcrv1.Image, string, error) +} + +//go:generate counterfeiter . SecretFetcher +type SecretFetcher interface { + SecretsForServiceAccount(ctx context.Context, serviceAccount, namespace string) ([]*corev1.Secret, error) + SecretsForSystemServiceAccount(context.Context) ([]*corev1.Secret, error) +} + +func NewController( + ctx context.Context, opt reconciler.Options, k8sClient k8sclient.Interface, + informer buildinformers.BuildInformer, podInformer corev1Informers.PodInformer, + metadataRetriever MetadataRetriever, + podGenerator PodGenerator, podProgressLogger *buildchange.ProgressLogger, + keychainFactory registry.KeychainFactory, + attester SLSAAttester, + secretFetcher SecretFetcher, + featureFlags config.FeatureFlags, +) *controller.Impl { c := &Reconciler{ - Client: opt.Client, - K8sClient: k8sClient, - MetadataRetriever: metadataRetriever, - Lister: informer.Lister(), - PodLister: podInformer.Lister(), - PodGenerator: podGenerator, - PodProgressLogger: podProgressLogger, - KeychainFactory: keychainFactory, - InjectedSidecarSupport: injectedSidecarSupport, + Client: opt.Client, + K8sClient: k8sClient, + MetadataRetriever: metadataRetriever, + Lister: informer.Lister(), + PodLister: podInformer.Lister(), + PodGenerator: podGenerator, + PodProgressLogger: podProgressLogger, + KeychainFactory: keychainFactory, + Attester: attester, + SecretFetcher: secretFetcher, + FeatureFlags: featureFlags, } logger := opt.Logger.With( @@ -79,15 +110,17 @@ func NewController(ctx context.Context, opt reconciler.Options, k8sClient k8scli } type Reconciler struct { - Client versioned.Interface - KeychainFactory registry.KeychainFactory - Lister buildlisters.BuildLister - MetadataRetriever MetadataRetriever - K8sClient k8sclient.Interface - PodLister v1Listers.PodLister - PodGenerator PodGenerator - PodProgressLogger PodProgressLogger - InjectedSidecarSupport bool + Client versioned.Interface + KeychainFactory registry.KeychainFactory + Lister buildlisters.BuildLister + MetadataRetriever MetadataRetriever + K8sClient k8sclient.Interface + PodLister v1Listers.PodLister + PodGenerator PodGenerator + PodProgressLogger PodProgressLogger + Attester SLSAAttester + SecretFetcher SecretFetcher + FeatureFlags config.FeatureFlags } func (c *Reconciler) Reconcile(ctx context.Context, key string) error { @@ -128,7 +161,7 @@ func (c *Reconciler) reconcile(ctx context.Context, build *buildapi.Build) error return controller.NewPermanentError(err) } - if c.InjectedSidecarSupport { + if c.FeatureFlags.InjectedSidecarSupport { pod, err = c.setBuildReady(ctx, pod) if err != nil { return err @@ -154,7 +187,7 @@ func (c *Reconciler) reconcile(ctx context.Context, build *buildapi.Build) error }) if err != nil { - return errors.Wrap(err, "unable to create app image keychain") + return fmt.Errorf("unable to create app image keychain: %v", err) } buildMetadata, err = c.MetadataRetriever.GetBuildMetadata(build.Tag(), cacheTag, keychain) @@ -164,12 +197,22 @@ func (c *Reconciler) reconcile(ctx context.Context, build *buildapi.Build) error } else { buildMetadata, err = c.buildMetadataFromBuildPod(pod) if err != nil { - return errors.Wrap(err, "failed to get build metadata from build pod") + return fmt.Errorf("failed to get build metadata from build pod: %v", err) } } + + var attestDigest string + if c.FeatureFlags.GenerateSlsaAttestation { + attestDigest, err = c.attestBuild(ctx, build, buildMetadata, pod) + if err != nil { + return fmt.Errorf("failed to attest build: %v", err) + } + } + build.Status.BuildMetadata = buildMetadata.BuildpackMetadata build.Status.LatestImage = buildMetadata.LatestImage build.Status.LatestCacheImage = buildMetadata.LatestCacheImage + build.Status.LatestAttestationImage = attestDigest build.Status.Stack.RunImage = buildMetadata.StackRunImage build.Status.Stack.ID = buildMetadata.StackID } @@ -355,7 +398,106 @@ func (c *Reconciler) buildMetadataFromBuildPod(pod *corev1.Pod) (*cnb.BuildMetad return cnb.DecompressBuildMetadata(status.State.Terminated.Message) } } - return nil, errors.New(buildapi.CompletionContainerName + " container not found") + return nil, fmt.Errorf("%v container not found", buildapi.CompletionContainerName) +} + +func (c *Reconciler) attestBuild(ctx context.Context, build *buildapi.Build, buildMetadata *cnb.BuildMetadata, pod *corev1.Pod) (string, error) { + keychain, err := c.KeychainFactory.KeychainForSecretRef(ctx, registry.SecretRef{ + ServiceAccount: build.Spec.ServiceAccountName, + Namespace: build.Namespace, + ImagePullSecrets: build.Spec.Builder.ImagePullSecrets, + }) + if err != nil { + return "", err + } + + controllerSecrets, err := c.SecretFetcher.SecretsForSystemServiceAccount(ctx) + if err != nil { + return "", fmt.Errorf("failed to get controller secrets: %v", err) + } + + buildSecrets, err := c.SecretFetcher.SecretsForServiceAccount(ctx, build.ServiceAccount(), build.Namespace) + if err != nil { + return "", fmt.Errorf("failed to get service account secrets: %v", err) + } + + secrets := append(controllerSecrets, buildSecrets...) + signingKeys, err := secret.FilterAndExtractSLSASecrets(secrets) + if err != nil { + return "", fmt.Errorf("failed to parse slsa secrets: %v", err) + } + + signers := make([]slsa.Signer, len(signingKeys)) + for i, key := range signingKeys { + var s slsa.Signer + switch key.Type { + case secret.CosignKeyType: + s, err = slsa.NewCosignSigner(key.Key, key.Password, key.SecretName) + case secret.PKCS8KeyType: + s, err = slsa.NewPKCS8Signer(key.Key, key.SecretName) + } + if err != nil { + return "", fmt.Errorf("failed to create signer: %v", err) + } + signers[i] = s + } + + buildId := slsa.UnsignedBuildID + if len(signers) > 0 { + buildId = slsa.SignedBuildID + } + + deps, err := c.attestBuildDeps(ctx, build, pod, secrets) + if err != nil { + return "", fmt.Errorf("failed to gather build deps: %v", err) + } + + statement, err := c.Attester.AttestBuild(build, buildMetadata, pod, keychain, buildId, deps...) + if err != nil { + return "", fmt.Errorf("failed to generate statement: %v", err) + } + + payload, err := c.Attester.Sign(ctx, statement, signers...) + if err != nil { + return "", fmt.Errorf("failed to sign statement: %v", err) + } + + _, digest, err := c.Attester.Write(ctx, buildMetadata.LatestImage, payload, keychain) + if err != nil { + return "", fmt.Errorf("failed to write attestation: %v", err) + } + + return digest, nil +} + +func (c *Reconciler) attestBuildDeps(ctx context.Context, build *buildapi.Build, pod *corev1.Pod, secrets []*corev1.Secret) ([]slsa.BuilderDependencyFn, error) { + ns, err := c.K8sClient.CoreV1().Namespaces().Get(ctx, build.Namespace, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + sa, err := c.K8sClient.CoreV1().ServiceAccounts(build.Namespace).Get(ctx, build.ServiceAccount(), metav1.GetOptions{}) + if err != nil { + return nil, err + } + + deps := []slsa.BuilderDependencyFn{ + slsa.WithVersionedObject("Namespace", ns), + slsa.WithVersionedObject("Build", build), + slsa.WithVersionedObject("Pod", pod), + slsa.WithVersionedObject("ServiceAccount", sa), + } + + attestSecrets := make([]slsa.K8sObject, len(secrets)) + for i, v := range secrets { + attestSecrets[i] = slsa.K8sObject(v) + } + + if len(attestSecrets) != 0 { + deps = append(deps, slsa.WithVersionedObjects("Secrets", attestSecrets)) + } + + return deps, nil } func contains(arr []string, s string) bool { diff --git a/pkg/reconciler/build/build_test.go b/pkg/reconciler/build/build_test.go index 47657cec9..f7d7a8b3a 100644 --- a/pkg/reconciler/build/build_test.go +++ b/pkg/reconciler/build/build_test.go @@ -2,10 +2,20 @@ package build_test import ( "context" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "errors" "fmt" + "testing" + "time" + + "github.com/in-toto/in-toto-golang/in_toto" "github.com/sclevine/spec" + "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -21,21 +31,19 @@ import ( "k8s.io/client-go/tools/record" "knative.dev/pkg/controller" rtesting "knative.dev/pkg/reconciler/testing" - "os" - "path/filepath" - "testing" - "time" buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" "github.com/pivotal/kpack/pkg/buildpod" "github.com/pivotal/kpack/pkg/client/clientset/versioned/fake" "github.com/pivotal/kpack/pkg/cnb" + "github.com/pivotal/kpack/pkg/config" "github.com/pivotal/kpack/pkg/reconciler/build" "github.com/pivotal/kpack/pkg/reconciler/build/buildfakes" "github.com/pivotal/kpack/pkg/reconciler/testhelpers" "github.com/pivotal/kpack/pkg/registry" "github.com/pivotal/kpack/pkg/registry/registryfakes" + "github.com/pivotal/kpack/pkg/slsa" ) func TestBuildReconciler(t *testing.T) { @@ -49,16 +57,20 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { key = "some-namespace/build-name" serviceAccountName = "someserviceaccount" originalGeneration int64 = 1 + // {"buildpackMetadata":[{"id":"some-id","version":"some-version","homepage":"some-homepage"},{"id":"some-other-id","version":"some-other-version"}],"latestImage":"some-latest-image","latestCacheImage":"some-cache-image","stackRunImage":"some-run-image","stackID":"some-stack-id"} + compressedBuildMetadata = `H4sIAMLug2IAA32QsQ7CIBCG9z4FYW5fwFWXDi6uxuGEixBbaArt0vTdPSAQGqPLhfv+j7vA1jDGn4se5ATifUUPEjzwE7tTwNgWKylaEuPOjtjRsc14xdlpa0qW+yIoohO8sBgFRGNvf66xXuH8d1kyMk3zqD7CBT6AR+f7sd6dWKcjrKwzCIVHVQRUm87T/9wWc9TmxXxJ/aXEsQ9vaPbmAxPQpvpqAQAA` ) var ( - fakeMetadataRetriever = &buildfakes.FakeMetadataRetriever{} - keychainFactory = ®istryfakes.FakeKeychainFactory{} - podGenerator = &testPodGenerator{} - podProgressLogger = &testPodProgressLogger{} - ctx = context.Background() - injectedSidecarSupport = false - reactors = make([]reactor, 0) + fakeMetadataRetriever = &buildfakes.FakeMetadataRetriever{} + fakeAttester = &buildfakes.FakeSLSAAttester{} + fakeSecretFetcher = &buildfakes.FakeSecretFetcher{} + keychainFactory = ®istryfakes.FakeKeychainFactory{} + podGenerator = &testPodGenerator{} + podProgressLogger = &testPodProgressLogger{} + ctx = context.Background() + featureFlags = config.FeatureFlags{} + reactors = make([]reactor, 0) ) rt := testhelpers.ReconcilerTester(t, @@ -75,15 +87,17 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { eventList := rtesting.EventList{Recorder: eventRecorder} r := &build.Reconciler{ - K8sClient: k8sfakeClient, - Client: fakeClient, - KeychainFactory: keychainFactory, - Lister: listers.GetBuildLister(), - MetadataRetriever: fakeMetadataRetriever, - PodLister: listers.GetPodLister(), - PodGenerator: podGenerator, - PodProgressLogger: podProgressLogger, - InjectedSidecarSupport: injectedSidecarSupport, + K8sClient: k8sfakeClient, + Client: fakeClient, + KeychainFactory: keychainFactory, + Lister: listers.GetBuildLister(), + MetadataRetriever: fakeMetadataRetriever, + PodLister: listers.GetPodLister(), + PodGenerator: podGenerator, + PodProgressLogger: podProgressLogger, + Attester: fakeAttester, + SecretFetcher: fakeSecretFetcher, + FeatureFlags: featureFlags, } rtesting.PrependGenerateNameReactor(&fakeClient.Fake) @@ -540,15 +554,12 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { }, }, } - compressedBuildMetadata, err := os.ReadFile(filepath.Join("testdata", "metadata")) - require.NoError(t, err) - pod.Status.ContainerStatuses = []corev1.ContainerStatus{ { Name: "completion", State: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ - Message: string(compressedBuildMetadata), + Message: compressedBuildMetadata, }, }, }, @@ -614,7 +625,7 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { }, { Terminated: &corev1.ContainerStateTerminated{ - Message: string(compressedBuildMetadata), + Message: compressedBuildMetadata, }, }, }, @@ -1107,8 +1118,9 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { }) }) }) + when("pod needs cleanup", func() { - injectedSidecarSupport = true + featureFlags.InjectedSidecarSupport = true var startTime = time.Now() it("updates activeDeadlineSeconds when a build terminates", func() { @@ -1214,9 +1226,6 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { }) it("marks build as successful if completion completes even if pod fails", func() { - compressedBuildMetadata, err := os.ReadFile(filepath.Join("testdata", "metadata")) - require.NoError(t, err) - deadline := int64(1) pod := &corev1.Pod{ TypeMeta: metav1.TypeMeta{}, @@ -1245,7 +1254,7 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { Terminated: &corev1.ContainerStateTerminated{ ExitCode: 0, Reason: "Terminated", - Message: string(compressedBuildMetadata), + Message: compressedBuildMetadata, ContainerID: "container.ID", }, }, @@ -1283,7 +1292,7 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { Terminated: &corev1.ContainerStateTerminated{ ExitCode: 0, Reason: "Terminated", - Message: string(compressedBuildMetadata), + Message: compressedBuildMetadata, ContainerID: "container.ID", }, }, @@ -1339,7 +1348,7 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { Terminated: &corev1.ContainerStateTerminated{ ExitCode: 0, Reason: "Terminated", - Message: string(compressedBuildMetadata), + Message: compressedBuildMetadata, ContainerID: "container.ID", }, }, @@ -1354,6 +1363,317 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { }) }) }) + + when("attestation is enabled", func() { + var ( + startTime = time.Now() + endTime = startTime.Add(5 * time.Minute) + + makeSecret = func(t *testing.T, alg string) *corev1.Secret { + t.Helper() + data := make(map[string][]byte) + switch alg { + case "cosign": + cosignKey, err := cosign.GenerateKeyPair(func(bool) ([]byte, error) { return nil, nil }) + require.NoError(t, err) + data["cosign.password"] = []byte("") + data["cosign.key"] = cosignKey.PrivateBytes + case "ed25519": + _, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + key, err := x509.MarshalPKCS8PrivateKey(priv) + require.NoError(t, err) + data["ssh-privatekey"] = pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: key, + }) + + case "rsa": + fallthrough + default: + priv, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(t, err) + key, err := x509.MarshalPKCS8PrivateKey(priv) + require.NoError(t, err) + data["ssh-privatekey"] = pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: key, + }) + + } + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%v-secret", alg), + Namespace: bld.GetNamespace(), + ResourceVersion: "4", + Annotations: map[string]string{ + "kpack.io/slsa": "", + }, + }, + Data: data, + } + } + + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: bld.GetName() + "-build-pod", + Namespace: bld.GetNamespace(), + ResourceVersion: "1", + }, + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + {Name: "prepare", Image: "prepare-image"}, + }, + Containers: []corev1.Container{ + {Name: "completion", Image: "completion-image"}, + }, + NodeName: "some-node", + }, + Status: corev1.PodStatus{ + InitContainerStatuses: []corev1.ContainerStatus{ + { + Name: "prepare", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Terminated", + Message: "Message", + ContainerID: "container.ID", + StartedAt: metav1.NewTime(startTime), + FinishedAt: metav1.NewTime(endTime), + }, + }, + }, + }, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "completion", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Terminated", + Message: compressedBuildMetadata, + ContainerID: "container.ID", + StartedAt: metav1.NewTime(startTime), + FinishedAt: metav1.NewTime(endTime), + }, + }, + }, + }, + Phase: corev1.PodSucceeded, + }, + } + ns = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: bld.GetNamespace(), + ResourceVersion: "2", + }, + } + + rsaSecret *corev1.Secret + ed25519Secret *corev1.Secret + cosignSecret *corev1.Secret + + sa = &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: bld.ServiceAccount(), + Namespace: bld.GetNamespace(), + ResourceVersion: "5", + }, + } + + expectedStatus = buildapi.BuildStatus{ + Status: corev1alpha1.Status{ + ObservedGeneration: originalGeneration, + Conditions: corev1alpha1.Conditions{ + { + Type: corev1alpha1.ConditionSucceeded, + Status: corev1.ConditionTrue, + Reason: build.ReasonCompleted, + }, + }, + }, + PodName: "build-name-build-pod", + BuildMetadata: corev1alpha1.BuildpackMetadataList{ + { + Id: "some-id", + Version: "some-version", + Homepage: "some-homepage", + }, + { + Id: "some-other-id", + Version: "some-other-version", + }, + }, + LatestImage: "some-latest-image", + LatestCacheImage: "some-cache-image", + LatestAttestationImage: "some-attestation-image", + Stack: corev1alpha1.BuildStack{ + RunImage: "some-run-image", + ID: "some-stack-id", + }, + StepStates: []corev1.ContainerState{ + { + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Terminated", + Message: "Message", + ContainerID: "container.ID", + StartedAt: metav1.NewTime(startTime), + FinishedAt: metav1.NewTime(endTime), + }, + }, + { + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Terminated", + Message: compressedBuildMetadata, + ContainerID: "container.ID", + StartedAt: metav1.NewTime(startTime), + FinishedAt: metav1.NewTime(endTime), + }, + }, + }, + StepsCompleted: []string{ + "prepare", + "completion", + }, + } + ) + + featureFlags.GenerateSlsaAttestation = true + bld.ResourceVersion = "0" + + it.Before(func() { + fakeAttester.AttestBuildReturns(in_toto.Statement{}, nil) + fakeAttester.WriteReturns(nil, "some-attestation-image", nil) + fakeSecretFetcher.SecretsForServiceAccountReturns([]*corev1.Secret{}, nil) + fakeSecretFetcher.SecretsForSystemServiceAccountReturns([]*corev1.Secret{}, nil) + + appImageSecretRef := registry.SecretRef{ + ServiceAccount: bld.ServiceAccount(), + Namespace: bld.Namespace, + ImagePullSecrets: bld.BuilderSpec().ImagePullSecrets, + } + appImageKeychain := ®istryfakes.FakeKeychain{} + keychainFactory.AddKeychainForSecretRef(t, appImageSecretRef, appImageKeychain) + + rsaSecret = makeSecret(t, "rsa") + ed25519Secret = makeSecret(t, "ed25519") + cosignSecret = makeSecret(t, "cosign") + }) + + it("generates unsigned attestation when there's no secrets", func() { + rt.Test(rtesting.TableRow{ + Key: key, + Objects: []runtime.Object{ + ns, sa, rsaSecret, ed25519Secret, cosignSecret, + bld, + pod, + }, + WantErr: false, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{ + { + Object: &buildapi.Build{ + ObjectMeta: bld.ObjectMeta, + Spec: bld.Spec, + Status: expectedStatus, + }, + }, + }, + }) + + require.Equal(t, 1, fakeAttester.AttestBuildCallCount()) + _, _, _, _, id, deps := fakeAttester.AttestBuildArgsForCall(0) + require.Equal(t, slsa.BuilderID("https://kpack.io/slsa/unsigned-build"), id) + require.Len(t, deps, 4) + + require.Equal(t, 1, fakeAttester.SignCallCount()) + _, _, signer := fakeAttester.SignArgsForCall(0) + require.Len(t, signer, 0) + + require.Equal(t, 1, fakeAttester.WriteCallCount()) + _, img, _, _ := fakeAttester.WriteArgsForCall(0) + require.Equal(t, img, "some-latest-image") + }) + + it("generates signed attestation when there's secrets in builder service account", func() { + fakeSecretFetcher.SecretsForServiceAccountReturns([]*corev1.Secret{ + rsaSecret, + }, nil) + + rt.Test(rtesting.TableRow{ + Key: key, + Objects: []runtime.Object{ + ns, sa, rsaSecret, ed25519Secret, cosignSecret, + bld, + pod, + }, + WantErr: false, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{ + { + Object: &buildapi.Build{ + ObjectMeta: bld.ObjectMeta, + Spec: bld.Spec, + Status: expectedStatus, + }, + }, + }, + }) + + require.Equal(t, 1, fakeAttester.AttestBuildCallCount()) + _, _, _, _, id, deps := fakeAttester.AttestBuildArgsForCall(0) + require.Equal(t, slsa.BuilderID("https://kpack.io/slsa/signed-build"), id) + require.Len(t, deps, 5) + + require.Equal(t, 1, fakeAttester.SignCallCount()) + _, _, signer := fakeAttester.SignArgsForCall(0) + require.Len(t, signer, 1) + + require.Equal(t, 1, fakeAttester.WriteCallCount()) + _, img, _, _ := fakeAttester.WriteArgsForCall(0) + require.Equal(t, img, "some-latest-image") + }) + it("generates signed attestation when there's secrets in system service account", func() { + fakeSecretFetcher.SecretsForSystemServiceAccountReturns([]*corev1.Secret{ + cosignSecret, ed25519Secret, + }, nil) + + rt.Test(rtesting.TableRow{ + Key: key, + Objects: []runtime.Object{ + ns, sa, rsaSecret, ed25519Secret, cosignSecret, + bld, + pod, + }, + WantErr: false, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{ + { + Object: &buildapi.Build{ + ObjectMeta: bld.ObjectMeta, + Spec: bld.Spec, + Status: expectedStatus, + }, + }, + }, + }) + + require.Equal(t, 1, fakeAttester.AttestBuildCallCount()) + _, _, _, _, id, deps := fakeAttester.AttestBuildArgsForCall(0) + require.Equal(t, slsa.BuilderID("https://kpack.io/slsa/signed-build"), id) + require.Len(t, deps, 5) + + require.Equal(t, 1, fakeAttester.SignCallCount()) + _, _, signer := fakeAttester.SignArgsForCall(0) + require.Len(t, signer, 2) + + require.Equal(t, 1, fakeAttester.WriteCallCount()) + _, img, _, _ := fakeAttester.WriteArgsForCall(0) + require.Equal(t, img, "some-latest-image") + }) + + }) }) } diff --git a/pkg/reconciler/build/buildfakes/fake_secret_fetcher.go b/pkg/reconciler/build/buildfakes/fake_secret_fetcher.go new file mode 100644 index 000000000..24b6abe00 --- /dev/null +++ b/pkg/reconciler/build/buildfakes/fake_secret_fetcher.go @@ -0,0 +1,201 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package buildfakes + +import ( + "context" + "sync" + + "github.com/pivotal/kpack/pkg/reconciler/build" + v1 "k8s.io/api/core/v1" +) + +type FakeSecretFetcher struct { + SecretsForServiceAccountStub func(context.Context, string, string) ([]*v1.Secret, error) + secretsForServiceAccountMutex sync.RWMutex + secretsForServiceAccountArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 string + } + secretsForServiceAccountReturns struct { + result1 []*v1.Secret + result2 error + } + secretsForServiceAccountReturnsOnCall map[int]struct { + result1 []*v1.Secret + result2 error + } + SecretsForSystemServiceAccountStub func(context.Context) ([]*v1.Secret, error) + secretsForSystemServiceAccountMutex sync.RWMutex + secretsForSystemServiceAccountArgsForCall []struct { + arg1 context.Context + } + secretsForSystemServiceAccountReturns struct { + result1 []*v1.Secret + result2 error + } + secretsForSystemServiceAccountReturnsOnCall map[int]struct { + result1 []*v1.Secret + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeSecretFetcher) SecretsForServiceAccount(arg1 context.Context, arg2 string, arg3 string) ([]*v1.Secret, error) { + fake.secretsForServiceAccountMutex.Lock() + ret, specificReturn := fake.secretsForServiceAccountReturnsOnCall[len(fake.secretsForServiceAccountArgsForCall)] + fake.secretsForServiceAccountArgsForCall = append(fake.secretsForServiceAccountArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 string + }{arg1, arg2, arg3}) + stub := fake.SecretsForServiceAccountStub + fakeReturns := fake.secretsForServiceAccountReturns + fake.recordInvocation("SecretsForServiceAccount", []interface{}{arg1, arg2, arg3}) + fake.secretsForServiceAccountMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSecretFetcher) SecretsForServiceAccountCallCount() int { + fake.secretsForServiceAccountMutex.RLock() + defer fake.secretsForServiceAccountMutex.RUnlock() + return len(fake.secretsForServiceAccountArgsForCall) +} + +func (fake *FakeSecretFetcher) SecretsForServiceAccountCalls(stub func(context.Context, string, string) ([]*v1.Secret, error)) { + fake.secretsForServiceAccountMutex.Lock() + defer fake.secretsForServiceAccountMutex.Unlock() + fake.SecretsForServiceAccountStub = stub +} + +func (fake *FakeSecretFetcher) SecretsForServiceAccountArgsForCall(i int) (context.Context, string, string) { + fake.secretsForServiceAccountMutex.RLock() + defer fake.secretsForServiceAccountMutex.RUnlock() + argsForCall := fake.secretsForServiceAccountArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeSecretFetcher) SecretsForServiceAccountReturns(result1 []*v1.Secret, result2 error) { + fake.secretsForServiceAccountMutex.Lock() + defer fake.secretsForServiceAccountMutex.Unlock() + fake.SecretsForServiceAccountStub = nil + fake.secretsForServiceAccountReturns = struct { + result1 []*v1.Secret + result2 error + }{result1, result2} +} + +func (fake *FakeSecretFetcher) SecretsForServiceAccountReturnsOnCall(i int, result1 []*v1.Secret, result2 error) { + fake.secretsForServiceAccountMutex.Lock() + defer fake.secretsForServiceAccountMutex.Unlock() + fake.SecretsForServiceAccountStub = nil + if fake.secretsForServiceAccountReturnsOnCall == nil { + fake.secretsForServiceAccountReturnsOnCall = make(map[int]struct { + result1 []*v1.Secret + result2 error + }) + } + fake.secretsForServiceAccountReturnsOnCall[i] = struct { + result1 []*v1.Secret + result2 error + }{result1, result2} +} + +func (fake *FakeSecretFetcher) SecretsForSystemServiceAccount(arg1 context.Context) ([]*v1.Secret, error) { + fake.secretsForSystemServiceAccountMutex.Lock() + ret, specificReturn := fake.secretsForSystemServiceAccountReturnsOnCall[len(fake.secretsForSystemServiceAccountArgsForCall)] + fake.secretsForSystemServiceAccountArgsForCall = append(fake.secretsForSystemServiceAccountArgsForCall, struct { + arg1 context.Context + }{arg1}) + stub := fake.SecretsForSystemServiceAccountStub + fakeReturns := fake.secretsForSystemServiceAccountReturns + fake.recordInvocation("SecretsForSystemServiceAccount", []interface{}{arg1}) + fake.secretsForSystemServiceAccountMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSecretFetcher) SecretsForSystemServiceAccountCallCount() int { + fake.secretsForSystemServiceAccountMutex.RLock() + defer fake.secretsForSystemServiceAccountMutex.RUnlock() + return len(fake.secretsForSystemServiceAccountArgsForCall) +} + +func (fake *FakeSecretFetcher) SecretsForSystemServiceAccountCalls(stub func(context.Context) ([]*v1.Secret, error)) { + fake.secretsForSystemServiceAccountMutex.Lock() + defer fake.secretsForSystemServiceAccountMutex.Unlock() + fake.SecretsForSystemServiceAccountStub = stub +} + +func (fake *FakeSecretFetcher) SecretsForSystemServiceAccountArgsForCall(i int) context.Context { + fake.secretsForSystemServiceAccountMutex.RLock() + defer fake.secretsForSystemServiceAccountMutex.RUnlock() + argsForCall := fake.secretsForSystemServiceAccountArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeSecretFetcher) SecretsForSystemServiceAccountReturns(result1 []*v1.Secret, result2 error) { + fake.secretsForSystemServiceAccountMutex.Lock() + defer fake.secretsForSystemServiceAccountMutex.Unlock() + fake.SecretsForSystemServiceAccountStub = nil + fake.secretsForSystemServiceAccountReturns = struct { + result1 []*v1.Secret + result2 error + }{result1, result2} +} + +func (fake *FakeSecretFetcher) SecretsForSystemServiceAccountReturnsOnCall(i int, result1 []*v1.Secret, result2 error) { + fake.secretsForSystemServiceAccountMutex.Lock() + defer fake.secretsForSystemServiceAccountMutex.Unlock() + fake.SecretsForSystemServiceAccountStub = nil + if fake.secretsForSystemServiceAccountReturnsOnCall == nil { + fake.secretsForSystemServiceAccountReturnsOnCall = make(map[int]struct { + result1 []*v1.Secret + result2 error + }) + } + fake.secretsForSystemServiceAccountReturnsOnCall[i] = struct { + result1 []*v1.Secret + result2 error + }{result1, result2} +} + +func (fake *FakeSecretFetcher) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.secretsForServiceAccountMutex.RLock() + defer fake.secretsForServiceAccountMutex.RUnlock() + fake.secretsForSystemServiceAccountMutex.RLock() + defer fake.secretsForSystemServiceAccountMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeSecretFetcher) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ build.SecretFetcher = new(FakeSecretFetcher) diff --git a/pkg/reconciler/build/buildfakes/fake_slsaattester.go b/pkg/reconciler/build/buildfakes/fake_slsaattester.go new file mode 100644 index 000000000..f828cfd2f --- /dev/null +++ b/pkg/reconciler/build/buildfakes/fake_slsaattester.go @@ -0,0 +1,313 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package buildfakes + +import ( + "context" + "sync" + + "github.com/google/go-containerregistry/pkg/authn" + v1a "github.com/google/go-containerregistry/pkg/v1" + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + "github.com/pivotal/kpack/pkg/cnb" + "github.com/pivotal/kpack/pkg/reconciler/build" + "github.com/pivotal/kpack/pkg/slsa" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + v1 "k8s.io/api/core/v1" +) + +type FakeSLSAAttester struct { + AttestBuildStub func(*v1alpha2.Build, *cnb.BuildMetadata, *v1.Pod, authn.Keychain, slsa.BuilderID, ...slsa.BuilderDependencyFn) (in_toto.Statement, error) + attestBuildMutex sync.RWMutex + attestBuildArgsForCall []struct { + arg1 *v1alpha2.Build + arg2 *cnb.BuildMetadata + arg3 *v1.Pod + arg4 authn.Keychain + arg5 slsa.BuilderID + arg6 []slsa.BuilderDependencyFn + } + attestBuildReturns struct { + result1 in_toto.Statement + result2 error + } + attestBuildReturnsOnCall map[int]struct { + result1 in_toto.Statement + result2 error + } + SignStub func(context.Context, in_toto.Statement, ...dsse.Signer) ([]byte, error) + signMutex sync.RWMutex + signArgsForCall []struct { + arg1 context.Context + arg2 in_toto.Statement + arg3 []dsse.Signer + } + signReturns struct { + result1 []byte + result2 error + } + signReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + WriteStub func(context.Context, string, []byte, authn.Keychain) (v1a.Image, string, error) + writeMutex sync.RWMutex + writeArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 []byte + arg4 authn.Keychain + } + writeReturns struct { + result1 v1a.Image + result2 string + result3 error + } + writeReturnsOnCall map[int]struct { + result1 v1a.Image + result2 string + result3 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeSLSAAttester) AttestBuild(arg1 *v1alpha2.Build, arg2 *cnb.BuildMetadata, arg3 *v1.Pod, arg4 authn.Keychain, arg5 slsa.BuilderID, arg6 ...slsa.BuilderDependencyFn) (in_toto.Statement, error) { + fake.attestBuildMutex.Lock() + ret, specificReturn := fake.attestBuildReturnsOnCall[len(fake.attestBuildArgsForCall)] + fake.attestBuildArgsForCall = append(fake.attestBuildArgsForCall, struct { + arg1 *v1alpha2.Build + arg2 *cnb.BuildMetadata + arg3 *v1.Pod + arg4 authn.Keychain + arg5 slsa.BuilderID + arg6 []slsa.BuilderDependencyFn + }{arg1, arg2, arg3, arg4, arg5, arg6}) + stub := fake.AttestBuildStub + fakeReturns := fake.attestBuildReturns + fake.recordInvocation("AttestBuild", []interface{}{arg1, arg2, arg3, arg4, arg5, arg6}) + fake.attestBuildMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5, arg6...) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSLSAAttester) AttestBuildCallCount() int { + fake.attestBuildMutex.RLock() + defer fake.attestBuildMutex.RUnlock() + return len(fake.attestBuildArgsForCall) +} + +func (fake *FakeSLSAAttester) AttestBuildCalls(stub func(*v1alpha2.Build, *cnb.BuildMetadata, *v1.Pod, authn.Keychain, slsa.BuilderID, ...slsa.BuilderDependencyFn) (in_toto.Statement, error)) { + fake.attestBuildMutex.Lock() + defer fake.attestBuildMutex.Unlock() + fake.AttestBuildStub = stub +} + +func (fake *FakeSLSAAttester) AttestBuildArgsForCall(i int) (*v1alpha2.Build, *cnb.BuildMetadata, *v1.Pod, authn.Keychain, slsa.BuilderID, []slsa.BuilderDependencyFn) { + fake.attestBuildMutex.RLock() + defer fake.attestBuildMutex.RUnlock() + argsForCall := fake.attestBuildArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6 +} + +func (fake *FakeSLSAAttester) AttestBuildReturns(result1 in_toto.Statement, result2 error) { + fake.attestBuildMutex.Lock() + defer fake.attestBuildMutex.Unlock() + fake.AttestBuildStub = nil + fake.attestBuildReturns = struct { + result1 in_toto.Statement + result2 error + }{result1, result2} +} + +func (fake *FakeSLSAAttester) AttestBuildReturnsOnCall(i int, result1 in_toto.Statement, result2 error) { + fake.attestBuildMutex.Lock() + defer fake.attestBuildMutex.Unlock() + fake.AttestBuildStub = nil + if fake.attestBuildReturnsOnCall == nil { + fake.attestBuildReturnsOnCall = make(map[int]struct { + result1 in_toto.Statement + result2 error + }) + } + fake.attestBuildReturnsOnCall[i] = struct { + result1 in_toto.Statement + result2 error + }{result1, result2} +} + +func (fake *FakeSLSAAttester) Sign(arg1 context.Context, arg2 in_toto.Statement, arg3 ...dsse.Signer) ([]byte, error) { + fake.signMutex.Lock() + ret, specificReturn := fake.signReturnsOnCall[len(fake.signArgsForCall)] + fake.signArgsForCall = append(fake.signArgsForCall, struct { + arg1 context.Context + arg2 in_toto.Statement + arg3 []dsse.Signer + }{arg1, arg2, arg3}) + stub := fake.SignStub + fakeReturns := fake.signReturns + fake.recordInvocation("Sign", []interface{}{arg1, arg2, arg3}) + fake.signMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3...) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSLSAAttester) SignCallCount() int { + fake.signMutex.RLock() + defer fake.signMutex.RUnlock() + return len(fake.signArgsForCall) +} + +func (fake *FakeSLSAAttester) SignCalls(stub func(context.Context, in_toto.Statement, ...dsse.Signer) ([]byte, error)) { + fake.signMutex.Lock() + defer fake.signMutex.Unlock() + fake.SignStub = stub +} + +func (fake *FakeSLSAAttester) SignArgsForCall(i int) (context.Context, in_toto.Statement, []dsse.Signer) { + fake.signMutex.RLock() + defer fake.signMutex.RUnlock() + argsForCall := fake.signArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeSLSAAttester) SignReturns(result1 []byte, result2 error) { + fake.signMutex.Lock() + defer fake.signMutex.Unlock() + fake.SignStub = nil + fake.signReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeSLSAAttester) SignReturnsOnCall(i int, result1 []byte, result2 error) { + fake.signMutex.Lock() + defer fake.signMutex.Unlock() + fake.SignStub = nil + if fake.signReturnsOnCall == nil { + fake.signReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.signReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeSLSAAttester) Write(arg1 context.Context, arg2 string, arg3 []byte, arg4 authn.Keychain) (v1a.Image, string, error) { + var arg3Copy []byte + if arg3 != nil { + arg3Copy = make([]byte, len(arg3)) + copy(arg3Copy, arg3) + } + fake.writeMutex.Lock() + ret, specificReturn := fake.writeReturnsOnCall[len(fake.writeArgsForCall)] + fake.writeArgsForCall = append(fake.writeArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 []byte + arg4 authn.Keychain + }{arg1, arg2, arg3Copy, arg4}) + stub := fake.WriteStub + fakeReturns := fake.writeReturns + fake.recordInvocation("Write", []interface{}{arg1, arg2, arg3Copy, arg4}) + fake.writeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeSLSAAttester) WriteCallCount() int { + fake.writeMutex.RLock() + defer fake.writeMutex.RUnlock() + return len(fake.writeArgsForCall) +} + +func (fake *FakeSLSAAttester) WriteCalls(stub func(context.Context, string, []byte, authn.Keychain) (v1a.Image, string, error)) { + fake.writeMutex.Lock() + defer fake.writeMutex.Unlock() + fake.WriteStub = stub +} + +func (fake *FakeSLSAAttester) WriteArgsForCall(i int) (context.Context, string, []byte, authn.Keychain) { + fake.writeMutex.RLock() + defer fake.writeMutex.RUnlock() + argsForCall := fake.writeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeSLSAAttester) WriteReturns(result1 v1a.Image, result2 string, result3 error) { + fake.writeMutex.Lock() + defer fake.writeMutex.Unlock() + fake.WriteStub = nil + fake.writeReturns = struct { + result1 v1a.Image + result2 string + result3 error + }{result1, result2, result3} +} + +func (fake *FakeSLSAAttester) WriteReturnsOnCall(i int, result1 v1a.Image, result2 string, result3 error) { + fake.writeMutex.Lock() + defer fake.writeMutex.Unlock() + fake.WriteStub = nil + if fake.writeReturnsOnCall == nil { + fake.writeReturnsOnCall = make(map[int]struct { + result1 v1a.Image + result2 string + result3 error + }) + } + fake.writeReturnsOnCall[i] = struct { + result1 v1a.Image + result2 string + result3 error + }{result1, result2, result3} +} + +func (fake *FakeSLSAAttester) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.attestBuildMutex.RLock() + defer fake.attestBuildMutex.RUnlock() + fake.signMutex.RLock() + defer fake.signMutex.RUnlock() + fake.writeMutex.RLock() + defer fake.writeMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeSLSAAttester) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ build.SLSAAttester = new(FakeSLSAAttester) diff --git a/pkg/reconciler/build/testdata/metadata b/pkg/reconciler/build/testdata/metadata deleted file mode 100644 index d7de2fae8..000000000 --- a/pkg/reconciler/build/testdata/metadata +++ /dev/null @@ -1 +0,0 @@ -H4sIAMLug2IAA32QsQ7CIBCG9z4FYW5fwFWXDi6uxuGEixBbaArt0vTdPSAQGqPLhfv+j7vA1jDGn4se5ATifUUPEjzwE7tTwNgWKylaEuPOjtjRsc14xdlpa0qW+yIoohO8sBgFRGNvf66xXuH8d1kyMk3zqD7CBT6AR+f7sd6dWKcjrKwzCIVHVQRUm87T/9wWc9TmxXxJ/aXEsQ9vaPbmAxPQpvpqAQAA diff --git a/pkg/registry/fetch.go b/pkg/registry/fetch.go index 0e0299ef6..d57cea6fe 100644 --- a/pkg/registry/fetch.go +++ b/pkg/registry/fetch.go @@ -3,8 +3,11 @@ package registry import ( "log" "os" + "path" "path/filepath" + "strings" + "github.com/BurntSushi/toml" "github.com/google/go-containerregistry/pkg/authn" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/pkg/errors" @@ -34,14 +37,17 @@ type Fetcher struct { Keychain authn.Keychain } -func (f *Fetcher) Fetch(dir, registryImage string) error { +func (f *Fetcher) Fetch(dir, registryImage string, metadataDir string) error { f.Logger.Printf("Pulling %s...", registryImage) - img, _, err := f.Client.Fetch(f.Keychain, registryImage) + img, identifer, err := f.Client.Fetch(f.Keychain, registryImage) if err != nil { return err } + parts := strings.SplitN(identifer, "@", 2) + ref, digest := parts[0], parts[1] + cType, err := getContentType(img) if err != nil { return err @@ -59,10 +65,31 @@ func (f *Fetcher) Fetch(dir, registryImage string) error { handler = handleSource } - if err := handler(img, dir); err != nil { + if err = handler(img, dir); err != nil { return err } + projectMetadataFile, err := os.Create(path.Join(metadataDir, "project-metadata.toml")) + if err != nil { + return errors.Wrapf(err, "invalid metadata destination '%s/project-metadata.toml' for image: %s", metadataDir, registryImage) + } + defer projectMetadataFile.Close() + + projectMd := Project{ + Source: Source{ + Type: "image", + Metadata: Metadata{ + Image: ref, + }, + Version: Version{ + Digest: digest, + }, + }, + } + if err := toml.NewEncoder(projectMetadataFile).Encode(projectMd); err != nil { + return errors.Wrapf(err, "invalid metadata destination '%s/project-metadata.toml' for image: %s", metadataDir, registryImage) + } + f.Logger.Printf("Successfully pulled %s in path %q", registryImage, dir) return nil @@ -199,3 +226,21 @@ func fetchLayer(layer v1.Layer, dir string) error { return archive.ExtractTar(reader, dir, 0) } + +type Project struct { + Source Source `toml:"source"` +} + +type Source struct { + Type string `toml:"type"` + Metadata Metadata `toml:"metadata"` + Version Version `toml:"version"` +} + +type Metadata struct { + Image string `toml:"image"` +} + +type Version struct { + Digest string `toml:"digest"` +} diff --git a/pkg/registry/fetch_test.go b/pkg/registry/fetch_test.go index b66b091a3..b0bd4968e 100644 --- a/pkg/registry/fetch_test.go +++ b/pkg/registry/fetch_test.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "os" + "path" "path/filepath" "testing" "time" @@ -36,13 +37,17 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { Client: client, Keychain: keychain, } - dir string + dir string + metadataDir string ) it.Before(func() { var err error dir, err = os.MkdirTemp("", "") require.NoError(t, err) + + metadataDir, err = os.MkdirTemp("", "test-git") + require.NoError(t, err) }) it.After(func() { @@ -63,7 +68,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { repoName := fmt.Sprintf("registry.example/some-image-%d", time.Now().Second()) client.AddImage(repoName, img, keychain) - err = fetcher.Fetch(dir, repoName) + err = fetcher.Fetch(dir, repoName, metadataDir) require.NoError(t, err) files, err := os.ReadDir(dir) @@ -99,7 +104,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { repoName := fmt.Sprintf("registry.example/some-image-%d", time.Now().Second()) client.AddImage(repoName, img, keychain) - err = fetcher.Fetch(dir, repoName) + err = fetcher.Fetch(dir, repoName, metadataDir) require.NoError(t, err) files, err := os.ReadDir(dir) @@ -126,7 +131,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { repoName := "registry.example/test-exe" client.AddImage(repoName, img, keychain) - err = fetcher.Fetch(dir, repoName) + err = fetcher.Fetch(dir, repoName, metadataDir) require.NoError(t, err) // the vendor/cache directory doesnt have proper headers @@ -145,7 +150,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { repoName := "registry.example/test-exe" client.AddImage(repoName, img, keychain) - err = fetcher.Fetch(dir, repoName) + err = fetcher.Fetch(dir, repoName, metadataDir) require.NoError(t, err) files, err := os.ReadDir(dir) @@ -169,11 +174,37 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { require.Equal(t, 0755, int(info.Mode())) }) + it("records project-metadata.toml", func() { + buf, err := os.ReadFile(filepath.Join("testdata", "reg.tar")) + require.NoError(t, err) + + img := createSourceImage(t, buf, "") + + repoName := "registry.example/some-image" + client.AddImage(repoName, img, keychain) + + err = fetcher.Fetch(dir, repoName, metadataDir) + require.NoError(t, err) + + p := path.Join(metadataDir, "project-metadata.toml") + contents, err := os.ReadFile(p) + require.NoError(t, err) + + expectedFile := `[source] + type = "image" + [source.metadata] + image = "registry.example/some-image" + [source.version] + digest = "sha256:0a2b3075a370ed209cc262ca189b56f0e09fee17dc69ab99a479f07168818374" +` + require.Equal(t, expectedFile, string(contents)) + }) + it("errors when the registry is inaccessible", func() { registryError := errors.New("some registry error") client.SetFetchError(registryError) - err := fetcher.Fetch(dir, "registry.example/error") + err := fetcher.Fetch(dir, "registry.example/error", metadataDir) require.Equal(t, err, registryError) }) } diff --git a/pkg/secret/constants.go b/pkg/secret/constants.go new file mode 100644 index 000000000..25cd2af5c --- /dev/null +++ b/pkg/secret/constants.go @@ -0,0 +1,15 @@ +package secret + +const ( + CosignSecretPrivateKey = "cosign.key" + CosignSecretPassword = "cosign.password" + CosignSecretPublicKey = "cosign.pub" + + CosignDockerMediaTypesAnnotation = "kpack.io/cosign.docker-media-types" + CosignRepositoryAnnotation = "kpack.io/cosign.repository" + + PKCS8SecretKey = "ssh-privatekey" + + SLSASecretAnnotation = "kpack.io/slsa" + SLSADockerMediaTypesAnnotation = "kpack.io/slsa.docker-media-types" +) diff --git a/pkg/secret/fetcher.go b/pkg/secret/fetcher.go index 62d1afde1..683ac4369 100644 --- a/pkg/secret/fetcher.go +++ b/pkg/secret/fetcher.go @@ -11,6 +11,9 @@ import ( type Fetcher struct { Client k8sclient.Interface + + SystemNamespace string + SystemServiceAccountName string } func (f *Fetcher) SecretsForServiceAccount(ctx context.Context, serviceAccount, namespace string) ([]*corev1.Secret, error) { @@ -34,3 +37,7 @@ func (f *Fetcher) secretsFromServiceAccount(ctx context.Context, account *corev1 } return secrets, nil } + +func (f *Fetcher) SecretsForSystemServiceAccount(ctx context.Context) ([]*corev1.Secret, error) { + return f.SecretsForServiceAccount(ctx, f.SystemServiceAccountName, f.SystemNamespace) +} diff --git a/pkg/secret/signing_secret.go b/pkg/secret/signing_secret.go new file mode 100644 index 000000000..160c1b64a --- /dev/null +++ b/pkg/secret/signing_secret.go @@ -0,0 +1,96 @@ +package secret + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" +) + +type KeyType int + +const ( + CosignKeyType KeyType = iota + PKCS8KeyType +) + +type SigningKey struct { + SecretName string + Key []byte + Password []byte + Type KeyType +} + +func FilterCosignSigningSecrets(secrets []*corev1.Secret) []*corev1.Secret { + return filterCosignSecrets(secrets, "") +} + +func FilterAndExtractSLSASecrets(secrets []*corev1.Secret) ([]SigningKey, error) { + cosignSecrets := filterCosignSecrets(secrets, SLSASecretAnnotation) + privKeySecrets := filterPrivateKeySecrets(secrets, SLSASecretAnnotation) + + return extractAttestationKeyFromSecrets(cosignSecrets, privKeySecrets) +} + +func filterCosignSecrets(serviceAccountSecrets []*corev1.Secret, annotation string) []*corev1.Secret { + cosignSecrets := make([]*corev1.Secret, 0) + + for _, secret := range serviceAccountSecrets { + _, passwordOk := secret.Data[CosignSecretPassword] + _, keyOk := secret.Data[CosignSecretPrivateKey] + _, annotationOk := secret.Annotations[annotation] + + if passwordOk && keyOk && (annotation == "" || annotationOk) { + cosignSecrets = append(cosignSecrets, secret) + } + } + + return cosignSecrets +} + +func filterPrivateKeySecrets(serviceAccountSecrets []*corev1.Secret, annotation string) []*corev1.Secret { + secrets := make([]*corev1.Secret, 0) + + for _, secret := range serviceAccountSecrets { + _, keyOk := secret.Data[PKCS8SecretKey] + _, annotationOk := secret.Annotations[annotation] + + if keyOk && (annotation == "" || annotationOk) { + secrets = append(secrets, secret) + } + } + + return secrets +} + +func extractAttestationKeyFromSecrets(cosignSecrets []*corev1.Secret, pkcs8Secrets []*corev1.Secret) ([]SigningKey, error) { + cosignKeys, err := getKey(cosignSecrets, CosignSecretPrivateKey, CosignSecretPassword, CosignKeyType) + if err != nil { + return nil, fmt.Errorf("getting cosign keys: %v", err) + } + + pkcs8Keys, err := getKey(pkcs8Secrets, PKCS8SecretKey, "", PKCS8KeyType) + if err != nil { + return nil, fmt.Errorf("getting pkcs#8 keys: %v", err) + } + + return append(cosignKeys, pkcs8Keys...), nil +} + +func getKey(secrets []*corev1.Secret, keyField, passField string, keyType KeyType) ([]SigningKey, error) { + privKeys := make([]SigningKey, 0) + for _, s := range secrets { + privKey, keyOk := s.Data[keyField] + if !keyOk { + return nil, fmt.Errorf("missing '%v' field: %v", keyField, s.Name) + } + password := s.Data[passField] + + privKeys = append(privKeys, SigningKey{ + SecretName: s.Name, + Key: privKey, + Password: password, + Type: keyType, + }) + } + return privKeys, nil +} diff --git a/pkg/secret/signing_secret_test.go b/pkg/secret/signing_secret_test.go new file mode 100644 index 000000000..3801f2b4c --- /dev/null +++ b/pkg/secret/signing_secret_test.go @@ -0,0 +1,133 @@ +package secret_test + +import ( + "testing" + + "github.com/pivotal/kpack/pkg/secret" + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestSigingSecret(t *testing.T) { + spec.Run(t, "Test signing secrets", testSigingSecret) +} + +func testSigingSecret(t *testing.T, when spec.G, it spec.S) { + var ( + genericSecret1 = &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: "some-generic-secret-1"}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "some-field": []byte("some-value"), + }, + } + genericSecret2 = &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: "some-generic-secret-2"}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "some-other-field": []byte("some-other-value"), + }, + } + cosignSecret1 = &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: "some-cosign-secret-1"}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "cosign.key": []byte("some-key"), + "cosign.pub": []byte("some-key"), + "cosign.password": []byte("some-password"), + }, + } + cosignSecret2 = &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: "some-cosign-secret-2"}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "cosign.key": []byte("some-other-key"), + "cosign.pub": []byte("some-other-key"), + "cosign.password": []byte("some-other-password"), + }, + } + sshSecret = &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: "some-private-key-1"}, + Type: corev1.SecretTypeSSHAuth, + Data: map[string][]byte{ + "ssh-privatekey": []byte("some-private-key"), + }, + } + slsaSecret1 = &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-private-key-3", + Annotations: map[string]string{ + "kpack.io/slsa": "", + }, + }, + Type: corev1.SecretTypeSSHAuth, + Data: map[string][]byte{ + "ssh-privatekey": []byte("some-slsa-private-key"), + }, + } + slsaSecret2 = &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-cosign-secret-3", + Annotations: map[string]string{ + "kpack.io/slsa": "", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "cosign.key": []byte("some-slsa-cosign-key"), + "cosign.pub": []byte("some-slsa-cosign-key"), + "cosign.password": []byte("some-slsa-cosign-password"), + }, + } + + secrets = []*corev1.Secret{ + genericSecret1, + cosignSecret1, + slsaSecret1, + sshSecret, + slsaSecret2, + cosignSecret2, + genericSecret2, + } + ) + + it("filters slsa secrets", func() { + keys, err := secret.FilterAndExtractSLSASecrets(secrets) + require.NoError(t, err) + + expected := []secret.SigningKey{ + { + SecretName: "some-cosign-secret-3", + Key: []byte("some-slsa-cosign-key"), + Password: []byte("some-slsa-cosign-password"), + Type: secret.CosignKeyType, + }, + { + SecretName: "some-private-key-3", + Key: []byte("some-slsa-private-key"), + Type: secret.PKCS8KeyType, + }, + } + + require.Equal(t, keys, expected) + }) + + it("filters cosign secrets", func() { + actual := secret.FilterCosignSigningSecrets(secrets) + + require.Len(t, actual, 3) + require.Contains(t, actual, cosignSecret1) + require.Contains(t, actual, cosignSecret2) + require.Contains(t, actual, slsaSecret2) + }) + +} diff --git a/pkg/slsa/attest.go b/pkg/slsa/attest.go new file mode 100644 index 000000000..86242e9bd --- /dev/null +++ b/pkg/slsa/attest.go @@ -0,0 +1,247 @@ +package slsa + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + intoto "github.com/in-toto/in-toto-golang/in_toto" + slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + slsav1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + corev1 "k8s.io/api/core/v1" + + buildv1alpha2 "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + "github.com/pivotal/kpack/pkg/cnb" + "github.com/pivotal/kpack/pkg/config" +) + +type BuilderID string + +const ( + SignedBuildID BuilderID = "https://kpack.io/slsa/signed-build" + UnsignedBuildID BuilderID = "https://kpack.io/slsa/unsigned-build" + MediaTypeJSON = "application/json" +) + +type LifecycleProvider interface { + Metadata() (cnb.LifecycleMetadata, error) +} + +type ImageReader interface { + Read(keychain authn.Keychain, repoName string) (string, string, map[string]string, error) +} + +type Attester struct { + Version string + + ImageReader ImageReader + LifecycleProvider LifecycleProvider + + Images config.Images + Features config.FeatureFlags + Config config.Config +} + +func (a *Attester) AttestBuild(build *buildv1alpha2.Build, buildMetadata *cnb.BuildMetadata, pod *corev1.Pod, builderAndAppKeychain authn.Keychain, builderId BuilderID, depFns ...BuilderDependencyFn) (intoto.Statement, error) { + builderRepo, builderSha, builderLabels, err := a.ImageReader.Read(builderAndAppKeychain, build.Spec.Builder.Image) + if err != nil { + return intoto.Statement{}, fmt.Errorf("failed to read builder image: %v", err) + } + + appRepo, appSha, appLabels, err := a.ImageReader.Read(builderAndAppKeychain, buildMetadata.LatestImage) + if err != nil { + return intoto.Statement{}, fmt.Errorf("failed to read app image: %v", err) + } + + source, sourceDigest, err := extractSourceFromLabel(appLabels) + if err != nil { + return intoto.Statement{}, fmt.Errorf("failed to extract source from label: %v", err) + } + + start, stop := getStartStopTime(pod) + + lifecycle, err := a.LifecycleProvider.Metadata() + if err != nil { + return intoto.Statement{}, fmt.Errorf("failed to read lifecycle metadata: %v", err) + } + + builderDeps := make([]slsav1.ResourceDescriptor, 0) + for i, fn := range depFns { + dep, err := fn() + if err != nil { + return intoto.Statement{}, fmt.Errorf("failed to fetch builder dependency #%v: %v", i, err) + } + + builderDeps = append(builderDeps, dep) + } + + pred := slsav1.ProvenancePredicate{ + BuildDefinition: slsav1.ProvenanceBuildDefinition{ + BuildType: getBuildType(a.Version), + ExternalParameters: build.Spec, + InternalParameters: a.internalParamsFor(build), + ResolvedDependencies: []slsav1.ResourceDescriptor{ + { + Name: "source", + URI: source, + Digest: sourceDigest, + }, + { + Name: "builder-image", + URI: builderRepo, + Digest: slsacommon.DigestSet{ + "sha256": builderSha, + }, + Annotations: convertMap(builderLabels), + }, + }, + }, + RunDetails: slsav1.ProvenanceRunDetails{ + Builder: slsav1.Builder{ + ID: string(builderId), + Version: map[string]string{ + "kpack": a.Version, + "lifecycle": lifecycle.Version, + }, + BuilderDependencies: builderDeps, + }, + BuildMetadata: slsav1.BuildMetadata{ + InvocationID: getInvocationId(build, pod), + StartedOn: start, + FinishedOn: stop, + }, + Byproducts: []slsav1.ResourceDescriptor{}, + }, + } + + return intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + Type: intoto.StatementInTotoV01, + PredicateType: slsav1.PredicateSLSAProvenance, + Subject: []intoto.Subject{ + { + Name: appRepo, + Digest: slsacommon.DigestSet{ + "sha256": appSha, + }, + }, + }, + }, + Predicate: pred, + }, nil +} + +type internalParams struct { + BuilderImage string `json:"builderImage"` + + config.Config + config.Images + config.FeatureFlags +} + +func (a *Attester) internalParamsFor(build *buildv1alpha2.Build) internalParams { + return internalParams{ + BuilderImage: build.Spec.Builder.Image, + + Config: a.Config, + FeatureFlags: a.Features, + Images: a.Images, + } +} + +func getInvocationId(build *buildv1alpha2.Build, pod *corev1.Pod) string { + return fmt.Sprintf("https://kpack.io/%v/%v/%v@%v", build.Namespace, build.Name, pod.Name, pod.Spec.NodeName) +} + +func getBuildType(version string) string { + return fmt.Sprintf("https://github.com/buildpacks-community/kpack/blob/v%v/docs/slsa.md", version) +} + +func getStartStopTime(pod *corev1.Pod) (*time.Time, *time.Time) { + var ( + start *time.Time + stop *time.Time + ) + + for _, c := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) { + if c.Name == buildv1alpha2.PrepareContainerName { + if c.State.Terminated != nil && c.State.Terminated.ExitCode == 0 { + start = &c.State.Terminated.StartedAt.Time + } + } + + if c.Name == buildv1alpha2.CompletionContainerName { + if c.State.Terminated != nil && c.State.Terminated.ExitCode == 0 { + stop = &c.State.Terminated.FinishedAt.Time + } + } + } + + return start, stop +} + +func convertMap(orig map[string]string) map[string]interface{} { + res := make(map[string]interface{}) + for k, v := range orig { + res[k] = v + } + return res +} + +type BuilderDependencyFn func() (slsav1.ResourceDescriptor, error) + +type versionedObject struct { + Name string `json:"name"` + ResourceVersion string `json:"resourceVersion"` +} + +type K8sObject interface { + GetName() string + GetResourceVersion() string +} + +// WithVersionedObject converts a kubernetes object to a SLSA ResourceDescriptor, where the name is +// the Kind, and the content is the json serialzed Name and ResourceVersion of the object. +func WithVersionedObject(kind string, obj K8sObject) BuilderDependencyFn { + return func() (slsav1.ResourceDescriptor, error) { + versioned := versionedObject{ + Name: obj.GetName(), + ResourceVersion: obj.GetResourceVersion(), + } + bytes, err := json.Marshal(versioned) + if err != nil { + return slsav1.ResourceDescriptor{}, fmt.Errorf("failed to marshal json: %v", err) + } + + return slsav1.ResourceDescriptor{ + Name: kind, + Content: bytes, + MediaType: MediaTypeJSON, + }, nil + } +} + +// WithVersionedObjects is the same as WithVersionedObject but handles a slice of objects. These +// objects must have the same GVK +func WithVersionedObjects(kind string, objs []K8sObject) BuilderDependencyFn { + return func() (slsav1.ResourceDescriptor, error) { + versioned := make([]versionedObject, len(objs)) + for i, obj := range objs { + versioned[i] = versionedObject{ + Name: obj.GetName(), + ResourceVersion: obj.GetResourceVersion(), + } + } + bytes, err := json.Marshal(versioned) + if err != nil { + return slsav1.ResourceDescriptor{}, fmt.Errorf("failed to marshal json: %v", err) + } + + return slsav1.ResourceDescriptor{ + Name: kind, + Content: bytes, + MediaType: MediaTypeJSON, + }, nil + } +} diff --git a/pkg/slsa/attest_test.go b/pkg/slsa/attest_test.go new file mode 100644 index 000000000..f4da89c76 --- /dev/null +++ b/pkg/slsa/attest_test.go @@ -0,0 +1,312 @@ +package slsa + +import ( + "encoding/json" + "testing" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + ggcrv1 "github.com/google/go-containerregistry/pkg/v1" + ggcrfake "github.com/google/go-containerregistry/pkg/v1/fake" + slsav1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + buildv1alpha2 "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" + "github.com/pivotal/kpack/pkg/cnb" + "github.com/pivotal/kpack/pkg/config" +) + +func TestAttester(t *testing.T) { + spec.Run(t, "Test SLSA generation", testAttester) +} + +func testAttester(t *testing.T, when spec.G, it spec.S) { + build := &buildv1alpha2.Build{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-build-1", + Namespace: "default", + }, + Spec: buildv1alpha2.BuildSpec{ + Builder: corev1alpha1.BuildBuilderSpec{ + Image: "some-registry.io/builder-image@sha256:de9964b5f501a77b8cf549659f81e29dbac4f8df7f1890ddc2b568dbed428b73", + }, + Cache: &buildv1alpha2.BuildCacheConfig{ + Volume: &buildv1alpha2.BuildPersistentVolumeCache{ + ClaimName: "test-cache", + }, + }, + RunImage: buildv1alpha2.BuildSpecImage{ + Image: "some-registry.io/run-image@sha256:e817bca35911221677b678bf8bf29a18c17ce867b29bd9d0b0c3342c063854e5", + }, + ServiceAccountName: "default", + Source: corev1alpha1.SourceConfig{ + Git: &corev1alpha1.Git{ + Revision: "82cb521d636b282340378d80a6307a08e3d4a4c4", + URL: "https://some-git.com/org/repo.git", + }, + }, + Tags: []string{ + "some-registry.io/some/repo", + "some-registry.io/some/repo:b1.20231108.210915", + }, + }, + } + + buildMetadata := &cnb.BuildMetadata{ + LatestImage: "some-registry.io/some/repo@sha256:27227f3eaf20afcd527f31bcaaa1a10d14f30c2a99b313c86b981906c54c07b9", + } + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-build-1-build-pod", + }, + Spec: corev1.PodSpec{ + NodeName: "some-node", + }, + Status: corev1.PodStatus{ + InitContainerStatuses: []corev1.ContainerStatus{ + { + Name: "prepare", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + StartedAt: metav1.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC), + FinishedAt: metav1.Date(2023, time.January, 1, 1, 0, 0, 0, time.UTC), + }, + }, + }, + }, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "completion", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + StartedAt: metav1.Date(2023, time.January, 1, 1, 0, 0, 0, time.UTC), + FinishedAt: metav1.Date(2023, time.January, 1, 2, 0, 0, 0, time.UTC), + }, + }, + }}, + }, + } + + builderImage := ggcrfake.FakeImage{} + builderImage.ConfigFileReturns(&ggcrv1.ConfigFile{ + Config: ggcrv1.Config{ + Labels: map[string]string{ + "io.buildpacks.buildpack.order": `[{"group":[{"id":"paketo-buildpacks/java-native-image","version":"8.23.0"}]},{"group":[{"id":"paketo-buildpacks/java","version":"10.4.0"}]},{"group":[{"id":"paketo-buildpacks/go","version":"4.6.1"}]},{"group":[{"id":"paketo-buildpacks/procfile","version":"5.6.7"}]}]`, + "io.buildpacks.stack.id": "io.buildpacks.stacks.jammy.tiny", + }, + }, + }, nil) + + appImage := ggcrfake.FakeImage{} + appImage.ConfigFileReturns(&ggcrv1.ConfigFile{ + Config: ggcrv1.Config{ + Labels: map[string]string{ + "io.buildpacks.project.metadata": `{"source":{"type":"git","version":{"commit":"some-commitsh"},"metadata":{"repository":"https://some-git.repo","revision":"some-branch"}}}`, + }, + }, + }, nil) + + r := NewImageReader(&fakeFetcher{ + images: map[string]ggcrv1.Image{ + "some-registry.io/builder-image@sha256:de9964b5f501a77b8cf549659f81e29dbac4f8df7f1890ddc2b568dbed428b73": &builderImage, + "some-registry.io/some/repo@sha256:27227f3eaf20afcd527f31bcaaa1a10d14f30c2a99b313c86b981906c54c07b9": &appImage, + }, + }) + + attester := &Attester{ + Version: "v0.0.0", + + LifecycleProvider: &fakeLifecycleProvider{}, + ImageReader: r, + + Images: config.Images{ + BuildInitImage: "build-init-image", BuildInitWindowsImage: "build-init-windows-image", + BuildWaiterImage: "build-waiter-image", + CompletionImage: "completion-image", CompletionWindowsImage: "completion-windows-image", + RebaseImage: "rebase-image", + }, + Config: config.Config{EnablePriorityClasses: false, MaximumPlatformApiVersion: "", SshTrustUnknownHosts: true}, + Features: config.FeatureFlags{InjectedSidecarSupport: false}, + } + + it("", func() { + stmt, err := attester.AttestBuild(build, buildMetadata, pod, authn.DefaultKeychain, UnsignedBuildID) + require.NoError(t, err) + + expected := `{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v1", + "subject": [ + { + "name": "some-registry.io/some/repo", + "digest": { + "sha256": "27227f3eaf20afcd527f31bcaaa1a10d14f30c2a99b313c86b981906c54c07b9" + } + } + ], + "predicate": { + "buildDefinition": { + "buildType": "https://github.com/buildpacks-community/kpack/blob/vv0.0.0/docs/slsa.md", + "externalParameters": { + "tags": [ + "some-registry.io/some/repo", + "some-registry.io/some/repo:b1.20231108.210915" + ], + "builder": { + "image": "some-registry.io/builder-image@sha256:de9964b5f501a77b8cf549659f81e29dbac4f8df7f1890ddc2b568dbed428b73" + }, + "serviceAccountName": "default", + "source": { + "git": { + "url": "https://some-git.com/org/repo.git", + "revision": "82cb521d636b282340378d80a6307a08e3d4a4c4" + } + }, + "cache": { + "volume": { + "persistentVolumeClaimName": "test-cache" + } + }, + "runImage": { + "image": "some-registry.io/run-image@sha256:e817bca35911221677b678bf8bf29a18c17ce867b29bd9d0b0c3342c063854e5" + }, + "resources": {} + }, + "internalParameters": { + "builderImage": "some-registry.io/builder-image@sha256:de9964b5f501a77b8cf549659f81e29dbac4f8df7f1890ddc2b568dbed428b73", + "systemNamespace": "", + "systemServiceAccount": "", + "enablePriorityClasses": false, + "maximumPlatformApiVersion": "", + "sshTrustUnknownHosts": true, + "buildInitImage": "build-init-image", + "buildInitWindowsImage": "build-init-windows-image", + "buildWaiterImage": "build-waiter-image", + "completionImage": "completion-image", + "completionWindowsImage": "completion-windows-image", + "rebaseImage": "rebase-image", + "injectedSidecarSupport": false, + "generateSlsaAttestation": false + }, + "resolvedDependencies": [ + { + "uri": "https://some-git.repo", + "digest": { + "sha1": "some-commitsh" + }, + "name": "source" + }, + { + "uri": "some-registry.io/builder-image", + "digest": { + "sha256": "de9964b5f501a77b8cf549659f81e29dbac4f8df7f1890ddc2b568dbed428b73" + }, + "name": "builder-image", + "annotations": { + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"paketo-buildpacks/java-native-image\",\"version\":\"8.23.0\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/java\",\"version\":\"10.4.0\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/go\",\"version\":\"4.6.1\"}]},{\"group\":[{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"5.6.7\"}]}]", + "io.buildpacks.stack.id": "io.buildpacks.stacks.jammy.tiny" + } + } + ] + }, + "runDetails": { + "builder": { + "id": "https://kpack.io/slsa/unsigned-build", + "version": { + "kpack": "v0.0.0", + "lifecycle": "1.2.3" + } + }, + "metadata": { + "invocationID": "https://kpack.io/default/test-build-1/test-build-1-build-pod@some-node", + "startedOn": "2023-01-01T00:00:00Z", + "finishedOn": "2023-01-01T02:00:00Z" + } + } + } +}` + + actual, err := json.MarshalIndent(stmt, "", " ") + require.NoError(t, err) + + require.Equal(t, expected, string(actual)) + }) + + when("using the builder dependency fn", func() { + it("records single object", func() { + fn := WithVersionedObject("Namespace", &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-namespace", + ResourceVersion: "5", + }, + }) + + actual, err := fn() + require.NoError(t, err) + + require.Equal(t, + slsav1.ResourceDescriptor{ + Name: "Namespace", + Content: []byte(`{"name":"some-namespace","resourceVersion":"5"}`), + MediaType: "application/json", + }, + actual, + ) + }) + + it("records multiple objects", func() { + fn := WithVersionedObjects("Secret", []K8sObject{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-secret-1", + ResourceVersion: "4", + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-secret-2", + ResourceVersion: "10", + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-secret-3", + ResourceVersion: "1041", + }, + }, + }) + + actual, err := fn() + require.NoError(t, err) + + require.Equal(t, + slsav1.ResourceDescriptor{ + Name: "Secret", + Content: []byte(`[{"name":"some-secret-1","resourceVersion":"4"},{"name":"some-secret-2","resourceVersion":"10"},{"name":"some-secret-3","resourceVersion":"1041"}]`), + MediaType: "application/json", + }, + actual, + ) + }) + }) +} + +type fakeLifecycleProvider struct { +} + +func (l *fakeLifecycleProvider) Metadata() (cnb.LifecycleMetadata, error) { + return cnb.LifecycleMetadata{ + LifecycleInfo: cnb.LifecycleInfo{ + Version: "1.2.3", + }, + }, nil +} diff --git a/pkg/slsa/cosign_signer.go b/pkg/slsa/cosign_signer.go new file mode 100644 index 000000000..aa5ceeb99 --- /dev/null +++ b/pkg/slsa/cosign_signer.go @@ -0,0 +1,41 @@ +package slsa + +import ( + "bytes" + "context" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/sigstore/pkg/signature" +) + +var _ dsse.Signer = (*cosignSigner)(nil) + +type cosignSigner struct { + signer signature.Signer + keyid string +} + +// NewCosignSigner loads a cosign private key into a dsse signer. The main difference between this signer and the one +// provided by sigstore's dsse.WrappedSigner is that this signer doesn't compute the PAE when signing +func NewCosignSigner(key, pass []byte, id string) (*cosignSigner, error) { + sv, err := cosign.LoadPrivateKey(key, pass) + if err != nil { + return nil, err + } + + return &cosignSigner{ + signer: sv, + keyid: id, + }, nil +} + +// KeyID implements dsse.Signer. +func (s *cosignSigner) KeyID() (string, error) { + return s.keyid, nil +} + +// Sign implements dsse.Signer. +func (s *cosignSigner) Sign(ctx context.Context, data []byte) ([]byte, error) { + return s.signer.SignMessage(bytes.NewReader(data)) +} diff --git a/pkg/slsa/image.go b/pkg/slsa/image.go new file mode 100644 index 000000000..2eff537af --- /dev/null +++ b/pkg/slsa/image.go @@ -0,0 +1,107 @@ +package slsa + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + ggcrv1 "github.com/google/go-containerregistry/pkg/v1" + slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" +) + +const ( + ProjectMetadataLabel = "io.buildpacks.project.metadata" +) + +type ImageFetcher interface { + Fetch(keychain authn.Keychain, repoName string) (ggcrv1.Image, string, error) +} + +type reader struct { + fetcher ImageFetcher +} + +func NewImageReader(fetcher ImageFetcher) *reader { + return &reader{ + fetcher: fetcher, + } +} + +func (r *reader) Read(keychain authn.Keychain, repoName string) (string, string, map[string]string, error) { + img, id, err := r.fetcher.Fetch(keychain, repoName) + if err != nil { + return "", "", nil, fmt.Errorf("failed to fetch image: %v", err) + } + + ref, err := name.NewDigest(id) + if err != nil { + return "", "", nil, fmt.Errorf("failed to parse digest: %v", err) + } + + configFile, err := img.ConfigFile() + if err != nil { + return "", "", nil, fmt.Errorf("failed to get image config: %v", err) + } + + sha, found := strings.CutPrefix(ref.DigestStr(), "sha256:") + if !found { + return "", "", nil, fmt.Errorf("unknown digest format '%v'", ref.DigestStr()) + } + + return ref.Context().Name(), sha, configFile.Config.Labels, nil +} + +func extractSourceFromLabel(labels map[string]string) (string, slsacommon.DigestSet, error) { + metadata, found := labels[ProjectMetadataLabel] + if !found { + return "", nil, fmt.Errorf("label not found: '%v'", ProjectMetadataLabel) + } + + var p project + err := json.Unmarshal([]byte(metadata), &p) + if err != nil { + return "", nil, fmt.Errorf("failed to unmarshal json: %v", err) + } + + switch p.Source.Type { + case "git": + // while sha256 support is available, go-git still defaults to sha1 for now + // https://github.com/go-git/go-git/issues/706 + return p.Source.Metadata.Repository, map[string]string{"sha1": p.Source.Version.Commit}, nil + case "blob": + return p.Source.Metadata.Url, map[string]string{"sha256": p.Source.Version.SHA256}, nil + case "image": + sha, found := strings.CutPrefix(p.Source.Version.Digest, "sha256:") + if !found { + return "", nil, fmt.Errorf("unknown digest format '%v'", p.Source.Version.Digest) + } + return p.Source.Metadata.Image, map[string]string{"sha256": sha}, nil + default: + return "", nil, fmt.Errorf("unknown project type: '%v'", p.Source.Type) + } +} + +type project struct { + Source source `json:"source"` +} + +type source struct { + Type string `json:"type"` + Metadata metadata `json:"metadata"` + Version version `json:"version"` +} + +type metadata struct { + Repository string `json:"repository"` + Revision string `json:"revision"` + Image string `json:"image"` + Url string `json:"url"` +} + +type version struct { + Commit string `json:"commit"` + Digest string `json:"digest"` + SHA256 string `json:"sha256sum"` +} diff --git a/pkg/slsa/image_test.go b/pkg/slsa/image_test.go new file mode 100644 index 000000000..523765799 --- /dev/null +++ b/pkg/slsa/image_test.go @@ -0,0 +1,152 @@ +package slsa + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/BurntSushi/toml" + "github.com/google/go-containerregistry/pkg/authn" + ggcrv1 "github.com/google/go-containerregistry/pkg/v1" + ggcrfake "github.com/google/go-containerregistry/pkg/v1/fake" + slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" + + "github.com/pivotal/kpack/pkg/blob" + "github.com/pivotal/kpack/pkg/git" + "github.com/pivotal/kpack/pkg/registry" +) + +func TestImageReader(t *testing.T) { + spec.Run(t, "Test image metadata reading", testImageReader) +} + +func testImageReader(t *testing.T, when spec.G, it spec.S) { + it("returns the correct image repository and sha", func() { + image := ggcrfake.FakeImage{} + image.ConfigFileReturns(&ggcrv1.ConfigFile{ + Config: ggcrv1.Config{ + Labels: map[string]string{"some-label": "some-value"}, + }, + }, nil) + + r := NewImageReader(&fakeFetcher{ + images: map[string]ggcrv1.Image{ + "some-registry.io/some/repo@sha256:8cc53b8113f3a2fba8bd5683a69178d44e38bb172787713cd6a5d21ac3a7ad13": &image, + }, + }) + + repo, sha, _, err := r.Read(authn.DefaultKeychain, "some-registry.io/some/repo@sha256:8cc53b8113f3a2fba8bd5683a69178d44e38bb172787713cd6a5d21ac3a7ad13") + require.NoError(t, err) + + require.Equal(t, "some-registry.io/some/repo", repo) + require.Equal(t, "8cc53b8113f3a2fba8bd5683a69178d44e38bb172787713cd6a5d21ac3a7ad13", sha) + }) + + it("can parse project metadata of type git", func() { + metadata, err := tomlToJsonString(git.Project{ + Source: git.Source{ + Type: "git", + Metadata: git.Metadata{Repository: "https://some-git.repo", Revision: "some-branch"}, + Version: git.Version{Commit: "some-commitsh"}, + }, + }) + require.NoError(t, err) + + labels := map[string]string{ + "some-label": "some-value", + "io.buildpacks.project.metadata": metadata, + } + + repo, sha, err := extractSourceFromLabel(labels) + require.NoError(t, err) + + require.Equal(t, "https://some-git.repo", repo) + require.Equal(t, slsacommon.DigestSet{"sha1": "some-commitsh"}, sha) + }) + + it("can parse project metadata of type blob", func() { + metadata, err := tomlToJsonString(blob.Project{ + Source: blob.Source{ + Type: "blob", + Metadata: blob.Metadata{Url: "https://some-blob.store"}, + Version: blob.Version{SHA256: "some-sha256sum"}, + }, + }) + require.NoError(t, err) + + labels := map[string]string{ + "some-label": "some-value", + "io.buildpacks.project.metadata": metadata, + } + + repo, sha, err := extractSourceFromLabel(labels) + require.NoError(t, err) + + require.Equal(t, "https://some-blob.store", repo) + require.Equal(t, slsacommon.DigestSet{"sha256": "some-sha256sum"}, sha) + }) + + it("can parse project metadata of type image", func() { + metadata, err := tomlToJsonString(registry.Project{ + Source: registry.Source{ + Type: "image", + Metadata: registry.Metadata{Image: "some-registry.io/repo"}, + Version: registry.Version{Digest: "sha256:some-image-digest"}, + }, + }) + require.NoError(t, err) + + labels := map[string]string{ + "some-label": "some-value", + "io.buildpacks.project.metadata": metadata, + } + + repo, sha, err := extractSourceFromLabel(labels) + require.NoError(t, err) + + require.Equal(t, "some-registry.io/repo", repo) + require.Equal(t, slsacommon.DigestSet{"sha256": "some-image-digest"}, sha) + }) +} + +type fakeFetcher struct { + images map[string]ggcrv1.Image +} + +func (f *fakeFetcher) Fetch(keychain authn.Keychain, repoName string) (ggcrv1.Image, string, error) { + return f.images[repoName], repoName, nil +} + +type projectMetadata struct { + Source *projectSource `toml:"source" json:"source,omitempty"` +} + +type projectSource struct { + Type string `toml:"type" json:"type,omitempty"` + Version map[string]interface{} `toml:"version" json:"version,omitempty"` + Metadata map[string]interface{} `toml:"metadata" json:"metadata,omitempty"` +} + +// This emulates the process of the git/blob/image fetch.go writing out toml, cnb parsing in toml +// and writing out json +func tomlToJsonString(val interface{}) (string, error) { + buf := &bytes.Buffer{} + err := toml.NewEncoder(buf).Encode(val) + if err != nil { + return "", err + } + + var metadata projectMetadata + _, err = toml.NewDecoder(buf).Decode(&metadata) + if err != nil { + return "", err + } + + b, err := json.Marshal(metadata) + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/pkg/slsa/pkcs8_signer.go b/pkg/slsa/pkcs8_signer.go new file mode 100644 index 000000000..81c15cad3 --- /dev/null +++ b/pkg/slsa/pkcs8_signer.go @@ -0,0 +1,111 @@ +package slsa + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io" +) + +// NewPKCS8Signer can parse either a RSA, ECDSA, or ED25519 private key in PEM +// format and convert it into a dsse signer. It currently doesn't support +// encrypted keys. +// +// For RSA, this uses RSASSA-PKCS1-V1_5-SIGN with SHA256 as the hash function +// For ECDSA, this uses rand.Reader as the source for k +func NewPKCS8Signer(key []byte, id string) (Signer, error) { + p, _ := pem.Decode([]byte(key)) + if p == nil { + return nil, fmt.Errorf("failed to decode pem block for '%v'", id) + } + + pkey, err := x509.ParsePKCS8PrivateKey(p.Bytes) + if err != nil { + return nil, fmt.Errorf("parsing private key: %v", err) + } + + switch pk := pkey.(type) { + case *rsa.PrivateKey: + return &rsaSigner{ + key: pk, + keyid: id, + }, nil + case *ecdsa.PrivateKey: + return &ecdsaSigner{ + key: pk, + keyid: id, + randFn: rand.Reader, + }, nil + case ed25519.PrivateKey: + return &ed25519Signer{ + key: pk, + keyid: id, + }, nil + default: + return nil, fmt.Errorf("'%v' not a supported key type (rsa, ecdsa, ecdh)", id) + } +} + +var _ Signer = (*rsaSigner)(nil) +var _ Signer = (*ecdsaSigner)(nil) +var _ Signer = (*ed25519Signer)(nil) + +type rsaSigner struct { + key *rsa.PrivateKey + keyid string +} + +func (s *rsaSigner) KeyID() (string, error) { + return s.keyid, nil +} + +func (s *rsaSigner) Sign(ctx context.Context, data []byte) ([]byte, error) { + hf := crypto.SHA256 + hasher := hf.New() + _, err := hasher.Write(data) + if err != nil { + return nil, fmt.Errorf("failed to hash data: %v", err) + } + + return rsa.SignPKCS1v15(nil, s.key, crypto.SHA256, hasher.Sum(nil)) +} + +type ecdsaSigner struct { + key *ecdsa.PrivateKey + keyid string + randFn io.Reader // this should be rand.Reader for anything other than tests +} + +func (s *ecdsaSigner) KeyID() (string, error) { + return s.keyid, nil +} + +func (s *ecdsaSigner) Sign(ctx context.Context, data []byte) ([]byte, error) { + hf := crypto.SHA256 + hasher := hf.New() + _, err := hasher.Write(data) + if err != nil { + return nil, fmt.Errorf("failed to hash data: %v", err) + } + + return ecdsa.SignASN1(s.randFn, s.key, hasher.Sum(nil)) +} + +type ed25519Signer struct { + key ed25519.PrivateKey + keyid string +} + +func (s *ed25519Signer) KeyID() (string, error) { + return s.keyid, nil +} + +func (s *ed25519Signer) Sign(ctx context.Context, data []byte) ([]byte, error) { + return ed25519.Sign(s.key, data), nil +} diff --git a/pkg/slsa/sign.go b/pkg/slsa/sign.go new file mode 100644 index 000000000..fe33d45c1 --- /dev/null +++ b/pkg/slsa/sign.go @@ -0,0 +1,137 @@ +package slsa + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + ggcrv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + intoto "github.com/in-toto/in-toto-golang/in_toto" + slsav1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + cosignremote "github.com/sigstore/cosign/v2/pkg/oci/remote" + cosignstatic "github.com/sigstore/cosign/v2/pkg/oci/static" +) + +type Signer = dsse.Signer + +const ( + DssePayloadType = "application/vnd.dsse.envelope.v1+json" + IntotoPayloadType = "application/vnd.in-toto+json" +) + +func (*Attester) Sign(ctx context.Context, stmt intoto.Statement, signers ...Signer) ([]byte, error) { + payload, err := json.Marshal(stmt) + if err != nil { + return nil, fmt.Errorf("failed to marshal statement: %v", err) + } + pae := dsse.PAE(IntotoPayloadType, payload) + + sigs := make([]dsse.Signature, len(signers)) + for i, signer := range signers { + keyId, err := signer.KeyID() + if err != nil { + return nil, fmt.Errorf("failed to retreive keyid: %v", err) + } + + sig, err := signer.Sign(ctx, pae) + if err != nil { + return nil, fmt.Errorf("failed to sign payload using '%v': %v", keyId, err) + } + + sigs[i] = dsse.Signature{ + KeyID: keyId, + Sig: base64.StdEncoding.EncodeToString(sig), + } + } + + envelope := dsse.Envelope{ + PayloadType: IntotoPayloadType, + Payload: base64.StdEncoding.EncodeToString(payload), + Signatures: sigs, + } + + b, err := json.Marshal(envelope) + if err != nil { + return nil, fmt.Errorf("failed to marshal envelope: %v", err) + } + + return b, nil +} + +func (*Attester) Write(ctx context.Context, digestStr string, payload []byte, keychain authn.Keychain) (ggcrv1.Image, string, error) { + opts := []cosignstatic.Option{ + cosignstatic.WithLayerMediaType(DssePayloadType), + cosignstatic.WithAnnotations(map[string]string{ + "predicateType": slsav1.PredicateSLSAProvenance, + }), + } + + attestation, err := cosignstatic.NewAttestation(payload, opts...) + if err != nil { + return nil, "", fmt.Errorf("failed to create attestation: %v", err) + } + + ref, err := name.ParseReference(digestStr) + if err != nil { + return nil, "", fmt.Errorf("failed to parse reference: %v", err) + } + + attestationTag, err := cosignremote.AttestationTag(ref) + if err != nil { + return nil, "", fmt.Errorf("failed to get attestation tag:%v", err) + } + + annots, err := attestation.Annotations() + if err != nil { + return nil, "", fmt.Errorf("failed to get attestation annotations: %v", err) + } + + // Overwrite any existing attestations with a new one. The only time this is + // relevant is when multiple builds result in bit-for-bit same images (since + // the digest would be the same in both builds). + img := scratchImage() + img, err = mutate.Append(img, mutate.Addendum{ + Layer: attestation, + Annotations: annots, + }) + if err != nil { + return nil, "", err + } + + remoteOpts := []remote.Option{ + remote.WithContext(ctx), + } + if keychain != nil { + remoteOpts = append(remoteOpts, remote.WithAuthFromKeychain(keychain)) + } + + err = remote.Write(attestationTag, img, remoteOpts...) + if err != nil { + return nil, "", fmt.Errorf("failed to write attestation: %v", err) + } + + signatureDigest, err := img.Digest() + if err != nil { + return nil, "", fmt.Errorf("failed to retrieve digest: %v", err) + } + + return img, fmt.Sprintf("%v@%v", attestationTag.Context().Name(), signatureDigest.String()), nil +} + +// TODO: figure out how to determine if we should use the default docker media +// type. since all the secrets/signatures are combined into a single +// attestation image, it'll probably have to be on the Build resource +func scratchImage() ggcrv1.Image { + img := empty.Image + img = mutate.MediaType(img, types.OCIManifestSchema1) + img = mutate.ConfigMediaType(img, types.OCIConfigJSON) + return img +} diff --git a/pkg/slsa/sign_test.go b/pkg/slsa/sign_test.go new file mode 100644 index 000000000..434af016e --- /dev/null +++ b/pkg/slsa/sign_test.go @@ -0,0 +1,358 @@ +package slsa + +import ( + "context" + "crypto/ed25519" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "log" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/remote" + intoto "github.com/in-toto/in-toto-golang/in_toto" + slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + slsav1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/sclevine/spec" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/attest" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/verify" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/stretchr/testify/require" +) + +func TestSigner(t *testing.T) { + spec.Run(t, "Test signer", testSigner) +} + +func testSigner(t *testing.T, when spec.G, it spec.S) { + var ( + statement intoto.Statement + attester = Attester{} + + ctx = context.Background() + timestamp = time.Date(2023, time.January, 1, 1, 0, 0, 0, time.UTC) + ) + + it.Before(func() { + statement = intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + Type: intoto.StatementInTotoV01, + PredicateType: slsav1.PredicateSLSAProvenance, + Subject: []intoto.Subject{ + { + Name: "subject", + Digest: slsacommon.DigestSet{ + "sha256": "some-sha", + }, + }, + }, + }, + Predicate: slsav1.ProvenancePredicate{ + BuildDefinition: slsav1.ProvenanceBuildDefinition{ + BuildType: "build-type", + ExternalParameters: map[string]interface{}{ + "external": "param", + }, + InternalParameters: map[string]interface{}{ + "internal": "param", + }, + ResolvedDependencies: []slsav1.ResourceDescriptor{}, + }, + RunDetails: slsav1.ProvenanceRunDetails{ + Builder: slsav1.Builder{ + ID: "unsigned", + Version: map[string]string{ + "some": "version", + }, + BuilderDependencies: []slsav1.ResourceDescriptor{}, + }, + BuildMetadata: slsav1.BuildMetadata{ + InvocationID: "some-invocation-id", + StartedOn: ×tamp, + FinishedOn: ×tamp, + }, + Byproducts: []slsav1.ResourceDescriptor{}, + }, + }, + } + }) + + when("signing statements", func() { + formatPayload := func(sigs string) string { + return fmt.Sprintf(`{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoic3ViamVjdCIsImRpZ2VzdCI6eyJzaGEyNTYiOiJzb21lLXNoYSJ9fV0sInByZWRpY2F0ZSI6eyJidWlsZERlZmluaXRpb24iOnsiYnVpbGRUeXBlIjoiYnVpbGQtdHlwZSIsImV4dGVybmFsUGFyYW1ldGVycyI6eyJleHRlcm5hbCI6InBhcmFtIn0sImludGVybmFsUGFyYW1ldGVycyI6eyJpbnRlcm5hbCI6InBhcmFtIn19LCJydW5EZXRhaWxzIjp7ImJ1aWxkZXIiOnsiaWQiOiJ1bnNpZ25lZCIsInZlcnNpb24iOnsic29tZSI6InZlcnNpb24ifX0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JRCI6InNvbWUtaW52b2NhdGlvbi1pZCIsInN0YXJ0ZWRPbiI6IjIwMjMtMDEtMDFUMDE6MDA6MDBaIiwiZmluaXNoZWRPbiI6IjIwMjMtMDEtMDFUMDE6MDA6MDBaIn19fX0=","signatures":[%v]}`, sigs) + } + + it("outputs the correct format when no signer is present", func() { + bytes, err := attester.Sign(ctx, statement) + require.NoError(t, err) + + expected := formatPayload("") + require.Equal(t, expected, string(bytes)) + }) + + it("outputs the correct format when rsa signer is used", func() { + p, _ := pem.Decode([]byte(`-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAMVLTljSp8KKogixo53ZA97eNOHajQANWsyJPNDw3W6dStfpWm9c +aiHk6Cd/VMRc1op9tksMTJAEYIHsC6Wk3a0CAwEAAQJAaTYYiMuFxPvzHtnEXBfv +tXkgEFVhHecBRdPlx7K7ExIDUnPZXkt45yBmtLc3fuq9Ap9qJlfT/qvJSxU+YbxH +jQIhAPpHCgIs+/vsk4Gg/Fd2KlNyXOuRo2oLjDSQntwcCNcDAiEAyc4jG/lL9o7Z +RbsRnpme5mkld4WV9czoVOl7WfxfQY8CIEhVUboxQB6eUD9txKCOgUseyWY38E/M +yJfEmHUrEQ77AiEAoDncwl8jIvW0KJsomCYcdZBSQR19PRWd+Z0PZRjtgJ0CIDsR +UeeHdmNHLNWThZtIpyC9Hrq1m8/F97sVa37x7c/O +-----END RSA PRIVATE KEY-----`)) + k, err := x509.ParsePKCS1PrivateKey(p.Bytes) + require.NoError(t, err) + + signer := &rsaSigner{ + key: k, + keyid: "some-rsa-key", + } + + bytes, err := attester.Sign(ctx, statement, signer) + require.NoError(t, err) + + // Note: the golang stdlib RSA PKCS1v15 signing is deterministic, so we get to enjoy + // hardcoding the signature. Other libraries and online checkers aren't neccessarily so. + expected := formatPayload(`{"keyid":"some-rsa-key","sig":"ogSegxffKMUXj5Se3d1f0+qgswxEUhDEGi49LqbXKzZfBnXtKMktw9mT7iKWgXuYe1mIuioPUq7tHzjYfUAUSw=="}`) + require.Equal(t, expected, string(bytes)) + }) + + it("outputs the correct format when ecdsa signer is used", func() { + p, _ := pem.Decode([]byte(`-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIML570JqmxT3O3QJYI6/yhM+EklNoUZBOCQwWzxJx/VeoAoGCCqGSM49 +AwEHoUQDQgAEct985zOnUCYL7rW84V0M3N78XHL4vZok3YBvjpWb6p1gIVim9CET +P4amRng1j+1PnrdDixxQJtmAZT1lJZdXvQ== +-----END EC PRIVATE KEY-----`)) + k, err := x509.ParseECPrivateKey(p.Bytes) + require.NoError(t, err) + + signer := &ecdsaSigner{ + key: k, + keyid: "some-ecdsa-key", + // ecdsa utilizes a random k during the signing process which normally makes it + // nondeterministic. so we force a static prng to make it work for our tests + randFn: &constReader{c: byte(0)}} + + bytes, err := attester.Sign(ctx, statement, signer) + require.NoError(t, err) + + expected := formatPayload(`{"keyid":"some-ecdsa-key","sig":"MEUCIQClEWFrDoq/PelVgvqm2Tp5FEg62fYmi1bIYkTmctOQaAIgfXNOZBQxd+hXGsgKQsP/UyFCXInenAgJUUWuHgHu2LE="}`) + require.Equal(t, expected, string(bytes)) + }) + + it("outputs the correct format when ed25519 signer is used", func() { + p, _ := pem.Decode([]byte(`-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIATRP4Od4Mta/KjTO7c99nfGL/PCUn9Grn7mnXCiIXuW +-----END PRIVATE KEY-----`)) + k, err := x509.ParsePKCS8PrivateKey(p.Bytes) + require.NoError(t, err) + + signer := &ed25519Signer{ + key: k.(ed25519.PrivateKey), + keyid: "some-ed25519-key", + } + + bytes, err := attester.Sign(ctx, statement, signer) + require.NoError(t, err) + + expected := formatPayload(`{"keyid":"some-ed25519-key","sig":"f4Ch73gK9ZBrM1uD+ifTffZ2sQfiQcBRQpUOBa0TCFN5/nIGnce7VXxB8t8fL1aD7OGCIxeovSKsrbt54dNZCA=="}`) + require.Equal(t, expected, string(bytes)) + }) + + when("parsing pkcs#8 keys", func() { + it("parses rsa key", func() { + signer, err := NewPKCS8Signer([]byte(`-----BEGIN PRIVATE KEY----- +MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAxUtOWNKnwoqiCLGj +ndkD3t404dqNAA1azIk80PDdbp1K1+lab1xqIeToJ39UxFzWin22SwxMkARggewL +paTdrQIDAQABAkBpNhiIy4XE+/Me2cRcF++1eSAQVWEd5wFF0+XHsrsTEgNSc9le +S3jnIGa0tzd+6r0Cn2omV9P+q8lLFT5hvEeNAiEA+kcKAiz7++yTgaD8V3YqU3Jc +65GjaguMNJCe3BwI1wMCIQDJziMb+Uv2jtlFuxGemZ7maSV3hZX1zOhU6XtZ/F9B +jwIgSFVRujFAHp5QP23EoI6BSx7JZjfwT8zIl8SYdSsRDvsCIQCgOdzCXyMi9bQo +myiYJhx1kFJBHX09FZ35nQ9lGO2AnQIgOxFR54d2Y0cs1ZOFm0inIL0eurWbz8X3 +uxVrfvHtz84= +-----END PRIVATE KEY-----`), "some-rsa-key") + require.NoError(t, err) + + bytes, err := attester.Sign(ctx, statement, signer) + require.NoError(t, err) + + expected := formatPayload(`{"keyid":"some-rsa-key","sig":"ogSegxffKMUXj5Se3d1f0+qgswxEUhDEGi49LqbXKzZfBnXtKMktw9mT7iKWgXuYe1mIuioPUq7tHzjYfUAUSw=="}`) + require.Equal(t, expected, string(bytes)) + }) + + // ECDSA isn't tested because signing with a random k isn't + // deterministic, and it's individually tested above + + it("parses ed25519 keys", func() { + signer, err := NewPKCS8Signer([]byte(`-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIATRP4Od4Mta/KjTO7c99nfGL/PCUn9Grn7mnXCiIXuW +-----END PRIVATE KEY-----`), "some-ed25519-key") + require.NoError(t, err) + + bytes, err := attester.Sign(ctx, statement, signer) + require.NoError(t, err) + + expected := formatPayload(`{"keyid":"some-ed25519-key","sig":"f4Ch73gK9ZBrM1uD+ifTffZ2sQfiQcBRQpUOBa0TCFN5/nIGnce7VXxB8t8fL1aD7OGCIxeovSKsrbt54dNZCA=="}`) + require.Equal(t, expected, string(bytes)) + }) + + it("errors on bogus keys", func() { + signer, err := NewPKCS8Signer([]byte(`some-bogus-key`), "some-bogus-key") + require.Nil(t, signer) + require.Error(t, err) + }) + }) + }) + + when("compared with cosign cli", func() { + var ( + server *httptest.Server + sinkLogger = log.New(io.Discard, "", 0) + + repo, digest, attTag string + privKeyFile, pubKeyFile string + predicateFile string + ) + + it.Before(func() { + server = httptest.NewServer(registry.New(registry.Logger(sinkLogger))) + + repo = fmt.Sprintf("%v/some-image", strings.TrimPrefix(server.URL, "http://")) + digest = fmt.Sprintf("%v@sha256:2074f12a86b413824be18bf6471e7b6b9c13bce83832fe18efc635591d9cb1d3", repo) + attTag = fmt.Sprintf("%v:sha256-2074f12a86b413824be18bf6471e7b6b9c13bce83832fe18efc635591d9cb1d3.att", repo) + + statement.Subject = []intoto.Subject{{ + Name: repo, + Digest: slsacommon.DigestSet{ + "sha256": "2074f12a86b413824be18bf6471e7b6b9c13bce83832fe18efc635591d9cb1d3", + }, + }} + + privKeyFile, pubKeyFile = generateCosignKey(t) + + f, err := os.CreateTemp("", "") + require.NoError(t, err) + defer f.Close() + + b, err := json.Marshal(statement.Predicate) + require.NoError(t, err) + + _, err = f.Write(b) + require.NoError(t, err) + predicateFile = f.Name() + }) + + it("generates the same image tag", func() { + // attest image via cosign + cmd := attest.AttestCommand{ + KeyOpts: options.KeyOpts{KeyRef: privKeyFile}, + TlogUpload: false, + PredicateType: options.PredicateSLSA1, + PredicatePath: predicateFile, + } + err := cmd.Exec(ctx, digest) + require.NoError(t, err) + + ref1, err := name.ParseReference(attTag) + require.NoError(t, err) + img1, err := remote.Image(ref1) + require.NoError(t, err) + + // delete image so we can reuse the same digest w/o appending signatures + err = remote.Delete(ref1) + require.NoError(t, err) + + // attest image via our implementation + signer := loadCosignSigner(t, privKeyFile) + payload, err := attester.Sign(ctx, statement, signer) + require.NoError(t, err) + img2, _, err := attester.Write(ctx, digest, payload, nil) + require.NoError(t, err) + + // assert attestation images are the same + // note that because cryptographic signings aren't deterministic (a random k is generated each + // time), we can't assert on digest or contents + size1, err := img1.Size() + require.NoError(t, err) + size2, err := img2.Size() + require.NoError(t, err) + + require.Equal(t, size1, size2) + }) + + it("is verifiable by cosign", func() { + // sign image via our implementation + signer := loadCosignSigner(t, privKeyFile) + payload, err := attester.Sign(ctx, statement, signer) + require.NoError(t, err) + _, _, err = attester.Write(ctx, digest, payload, nil) + require.NoError(t, err) + + // attest image via cosign + cmd := verify.VerifyAttestationCommand{ + IgnoreTlog: true, + KeyRef: pubKeyFile, + PredicateType: options.PredicateSLSA1, + } + err = cmd.Exec(ctx, []string{digest}) + require.NoError(t, err, "Result differs from `cosign verify-attestation`") + }) + }) +} + +func generateCosignKey(t *testing.T) (string, string) { + t.Helper() + + keys, err := cosign.GenerateKeyPair(nil) + require.NoError(t, err) + + privKey, err := os.CreateTemp("", "") + require.NoError(t, err) + defer privKey.Close() + + err = privKey.Chmod(0600) + require.NoError(t, err) + _, err = privKey.Write(keys.PrivateBytes) + require.NoError(t, err) + + pubKey, err := os.CreateTemp("", "") + require.NoError(t, err) + defer pubKey.Close() + + err = pubKey.Chmod(0644) + require.NoError(t, err) + _, err = pubKey.Write(keys.PublicBytes) + require.NoError(t, err) + + return privKey.Name(), pubKey.Name() +} + +func loadCosignSigner(t *testing.T, keyFile string) Signer { + t.Helper() + b, err := os.ReadFile(keyFile) + require.NoError(t, err) + + s, err := NewCosignSigner(b, nil, "") + require.NoError(t, err) + return s +} + +type constReader struct { + c byte +} + +func (r *constReader) Read(b []byte) (int, error) { + for i := range b { + b[i] = r.c + } + return len(b), nil +} diff --git a/test/cosign_e2e_test.go b/test/cosign_e2e_test.go index 9ad5a8567..dcee00132 100644 --- a/test/cosign_e2e_test.go +++ b/test/cosign_e2e_test.go @@ -6,7 +6,7 @@ import ( "testing" cosigntesting "github.com/pivotal/kpack/pkg/cosign/testing" - cosignutil "github.com/pivotal/kpack/pkg/cosign/util" + "github.com/pivotal/kpack/pkg/secret" "github.com/sclevine/spec" "github.com/stretchr/testify/assert" @@ -19,17 +19,21 @@ import ( corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" ) +func TestSignBuilder(t *testing.T) { + spec.Run(t, "SignBuilder", testSignBuilder) +} + func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { const ( - testNamespace = "test" + testNamespace = "test-cosign" dockerSecret = "docker-secret" serviceAccountName = "image-service-account" - clusterStoreName = "store" + clusterStoreName = "store-cosign" buildpackName = "buildpack" - clusterBuildpackName = "cluster-buildpack" - clusterStackName = "stack" + clusterBuildpackName = "cluster-buildpack-cosign" + clusterStackName = "stack-cosign" builderName = "custom-signed-builder" - clusterBuilderName = "custom-signed-cluster-builder" + clusterBuilderName = "custom-signed-cluster-builder-cosign" cosignSecretName = "cosign-creds" secretRefFormat = "k8s://%s/%s" ) @@ -542,7 +546,7 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { const expectedErrorMessage = "unable to sign" cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, CosignKeyPassword, nil) - cosignCredSecret.Data[cosignutil.SecretDataCosignPassword] = []byte(invalidPassword) + cosignCredSecret.Data[secret.CosignSecretPassword] = []byte(invalidPassword) _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) require.NoError(t, err) @@ -1020,9 +1024,9 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { const expectedErrorMessage = "unable to sign" cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, cosignKeyPassword, nil) - cosignCredSecret.Data[cosignutil.SecretDataCosignPassword] = []byte(invalidPassword) + cosignCredSecret.Data[secret.CosignSecretPassword] = []byte(invalidPassword) - _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) require.NoError(t, err) serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) diff --git a/test/e2e.go b/test/e2e.go index bf5044aed..07198fbe0 100644 --- a/test/e2e.go +++ b/test/e2e.go @@ -23,11 +23,11 @@ var ( k8sClient *kubernetes.Clientset dynamicClient dynamic.Interface clusterConfig *rest.Config - err error ) func newClients(t *testing.T) (*clients, error) { setup.Do(func() { + var err error kubeconfig := flag.String("kubeconfig", getKubeConfig(), "Path to a kubeconfig. Only required if out-of-cluster.") masterURL := flag.String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") diff --git a/test/execute_build_test.go b/test/execute_build_test.go index e404c75b3..ef8c2ccd4 100644 --- a/test/execute_build_test.go +++ b/test/execute_build_test.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "math/rand" "os" "strings" "testing" @@ -36,10 +35,7 @@ import ( ) func TestKpackE2E(t *testing.T) { - rand.Seed(time.Now().Unix()) - spec.Run(t, "CreateImage", testCreateImage) - spec.Run(t, "SignBuilder", testSignBuilder) } func testCreateImage(t *testing.T, _ spec.G, it spec.S) { @@ -489,7 +485,7 @@ func testCreateImage(t *testing.T, _ spec.G, it spec.S) { basicSecret, basicAuthRepo := cfg.makeGitBasicAuthSecret(gitBasicSecret, testNamespace) if basicSecret != nil { - _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, basicSecret, metav1.CreateOptions{}) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, basicSecret, metav1.CreateOptions{}) require.NoError(t, err) sa.Secrets = append(sa.Secrets, corev1.ObjectReference{ @@ -499,7 +495,7 @@ func testCreateImage(t *testing.T, _ spec.G, it spec.S) { sshSecret, sshAuthRepo := cfg.makeGitSSHAuthSecret(gitSSHSecret, testNamespace) if sshSecret != nil { - _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, sshSecret, metav1.CreateOptions{}) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, sshSecret, metav1.CreateOptions{}) require.NoError(t, err) sa.Secrets = append(sa.Secrets, corev1.ObjectReference{ @@ -674,7 +670,7 @@ func waitUntilFailed(t *testing.T, ctx context.Context, clients *clients, condit require.NoError(t, err) condition := kResource.Status.GetCondition(apis.ConditionType(condition)) - return condition.IsFalse() && "" != condition.Message && strings.Contains(condition.Message, expectedMessage) + return condition.IsFalse() && condition.Message != "" && strings.Contains(condition.Message, expectedMessage) }, 1*time.Second, 8*time.Minute) } } diff --git a/test/slsa_test.go b/test/slsa_test.go new file mode 100644 index 000000000..18353f325 --- /dev/null +++ b/test/slsa_test.go @@ -0,0 +1,808 @@ +package test + +import ( + "context" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + intoto "github.com/in-toto/in-toto-golang/in_toto" + slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + slsav1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/sclevine/spec" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/verify" + cosignremote "github.com/sigstore/cosign/v2/pkg/oci/remote" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" + cosigntesting "github.com/pivotal/kpack/pkg/cosign/testing" + "github.com/pivotal/kpack/pkg/secret" +) + +func TestSlsa(t *testing.T) { + t.Cleanup(func() { + fmt.Println("TestSlsa cleanup") + }) + spec.Run(t, "SLSA", testSlsaBuild) +} + +func testSlsaBuild(t *testing.T, when spec.G, it spec.S) { + const ( + testNamespace = "test-slsa" + controllerNamespace = "kpack" + controllerServiceAccount = "controller" + dockerSecret = "docker-secret" + serviceAccountName = "image-service-account" + clusterStoreName = "store-slsa" + buildpackName = "buildpack" + clusterBuildpackName = "cluster-buildpack-slsa" + clusterStackName = "stack-slsa" + builderName = "custom-builder" + clusterBuilderName = "custom-cluster-builder-slsa" + cosignSecretName = "cosign-creds" + cosignSecretRefFormat = "k8s://%s/%s" + ) + var ( + cfg config + clients *clients + ctx = context.Background() + builtImages map[string]struct{} + ) + + it.Before(func() { + cfg = loadConfig(t) + builtImages = map[string]struct{}{} + + var err error + clients, err = newClients(t) + require.NoError(t, err) + + err = clients.client.KpackV1alpha2().ClusterStores().Delete(ctx, clusterStoreName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().Buildpacks(testNamespace).Delete(ctx, buildpackName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().ClusterBuildpacks().Delete(ctx, clusterBuildpackName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().ClusterStacks().Delete(ctx, clusterStackName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().ClusterBuilders().Delete(ctx, clusterBuilderName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + deleteNamespace(t, ctx, clients, testNamespace) + + _, err = clients.k8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + Labels: readNamespaceLabelsFromEnv(), + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + }) + + it.After(func() { + for tag := range builtImages { + deleteImageTag(t, tag) + } + }) + + it.Before(func() { + secret, err := cfg.makeRegistrySecret(dockerSecret, testNamespace) + require.NoError(t, err) + + _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, secret, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Create(ctx, &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccountName, + }, + Secrets: []corev1.ObjectReference{ + { + Name: dockerSecret, + }, + }, + ImagePullSecrets: []corev1.LocalObjectReference{ + { + Name: dockerSecret, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().ClusterStores().Create(ctx, &buildapi.ClusterStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterStoreName, + }, + Spec: buildapi.ClusterStoreSpec{ + Sources: []corev1alpha1.ImageSource{ + {Image: "gcr.io/paketo-buildpacks/bellsoft-liberica"}, + {Image: "gcr.io/paketo-buildpacks/gradle"}, + {Image: "gcr.io/paketo-buildpacks/syft"}, + {Image: "gcr.io/paketo-buildpacks/executable-jar"}, + {Image: "gcr.io/paketo-buildpacks/dist-zip"}, + {Image: "gcr.io/paketo-buildpacks/spring-boot"}, + {Image: "gcr.io/paketo-buildpacks/go"}, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().Buildpacks(testNamespace).Create(ctx, &buildapi.Buildpack{ + ObjectMeta: metav1.ObjectMeta{ + Name: buildpackName, + }, + Spec: buildapi.BuildpackSpec{ + ImageSource: corev1alpha1.ImageSource{ + Image: "gcr.io/paketo-buildpacks/bellsoft-liberica", + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().ClusterBuildpacks().Create(ctx, &buildapi.ClusterBuildpack{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuildpackName, + }, + Spec: buildapi.ClusterBuildpackSpec{ + ImageSource: corev1alpha1.ImageSource{ + Image: "gcr.io/paketo-buildpacks/nodejs", + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().ClusterStacks().Create(ctx, &buildapi.ClusterStack{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterStackName, + }, + Spec: buildapi.ClusterStackSpec{ + Id: "io.buildpacks.stacks.jammy", + BuildImage: buildapi.ClusterStackSpecImage{ + Image: "gcr.io/paketo-buildpacks/build-jammy-base", + }, + RunImage: buildapi.ClusterStackSpecImage{ + Image: "gcr.io/paketo-buildpacks/run-jammy-base", + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + builder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ + ObjectMeta: metav1.ObjectMeta{ + Name: builderName, + Namespace: testNamespace, + }, + Spec: buildapi.NamespacedBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/go", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/nodejs", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: buildpackName, + Kind: "Buildpack", + }, + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountName: serviceAccountName, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + clusterBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Create(ctx, &buildapi.ClusterBuilder{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuilderName, + }, + Spec: buildapi.ClusterBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/go", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: clusterBuildpackName, + Kind: "ClusterBuildpack", + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountRef: corev1.ObjectReference{ + Namespace: testNamespace, + Name: serviceAccountName, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilCondition(t, ctx, clients, corev1alpha1.ConditionReady, builder, clusterBuilder) + waitUntilCondition(t, ctx, clients, buildapi.ConditionUpToDate, builder, clusterBuilder) + }) + + when("no signing keys are present", func() { + it("records the build details", func() { + imageTag := cfg.newImageTag() + image, err := clients.client.KpackV1alpha2().Images(testNamespace).Create(ctx, &buildapi.Image{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-image", + }, + Spec: buildapi.ImageSpec{ + Tag: imageTag, + Builder: corev1.ObjectReference{ + Kind: buildapi.BuilderKind, + Name: builderName, + }, + ServiceAccountName: serviceAccountName, + Source: corev1alpha1.SourceConfig{ + Git: &corev1alpha1.Git{ + URL: "https://github.com/cloudfoundry-samples/cf-sample-app-nodejs", + Revision: "master", + }, + }, + ImageTaggingStrategy: corev1alpha1.None, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + builtImages[validateImageCreate(t, clients, image, image.Resources())] = struct{}{} + + image, err = clients.client.KpackV1alpha2().Images(testNamespace).Get(ctx, image.Name, metav1.GetOptions{}) + require.NoError(t, err) + + verifySLSAProvenance(t, image.Status.LatestImage, image, false) + }) + + it("can read the source from git, blob, and registry images", func() { + type row struct { + name string + source corev1alpha1.SourceConfig + verifyFn func(sourceConfig map[string]interface{}, resolvedSource slsav1.ResourceDescriptor) + } + + testImage := func(r row) { + t.Run(r.name, func(t *testing.T) { + t.Parallel() + + imageTag := cfg.newImageTag() + image, err := clients.client.KpackV1alpha2().Images(testNamespace).Create(ctx, &buildapi.Image{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.name, + }, + Spec: buildapi.ImageSpec{ + Tag: imageTag, + Builder: corev1.ObjectReference{ + Kind: buildapi.BuilderKind, + Name: builderName, + }, + ServiceAccountName: serviceAccountName, + Source: r.source, + ImageTaggingStrategy: corev1alpha1.None, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + builtImages[validateImageCreate(t, clients, image, image.Resources())] = struct{}{} + + image, err = clients.client.KpackV1alpha2().Images(testNamespace).Get(ctx, image.Name, metav1.GetOptions{}) + require.NoError(t, err) + + stmt := verifySLSAProvenance(t, image.Status.LatestImage, image, false) + + params, ok := stmt.Predicate.BuildDefinition.ExternalParameters.(map[string]interface{}) + require.True(t, ok) + + source, ok := params["source"].(map[string]interface{}) + require.True(t, ok) + + config, ok := source[r.name].(map[string]interface{}) + require.True(t, ok) + + require.Greater(t, len(stmt.Predicate.BuildDefinition.ResolvedDependencies), 1) + r.verifyFn(config, stmt.Predicate.BuildDefinition.ResolvedDependencies[0]) + }) + } + + table := []row{ + { + name: "git", + source: corev1alpha1.SourceConfig{ + Git: &corev1alpha1.Git{ + URL: "https://github.com/cloudfoundry-samples/cf-sample-app-nodejs", + Revision: "master", + }, + }, + verifyFn: func(config map[string]interface{}, resolved slsav1.ResourceDescriptor) { + require.Equal(t, "https://github.com/cloudfoundry-samples/cf-sample-app-nodejs", config["url"]) + require.NotEmpty(t, config["revision"]) + + require.Equal(t, "https://github.com/cloudfoundry-samples/cf-sample-app-nodejs", resolved.URI) + require.Equal(t, resolved.Digest["sha1"], config["revision"]) + }, + }, + { + name: "blob", + source: corev1alpha1.SourceConfig{ + Blob: &corev1alpha1.Blob{ + URL: "https://storage.googleapis.com/build-service/sample-apps/spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar", + }, + }, + verifyFn: func(config map[string]interface{}, resolved slsav1.ResourceDescriptor) { + require.Equal(t, "https://storage.googleapis.com/build-service/sample-apps/spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar", config["url"]) + + require.Equal(t, "https://storage.googleapis.com/build-service/sample-apps/spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar", resolved.URI) + require.Equal(t, "0ea773b255487f9ed45bbf6dea66d45f6c593b0c1c02b2c71c5bf20542e86d3c", resolved.Digest["sha256"]) + }, + }, + { + name: "registry", + source: corev1alpha1.SourceConfig{ + Registry: &corev1alpha1.Registry{ + Image: "gcr.io/cf-build-service-public/fixtures/nodejs-source@sha256:76cb2e087b6f1355caa8ed4a5eebb1ad7376e26995a8d49a570cdc10e4976e44", + }, + }, + verifyFn: func(config map[string]interface{}, resolved slsav1.ResourceDescriptor) { + require.Equal(t, "gcr.io/cf-build-service-public/fixtures/nodejs-source@sha256:76cb2e087b6f1355caa8ed4a5eebb1ad7376e26995a8d49a570cdc10e4976e44", config["image"]) + + require.Equal(t, "gcr.io/cf-build-service-public/fixtures/nodejs-source@sha256:76cb2e087b6f1355caa8ed4a5eebb1ad7376e26995a8d49a570cdc10e4976e44", resolved.URI) + require.Equal(t, "76cb2e087b6f1355caa8ed4a5eebb1ad7376e26995a8d49a570cdc10e4976e44", resolved.Digest["sha256"]) + }, + }, + } + + for _, r := range table { + testImage(r) + } + }) + }) + + // TODO(chenbh): add tests for verifying rsa/ecdsa/ed25519 keys + when("there are signing keys", func() { + verifyViaCosignCLI := func(digest, secretRef string) { + cmd := verify.VerifyAttestationCommand{ + IgnoreTlog: true, + KeyRef: secretRef, + PredicateType: options.PredicateSLSA1, + } + err := cmd.Exec(ctx, []string{digest}) + require.NoError(t, err, "Result differs from `cosign verify-attestation`") + } + + it("signs using builder service account keys", func() { + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, "", map[string]string{secret.SLSASecretAnnotation: ""}) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + imageTag := cfg.newImageTag() + image, err := clients.client.KpackV1alpha2().Images(testNamespace).Create(ctx, &buildapi.Image{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cosign-signing", + }, + Spec: buildapi.ImageSpec{ + Tag: imageTag, + Builder: corev1.ObjectReference{ + Kind: buildapi.BuilderKind, + Name: builderName, + }, + ServiceAccountName: serviceAccountName, + Source: corev1alpha1.SourceConfig{ + Git: &corev1alpha1.Git{ + URL: "https://github.com/cloudfoundry-samples/cf-sample-app-nodejs", + Revision: "master", + }, + }, + ImageTaggingStrategy: corev1alpha1.None, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + builtImages[validateImageCreate(t, clients, image, image.Resources())] = struct{}{} + + image, err = clients.client.KpackV1alpha2().Images(testNamespace).Get(ctx, image.Name, metav1.GetOptions{}) + require.NoError(t, err) + + verifySLSAProvenance(t, image.Status.LatestImage, image, true) + verifyViaCosignCLI(image.Status.LatestImage, fmt.Sprintf(cosignSecretRefFormat, testNamespace, cosignSecretName)) + }) + + it("signs using controller service account keys", func() { + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, controllerNamespace, "", map[string]string{secret.SLSASecretAnnotation: ""}) + _, err := clients.k8sClient.CoreV1().Secrets(controllerNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + defer func() { + err = clients.k8sClient.CoreV1().Secrets(controllerNamespace).Delete(ctx, cosignCredSecret.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }() + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(controllerNamespace).Get(ctx, controllerServiceAccount, metav1.GetOptions{}) + require.NoError(t, err) + + oldSecrets := serviceAccount.Secrets + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, corev1.ObjectReference{Name: cosignCredSecret.Name}) + + serviceAccount, err = clients.k8sClient.CoreV1().ServiceAccounts(controllerNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + defer func() { + serviceAccount.Secrets = oldSecrets + _, err = clients.k8sClient.CoreV1().ServiceAccounts(controllerNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + }() + + imageTag := cfg.newImageTag() + image, err := clients.client.KpackV1alpha2().Images(testNamespace).Create(ctx, &buildapi.Image{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cosign-cluster-signing", + }, + Spec: buildapi.ImageSpec{ + Tag: imageTag, + Builder: corev1.ObjectReference{ + Kind: buildapi.BuilderKind, + Name: builderName, + }, + ServiceAccountName: serviceAccountName, + Source: corev1alpha1.SourceConfig{ + Git: &corev1alpha1.Git{ + URL: "https://github.com/cloudfoundry-samples/cf-sample-app-nodejs", + Revision: "master", + }, + }, + ImageTaggingStrategy: corev1alpha1.None, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + builtImages[validateImageCreate(t, clients, image, image.Resources())] = struct{}{} + + image, err = clients.client.KpackV1alpha2().Images(testNamespace).Get(ctx, image.Name, metav1.GetOptions{}) + require.NoError(t, err) + + verifySLSAProvenance(t, image.Status.LatestImage, image, true) + verifyViaCosignCLI(image.Status.LatestImage, fmt.Sprintf(cosignSecretRefFormat, controllerNamespace, cosignSecretName)) + }) + }) +} + +type statement struct { + intoto.StatementHeader + Predicate slsav1.ProvenancePredicate `json:"predicate"` +} + +func parseSLSAProvenance(t *testing.T, img v1.Image) statement { + layers, err := img.Layers() + require.NoError(t, err) + require.Len(t, layers, 1, "attestation images must have exactly 1 layer") + + mt, err := layers[0].MediaType() + require.NoError(t, err) + require.Equal(t, types.MediaType("application/vnd.dsse.envelope.v1+json"), mt) + + reader, err := layers[0].Uncompressed() + require.NoError(t, err) + + var envelope dsse.Envelope + require.NoError(t, json.NewDecoder(reader).Decode(&envelope)) + + require.Equal(t, "application/vnd.in-toto+json", envelope.PayloadType) + + payloadBytes, err := base64.StdEncoding.DecodeString(envelope.Payload) + require.NoError(t, err) + + var stmt statement + require.NoError(t, json.Unmarshal(payloadBytes, &stmt)) + return stmt +} + +func verifySLSAProvenance(t *testing.T, digest string, image *buildapi.Image, signed bool) statement { + ref, err := name.ParseReference(digest) + require.NoError(t, err) + + auth, err := authn.DefaultKeychain.Resolve(ref.Context().Registry) + require.NoError(t, err) + + appImg, err := remote.Image(ref, remote.WithAuth(auth)) + require.NoError(t, err) + + appDigest, err := appImg.Digest() + require.NoError(t, err) + + attTag, err := cosignremote.AttestationTag(ref) + require.NoError(t, err) + + attImg, err := remote.Image(attTag, remote.WithAuth(auth)) + require.NoError(t, err) + + stmt := parseSLSAProvenance(t, attImg) + + // asserts instead of requires are used so that in case we change the + // attestation format, we consolidate all the failures in a single run + // rather than having to rerun the test for every little typo + assert.Equal(t, "https://slsa.dev/provenance/v1", stmt.PredicateType) + + require.Len(t, stmt.Subject, 1) + assert.Equal(t, stmt.Subject[0], intoto.Subject{ + Name: ref.Context().Name(), + Digest: slsacommon.DigestSet{ + appDigest.Algorithm: appDigest.Hex, + }, + }) + + pred := stmt.Predicate + assert.Regexp(t, "^https://github.com/buildpacks-community/kpack/blob/v.*/docs/slsa.md$", pred.BuildDefinition.BuildType) + // external params + params, ok := pred.BuildDefinition.ExternalParameters.(map[string]interface{}) + require.True(t, ok) + assert.Contains(t, params["source"], "git") + assert.NotNil(t, params["tags"]) + assert.NotNil(t, params["runImage"]) + + // internal params + assert.Contains(t, pred.BuildDefinition.InternalParameters, "builderImage") + assert.Contains(t, pred.BuildDefinition.InternalParameters, "completionImage") + + // build depedencies + deps := pred.BuildDefinition.ResolvedDependencies + require.Len(t, deps, 2) + + assert.Equal(t, deps[0].Name, "source") + assert.NotEmpty(t, deps[0].URI) + assert.Contains(t, deps[0].Digest, "sha1") + + assert.Equal(t, deps[1].Name, "builder-image") + assert.NotEmpty(t, deps[1].URI) + assert.Contains(t, deps[1].Digest, "sha256") + assert.Greater(t, len(deps[1].Annotations), 0) + + // builder run details + if signed { + assert.Equal(t, "https://kpack.io/slsa/signed-build", pred.RunDetails.Builder.ID) + } else { + assert.Equal(t, "https://kpack.io/slsa/unsigned-build", pred.RunDetails.Builder.ID) + } + assert.Contains(t, pred.RunDetails.Builder.Version, "kpack") + assert.Contains(t, pred.RunDetails.Builder.Version, "lifecycle") + assert.Greater(t, len(pred.RunDetails.Builder.BuilderDependencies), 0) + + // builder metadata + metadata := pred.RunDetails.BuildMetadata + expectedId := fmt.Sprintf("^https://kpack.io/%v/%v/.*@.*$", image.Namespace, image.Status.LatestBuildRef) + assert.Regexp(t, expectedId, metadata.InvocationID) + assert.NotNil(t, metadata.StartedOn) + assert.NotNil(t, metadata.FinishedOn) + + return stmt +} + +func makePrivateKey(t *testing.T, alg, secretName, namespace string) *corev1.Secret { + t.Helper() + + var keyBytes []byte + var ( + key any + err error + ) + switch alg { + case "rsa": + key, err = rsa.GenerateKey(rand.Reader, 1024) + require.NoError(t, err) + case "ecdsa": + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + case "ed25519": + _, key, err = ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + default: + t.Fatal("invalid key type") + } + keyBytes, err = x509.MarshalPKCS8PrivateKey(key) + require.NoError(t, err) + + pem := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + }) + require.NotNil(t, pem) + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Annotations: map[string]string{ + secret.SLSASecretAnnotation: "", + }, + }, + Data: map[string][]byte{ + secret.PKCS8SecretKey: pem, + }, + Type: corev1.SecretTypeSSHAuth, + } +}