Add verification support for notation signed artifacts
Introduces a new verification provider `notation` to verify notation signed artifacts. Currently only cosign is supported and that is a problem if the end user utilises notation. --------- Signed-off-by: Jason <jagoodse@microsoft.com> Signed-off-by: JasonTheDeveloper <jagoodse@microsoft.com> Signed-off-by: Jagpreet Singh Tamber <jagpreetstamber@gmail.com> Co-authored-by: souleb <bah.soule@gmail.com> Co-authored-by: Jagpreet Singh Tamber <jagpreetstamber@gmail.com> Co-authored-by: Sunny <github@darkowlzz.space>
This commit is contained in:
parent
565f6ee039
commit
553945ab8e
|
@ -58,7 +58,7 @@ make run
|
|||
|
||||
### Building the container image
|
||||
|
||||
Set the name of the container image to be created from the source code. This will be used
|
||||
Set the name of the container image to be created from the source code. This will be used
|
||||
when building, pushing and referring to the image on YAML files:
|
||||
|
||||
```sh
|
||||
|
@ -79,7 +79,7 @@ make docker-push
|
|||
```
|
||||
|
||||
Alternatively, the three steps above can be done in a single line:
|
||||
|
||||
|
||||
```sh
|
||||
IMG=registry-path/source-controller TAG=latest BUILD_ARGS=--push \
|
||||
make docker-build
|
||||
|
@ -128,7 +128,8 @@ Create a `.vscode/launch.json` file:
|
|||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/main.go"
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"args": ["--storage-adv-addr=:0", "--storage-path=${workspaceFolder}/bin/data"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ and is a core component of the [GitOps toolkit](https://fluxcd.io/flux/component
|
|||
## Features
|
||||
|
||||
* authenticates to sources (SSH, user/password, API token, Workload Identity)
|
||||
* validates source authenticity (PGP, Cosign)
|
||||
* validates source authenticity (PGP, Cosign, Notation)
|
||||
* detects source changes based on update policies (semver)
|
||||
* fetches resources on-demand and on-a-schedule
|
||||
* packages the fetched resources into a well-known format (tar.gz, yaml)
|
||||
|
|
|
@ -182,7 +182,7 @@ type OCILayerSelector struct {
|
|||
// OCIRepositoryVerification verifies the authenticity of an OCI Artifact
|
||||
type OCIRepositoryVerification struct {
|
||||
// Provider specifies the technology used to sign the OCI Artifact.
|
||||
// +kubebuilder:validation:Enum=cosign
|
||||
// +kubebuilder:validation:Enum=cosign;notation
|
||||
// +kubebuilder:default:=cosign
|
||||
Provider string `json:"provider"`
|
||||
|
||||
|
|
|
@ -468,6 +468,7 @@ spec:
|
|||
OCI Artifact.
|
||||
enum:
|
||||
- cosign
|
||||
- notation
|
||||
type: string
|
||||
secretRef:
|
||||
description: |-
|
||||
|
|
|
@ -224,6 +224,7 @@ spec:
|
|||
OCI Artifact.
|
||||
enum:
|
||||
- cosign
|
||||
- notation
|
||||
type: string
|
||||
secretRef:
|
||||
description: |-
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: podinfo-notation
|
||||
spec:
|
||||
url: oci://ghcr.io/stefanprodan/charts
|
||||
type: "oci"
|
||||
interval: 1m
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: HelmChart
|
||||
metadata:
|
||||
name: podinfo-notation
|
||||
spec:
|
||||
chart: podinfo
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: podinfo-notation
|
||||
version: '6.6.0'
|
||||
interval: 1m
|
||||
verify:
|
||||
provider: notation
|
||||
secretRef:
|
||||
name: notation-config
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: podinfo-deploy-signed-with-notation
|
||||
spec:
|
||||
interval: 5m
|
||||
url: oci://ghcr.io/stefanprodan/podinfo-deploy
|
||||
ref:
|
||||
semver: "6.6.x"
|
||||
verify:
|
||||
provider: notation
|
||||
secretRef:
|
||||
name: notation-config
|
|
@ -252,15 +252,20 @@ For practical information, see
|
|||
|
||||
**Note:** This feature is available only for Helm charts fetched from an OCI Registry.
|
||||
|
||||
`.spec.verify` is an optional field to enable the verification of [Cosign](https://github.com/sigstore/cosign)
|
||||
`.spec.verify` is an optional field to enable the verification of [Cosign](https://github.com/sigstore/cosign) or [Notation](https://github.com/notaryproject/notation)
|
||||
signatures. The field offers three subfields:
|
||||
|
||||
- `.provider`, to specify the verification provider. Only supports `cosign` at present.
|
||||
- `.provider`, to specify the verification provider. The supported options are `cosign` and `notation` at present.
|
||||
- `.secretRef.name`, to specify a reference to a Secret in the same namespace as
|
||||
the HelmChart, containing the Cosign public keys of trusted authors.
|
||||
- `.matchOIDCIdentity`, to specify a list of OIDC identity matchers. Please see
|
||||
the HelmChart, containing the public keys of trusted authors. For Notation this Secret should also include the [trust policy](https://github.com/notaryproject/specifications/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy) in
|
||||
addition to the CA certificate.
|
||||
- `.matchOIDCIdentity`, to specify a list of OIDC identity matchers (only supported when using `cosign` as the verification provider). Please see
|
||||
[Keyless verification](#keyless-verification) for more details.
|
||||
|
||||
#### Cosign
|
||||
|
||||
The `cosign` provider can be used to verify the signature of an OCI artifact using either a known public key or via the [Cosign Keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) procedure.
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
|
@ -281,7 +286,7 @@ following attributes to the HelmChart's `.status.conditions`:
|
|||
- `status: "True"`
|
||||
- `reason: Succeeded`
|
||||
|
||||
#### Public keys verification
|
||||
##### Public keys verification
|
||||
|
||||
To verify the authenticity of HelmChart hosted in an OCI Registry, create a Kubernetes
|
||||
secret with the Cosign public keys:
|
||||
|
@ -303,7 +308,7 @@ Note that the keys must have the `.pub` extension for Flux to make use of them.
|
|||
Flux will loop over the public keys and use them to verify a HelmChart's signature.
|
||||
This allows for older HelmCharts to be valid as long as the right key is in the secret.
|
||||
|
||||
#### Keyless verification
|
||||
##### Keyless verification
|
||||
|
||||
For publicly available HelmCharts, which are signed using the
|
||||
[Cosign Keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) procedure,
|
||||
|
@ -362,6 +367,55 @@ instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/).
|
|||
Note that keyless verification is an **experimental feature**, using
|
||||
custom root CAs or self-hosted Rekor instances are not currently supported.
|
||||
|
||||
#### Notation
|
||||
|
||||
The `notation` provider can be used to verify the signature of an OCI artifact using known
|
||||
trust policy and CA certificate.
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: HelmChart
|
||||
metadata:
|
||||
name: podinfo
|
||||
spec:
|
||||
verify:
|
||||
provider: notation
|
||||
secretRef:
|
||||
name: notation-config
|
||||
```
|
||||
|
||||
When the verification succeeds, the controller adds a Condition with the
|
||||
following attributes to the HelmChart's `.status.conditions`:
|
||||
|
||||
- `type: SourceVerified`
|
||||
- `status: "True"`
|
||||
- `reason: Succeeded`
|
||||
|
||||
To verify the authenticity of an OCI artifact, create a Kubernetes secret
|
||||
containing Certificate Authority (CA) root certificates and the a `trust policy`
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: notation-config
|
||||
type: Opaque
|
||||
data:
|
||||
certificate1.pem: <BASE64>
|
||||
certificate2.crt: <BASE64>
|
||||
trustpolicy.json: <BASE64>
|
||||
```
|
||||
|
||||
Note that the CA certificates must have either `.pem` or `.crt` extension and your trust policy must
|
||||
be named `trustpolicy.json` for Flux to make use of them.
|
||||
|
||||
For more information on the signing and verification process see [Signing and Verification Workflow](https://github.com/notaryproject/specifications/blob/v1.0.0/specs/signing-and-verification-workflow.md).
|
||||
|
||||
Flux will loop over the certificates and use them to verify an artifact's signature.
|
||||
This allows for older artifacts to be valid as long as the right certificate is in the secret.
|
||||
|
||||
## Working with HelmCharts
|
||||
|
||||
### Triggering a reconcile
|
||||
|
|
|
@ -237,7 +237,7 @@ patches:
|
|||
target:
|
||||
kind: Deployment
|
||||
name: source-controller
|
||||
```
|
||||
```
|
||||
|
||||
When using pod-managed identity on an AKS cluster, AAD Pod Identity
|
||||
has to be used to give the `source-controller` pod access to the ACR.
|
||||
|
@ -279,7 +279,7 @@ patches:
|
|||
target:
|
||||
kind: ServiceAccount
|
||||
name: source-controller
|
||||
```
|
||||
```
|
||||
|
||||
The Artifact Registry service uses the permission `artifactregistry.repositories.downloadArtifacts`
|
||||
that is located under the Artifact Registry Reader role. If you are using
|
||||
|
@ -454,7 +454,7 @@ metadata:
|
|||
spec:
|
||||
ref:
|
||||
digest: "sha256:<SHA-value>"
|
||||
```
|
||||
```
|
||||
|
||||
This field takes precedence over all other fields.
|
||||
|
||||
|
@ -501,14 +501,23 @@ for more information.
|
|||
### Verification
|
||||
|
||||
`.spec.verify` is an optional field to enable the verification of [Cosign](https://github.com/sigstore/cosign)
|
||||
or [Notation](https://github.com/notaryproject/notation)
|
||||
signatures. The field offers three subfields:
|
||||
|
||||
- `.provider`, to specify the verification provider. Only supports `cosign` at present.
|
||||
- `.provider`, to specify the verification provider. The supported options are `cosign` and `notation` at present.
|
||||
- `.secretRef.name`, to specify a reference to a Secret in the same namespace as
|
||||
the OCIRepository, containing the Cosign public keys of trusted authors.
|
||||
- `.matchOIDCIdentity`, to specify a list of OIDC identity matchers. Please see
|
||||
the OCIRepository, containing the Cosign public keys of trusted authors. For Notation this Secret should also
|
||||
include the [trust policy](https://github.com/notaryproject/specifications/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy) in
|
||||
addition to the CA certificate.
|
||||
- `.matchOIDCIdentity`, to specify a list of OIDC identity matchers (only supported when using `cosign` as the
|
||||
verification provider). Please see
|
||||
[Keyless verification](#keyless-verification) for more details.
|
||||
|
||||
#### Cosign
|
||||
|
||||
The `cosign` provider can be used to verify the signature of an OCI artifact using either a known public key
|
||||
or via the [Cosign Keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) procedure.
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
|
@ -529,7 +538,7 @@ following attributes to the OCIRepository's `.status.conditions`:
|
|||
- `status: "True"`
|
||||
- `reason: Succeeded`
|
||||
|
||||
#### Public keys verification
|
||||
##### Public keys verification
|
||||
|
||||
To verify the authenticity of an OCI artifact, create a Kubernetes secret
|
||||
with the Cosign public keys:
|
||||
|
@ -551,7 +560,7 @@ Note that the keys must have the `.pub` extension for Flux to make use of them.
|
|||
Flux will loop over the public keys and use them to verify an artifact's signature.
|
||||
This allows for older artifacts to be valid as long as the right key is in the secret.
|
||||
|
||||
#### Keyless verification
|
||||
##### Keyless verification
|
||||
|
||||
For publicly available OCI artifacts, which are signed using the
|
||||
[Cosign Keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) procedure,
|
||||
|
@ -593,6 +602,55 @@ instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/).
|
|||
Note that keyless verification is an **experimental feature**, using
|
||||
custom root CAs or self-hosted Rekor instances are not currently supported.
|
||||
|
||||
#### Notation
|
||||
|
||||
The `notation` provider can be used to verify the signature of an OCI artifact using known
|
||||
trust policy and CA certificate.
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: <repository-name>
|
||||
spec:
|
||||
verify:
|
||||
provider: notation
|
||||
secretRef:
|
||||
name: notation-config
|
||||
```
|
||||
|
||||
When the verification succeeds, the controller adds a Condition with the
|
||||
following attributes to the OCIRepository's `.status.conditions`:
|
||||
|
||||
- `type: SourceVerified`
|
||||
- `status: "True"`
|
||||
- `reason: Succeeded`
|
||||
|
||||
To verify the authenticity of an OCI artifact, create a Kubernetes secret
|
||||
containing Certificate Authority (CA) root certificates and the a `trust policy`
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: notation-config
|
||||
type: Opaque
|
||||
data:
|
||||
certificate1.pem: <BASE64>
|
||||
certificate2.crt: <BASE64>
|
||||
trustpolicy.json: <BASE64>
|
||||
```
|
||||
|
||||
Note that the CA certificates must have either `.pem` or `.crt` extension and your trust policy must
|
||||
be named `trustpolicy.json` for Flux to make use of them.
|
||||
|
||||
For more information on the signing and verification process see [Signing and Verification Workflow](https://github.com/notaryproject/specifications/blob/v1.0.0/specs/signing-and-verification-workflow.md).
|
||||
|
||||
Flux will loop over the certificates and use them to verify an artifact's signature.
|
||||
This allows for older artifacts to be valid as long as the right certificate is in the secret.
|
||||
|
||||
### Suspend
|
||||
|
||||
`.spec.suspend` is an optional field to suspend the reconciliation of a
|
||||
|
|
11
go.mod
11
go.mod
|
@ -44,9 +44,12 @@ require (
|
|||
github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20231202142526-55ffb0092afd
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/minio/minio-go/v7 v7.0.66
|
||||
github.com/notaryproject/notation-core-go v1.0.2
|
||||
github.com/notaryproject/notation-go v1.1.0
|
||||
github.com/onsi/gomega v1.31.1
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/ory/dockertest/v3 v3.10.0
|
||||
github.com/otiai10/copy v1.14.0
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
|
||||
|
@ -64,6 +67,7 @@ require (
|
|||
k8s.io/apimachinery v0.28.6
|
||||
k8s.io/client-go v0.28.6
|
||||
k8s.io/utils v0.0.0-20231127182322-b307cd553661
|
||||
oras.land/oras-go/v2 v2.3.1
|
||||
sigs.k8s.io/controller-runtime v0.16.3
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
@ -87,6 +91,7 @@ require (
|
|||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
|
@ -169,11 +174,14 @@ require (
|
|||
github.com/fluxcd/gitkit v0.6.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
||||
github.com/go-ldap/ldap/v3 v3.4.6 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-logr/zapr v1.3.0 // indirect
|
||||
github.com/go-openapi/analysis v0.22.0 // indirect
|
||||
|
@ -265,7 +273,6 @@ require (
|
|||
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/oleiade/reflections v1.0.1 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/opencontainers/runc v1.1.5 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pborman/uuid v1.2.1 // indirect
|
||||
|
@ -312,6 +319,8 @@ require (
|
|||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/transparency-dev/merkle v0.0.2 // indirect
|
||||
github.com/vbatts/tar-split v0.11.5 // indirect
|
||||
github.com/veraison/go-cose v1.2.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/go-gitlab v0.96.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
|
|
29
go.sum
29
go.sum
|
@ -62,6 +62,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
|
|||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
|
@ -95,6 +97,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
|||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
||||
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.2/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
|
@ -368,8 +372,12 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
|
|||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
|
||||
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/gliderlabs/ssh v0.3.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8=
|
||||
github.com/gliderlabs/ssh v0.3.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
|
||||
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
|
@ -387,6 +395,8 @@ github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpj
|
|||
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A=
|
||||
github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
|
@ -526,6 +536,7 @@ github.com/google/trillian v1.5.3/go.mod h1:p4tcg7eBr7aT6DxrAoILpc3uXNfcuAvZSnQK
|
|||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
|
@ -721,6 +732,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/notaryproject/notation-core-go v1.0.2 h1:VEt+mbsgdANd9b4jqgmx2C7U0DmwynOuD2Nhxh3bANw=
|
||||
github.com/notaryproject/notation-core-go v1.0.2/go.mod h1:2HkQzUwg08B3x9oVIztHsEh7Vil2Rj+tYgxH+JObLX4=
|
||||
github.com/notaryproject/notation-go v1.1.0 h1:7WBeH8FGoA+GkeUwmBIBnlJc/PpdYaUKfiXu6ZZeEeg=
|
||||
github.com/notaryproject/notation-go v1.1.0/go.mod h1:ZSk34URQar5fnWflaFByzpDvuefgZKm/mp8Q2tQpBaw=
|
||||
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE=
|
||||
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
|
@ -748,8 +763,8 @@ github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be h1:f2Pl
|
|||
github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98 h1:LTxrNWOPwquJy9Cu3oz6QHJIO5M5gNyOZtSybXdyLA4=
|
||||
github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98/go.mod h1:kqQaIc6bZstKgnGpL7GD5dWoLKbA6mH1Y9ULjGImBnM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs=
|
||||
github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
|
||||
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
|
@ -930,8 +945,12 @@ github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXG
|
|||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
|
||||
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
|
||||
github.com/veraison/go-cose v1.2.0 h1:Ok0Hr3GMAf8K/1NB4sV65QGgCiukG1w1QD+H5tmt0Ow=
|
||||
github.com/veraison/go-cose v1.2.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/go-gitlab v0.96.0 h1:LGkZ+wSNMRtHIBaYE4Hq3dZVjprwHv3Y1+rhKU3WETs=
|
||||
github.com/xanzy/go-gitlab v0.96.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
|
@ -1039,6 +1058,7 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
|
|||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
@ -1139,6 +1159,7 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
@ -1148,6 +1169,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -1161,6 +1183,7 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
|
@ -1289,6 +1312,8 @@ k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6R
|
|||
k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
oras.land/oras-go v1.2.4 h1:djpBY2/2Cs1PV87GSJlxv4voajVOMZxqqtq9AB8YNvY=
|
||||
oras.land/oras-go v1.2.4/go.mod h1:DYcGfb3YF1nKjcezfX2SNlDAeQFKSXmf+qrFmrh4324=
|
||||
oras.land/oras-go/v2 v2.3.1 h1:lUC6q8RkeRReANEERLfH86iwGn55lbSWP20egdFHVec=
|
||||
oras.land/oras-go/v2 v2.3.1/go.mod h1:5AQXVEu1X/FKp1F9DMOb5ZItZBOa0y5dha0yCm4NR9c=
|
||||
sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4=
|
||||
sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
|
|
|
@ -144,6 +144,12 @@ kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/helmchart-from-oc
|
|||
kubectl -n source-system wait helmchart/podinfo --for=condition=ready --timeout=1m
|
||||
kubectl -n source-system wait helmchart/podinfo-keyless --for=condition=ready --timeout=1m
|
||||
|
||||
kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/helmchart-from-oci/notation.yaml"
|
||||
curl -sSLo notation.crt https://raw.githubusercontent.com/stefanprodan/podinfo/master/.notation/notation.crt
|
||||
curl -sSLo trustpolicy.json https://raw.githubusercontent.com/stefanprodan/podinfo/master/.notation/trustpolicy.json
|
||||
kubectl -n source-system create secret generic notation-config --from-file=notation.crt --from-file=trustpolicy.json --dry-run=client -o yaml | kubectl apply -f -
|
||||
kubectl -n source-system wait helmchart/podinfo-notation --for=condition=ready --timeout=1m
|
||||
|
||||
echo "Run OCIRepository verify tests"
|
||||
kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/ocirepository/signed-with-key.yaml"
|
||||
kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/ocirepository/signed-with-keyless.yaml"
|
||||
|
@ -152,3 +158,6 @@ kubectl -n source-system create secret generic cosign-key --from-file=cosign.pub
|
|||
|
||||
kubectl -n source-system wait ocirepository/podinfo-deploy-signed-with-key --for=condition=ready --timeout=1m
|
||||
kubectl -n source-system wait ocirepository/podinfo-deploy-signed-with-keyless --for=condition=ready --timeout=1m
|
||||
|
||||
kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/ocirepository/signed-with-notation.yaml"
|
||||
kubectl -n source-system wait ocirepository/podinfo-deploy-signed-with-notation --for=condition=ready --timeout=1m
|
||||
|
|
|
@ -19,6 +19,7 @@ package controller
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
@ -29,6 +30,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/sigstore/cosign/v2/pkg/cosign"
|
||||
helmgetter "helm.sh/helm/v3/pkg/getter"
|
||||
|
@ -69,7 +71,10 @@ import (
|
|||
"github.com/fluxcd/source-controller/internal/helm/chart"
|
||||
"github.com/fluxcd/source-controller/internal/helm/getter"
|
||||
"github.com/fluxcd/source-controller/internal/helm/repository"
|
||||
"github.com/fluxcd/source-controller/internal/oci"
|
||||
soci "github.com/fluxcd/source-controller/internal/oci"
|
||||
scosign "github.com/fluxcd/source-controller/internal/oci/cosign"
|
||||
"github.com/fluxcd/source-controller/internal/oci/notation"
|
||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
|
||||
"github.com/fluxcd/source-controller/internal/util"
|
||||
|
@ -579,7 +584,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
|||
provider := obj.Spec.Verify.Provider
|
||||
verifiers, err = r.makeVerifiers(ctx, obj, *clientOpts)
|
||||
if err != nil {
|
||||
if obj.Spec.Verify.SecretRef == nil {
|
||||
if obj.Spec.Verify.SecretRef == nil && obj.Spec.Verify.Provider == "cosign" {
|
||||
provider = fmt.Sprintf("%s keyless", provider)
|
||||
}
|
||||
e := serror.NewGeneric(
|
||||
|
@ -1244,7 +1249,9 @@ func observeChartBuild(ctx context.Context, sp *patch.SerialPatcher, pOpts []pat
|
|||
if build.Complete() {
|
||||
conditions.Delete(obj, sourcev1.FetchFailedCondition)
|
||||
conditions.Delete(obj, sourcev1.BuildFailedCondition)
|
||||
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, fmt.Sprintf("verified signature of version %s", build.Version))
|
||||
if build.VerifiedResult == oci.VerificationResultSuccess {
|
||||
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, fmt.Sprintf("verified signature of version %s", build.Version))
|
||||
}
|
||||
}
|
||||
|
||||
if obj.Spec.Verify == nil {
|
||||
|
@ -1318,26 +1325,27 @@ func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *helmv1.Hel
|
|||
|
||||
switch obj.Spec.Verify.Provider {
|
||||
case "cosign":
|
||||
defaultCosignOciOpts := []soci.Options{
|
||||
soci.WithRemoteOptions(verifyOpts...),
|
||||
defaultCosignOciOpts := []scosign.Options{
|
||||
scosign.WithRemoteOptions(verifyOpts...),
|
||||
}
|
||||
|
||||
// get the public keys from the given secret
|
||||
if secretRef := obj.Spec.Verify.SecretRef; secretRef != nil {
|
||||
certSecretName := types.NamespacedName{
|
||||
|
||||
verifySecret := types.NamespacedName{
|
||||
Namespace: obj.Namespace,
|
||||
Name: secretRef.Name,
|
||||
}
|
||||
|
||||
var pubSecret corev1.Secret
|
||||
if err := r.Get(ctx, certSecretName, &pubSecret); err != nil {
|
||||
pubSecret, err := r.retrieveSecret(ctx, verifySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, data := range pubSecret.Data {
|
||||
// search for public keys in the secret
|
||||
if strings.HasSuffix(k, ".pub") {
|
||||
verifier, err := soci.NewCosignVerifier(ctx, append(defaultCosignOciOpts, soci.WithPublicKey(data))...)
|
||||
verifier, err := scosign.NewCosignVerifier(ctx, append(defaultCosignOciOpts, scosign.WithPublicKey(data))...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1346,7 +1354,7 @@ func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *helmv1.Hel
|
|||
}
|
||||
|
||||
if len(verifiers) == 0 {
|
||||
return nil, fmt.Errorf("no public keys found in secret '%s'", certSecretName)
|
||||
return nil, fmt.Errorf("no public keys found in secret '%s'", verifySecret.String())
|
||||
}
|
||||
return verifiers, nil
|
||||
}
|
||||
|
@ -1359,9 +1367,67 @@ func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *helmv1.Hel
|
|||
SubjectRegExp: match.Subject,
|
||||
})
|
||||
}
|
||||
defaultCosignOciOpts = append(defaultCosignOciOpts, soci.WithIdentities(identities))
|
||||
defaultCosignOciOpts = append(defaultCosignOciOpts, scosign.WithIdentities(identities))
|
||||
|
||||
verifier, err := soci.NewCosignVerifier(ctx, defaultCosignOciOpts...)
|
||||
verifier, err := scosign.NewCosignVerifier(ctx, defaultCosignOciOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verifiers = append(verifiers, verifier)
|
||||
return verifiers, nil
|
||||
case "notation":
|
||||
// get the public keys from the given secret
|
||||
secretRef := obj.Spec.Verify.SecretRef
|
||||
|
||||
if secretRef == nil {
|
||||
return nil, fmt.Errorf("verification secret cannot be empty: '%s'", obj.Name)
|
||||
}
|
||||
|
||||
verifySecret := types.NamespacedName{
|
||||
Namespace: obj.Namespace,
|
||||
Name: secretRef.Name,
|
||||
}
|
||||
|
||||
pubSecret, err := r.retrieveSecret(ctx, verifySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, ok := pubSecret.Data[notation.DefaultTrustPolicyKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' not found in secret '%s'", notation.DefaultTrustPolicyKey, verifySecret.String())
|
||||
}
|
||||
|
||||
var doc trustpolicy.Document
|
||||
|
||||
if err := json.Unmarshal(data, &doc); err != nil {
|
||||
return nil, fmt.Errorf("error occurred while parsing %s: %w", notation.DefaultTrustPolicyKey, err)
|
||||
}
|
||||
|
||||
var certs [][]byte
|
||||
|
||||
for k, data := range pubSecret.Data {
|
||||
if strings.HasSuffix(k, ".crt") || strings.HasSuffix(k, ".pem") {
|
||||
certs = append(certs, data)
|
||||
}
|
||||
}
|
||||
|
||||
if certs == nil {
|
||||
return nil, fmt.Errorf("no certificates found in secret '%s'", verifySecret.String())
|
||||
}
|
||||
|
||||
trustPolicy := notation.CleanTrustPolicy(&doc, ctrl.LoggerFrom(ctx))
|
||||
defaultNotationOciOpts := []notation.Options{
|
||||
notation.WithTrustPolicy(trustPolicy),
|
||||
notation.WithRemoteOptions(verifyOpts...),
|
||||
notation.WithAuth(clientOpts.Authenticator),
|
||||
notation.WithKeychain(clientOpts.Keychain),
|
||||
notation.WithInsecureRegistry(clientOpts.Insecure),
|
||||
notation.WithLogger(ctrl.LoggerFrom(ctx)),
|
||||
notation.WithRootCertificates(certs),
|
||||
}
|
||||
|
||||
verifier, err := notation.NewNotationVerifier(defaultNotationOciOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1371,3 +1437,15 @@ func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *helmv1.Hel
|
|||
return nil, fmt.Errorf("unsupported verification provider: %s", obj.Spec.Verify.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
// retrieveSecret retrieves a secret from the specified namespace with the given secret name.
|
||||
// It returns the retrieved secret and any error encountered during the retrieval process.
|
||||
func (r *HelmChartReconciler) retrieveSecret(ctx context.Context, verifySecret types.NamespacedName) (corev1.Secret, error) {
|
||||
|
||||
var pubSecret corev1.Secret
|
||||
|
||||
if err := r.Get(ctx, verifySecret, &pubSecret); err != nil {
|
||||
return corev1.Secret{}, err
|
||||
}
|
||||
return pubSecret, nil
|
||||
}
|
||||
|
|
|
@ -19,7 +19,9 @@ package controller
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -34,6 +36,12 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/foxcpp/go-mockdns"
|
||||
"github.com/notaryproject/notation-core-go/signature/cose"
|
||||
"github.com/notaryproject/notation-core-go/testhelper"
|
||||
"github.com/notaryproject/notation-go"
|
||||
nr "github.com/notaryproject/notation-go/registry"
|
||||
"github.com/notaryproject/notation-go/signer"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
. "github.com/onsi/gomega"
|
||||
coptions "github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
|
||||
|
@ -45,6 +53,7 @@ import (
|
|||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/record"
|
||||
oras "oras.land/oras-go/v2/registry/remote"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
@ -66,6 +75,7 @@ import (
|
|||
"github.com/fluxcd/source-controller/internal/helm/chart/secureloader"
|
||||
"github.com/fluxcd/source-controller/internal/helm/registry"
|
||||
"github.com/fluxcd/source-controller/internal/oci"
|
||||
snotation "github.com/fluxcd/source-controller/internal/oci/notation"
|
||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
|
||||
)
|
||||
|
@ -2733,7 +2743,331 @@ func TestHelmChartRepository_reconcileSource_verifyOCISourceSignature_keyless(t
|
|||
}
|
||||
}
|
||||
|
||||
func TestHelmChartReconciler_reconcileSourceFromOCI_verifySignature(t *testing.T) {
|
||||
func TestHelmChartReconciler_reconcileSourceFromOCI_verifySignatureNotation(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
server, err := setupRegistryServer(ctx, tmpDir, registryOptions{})
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
t.Cleanup(func() {
|
||||
server.Close()
|
||||
})
|
||||
|
||||
const (
|
||||
chartPath = "testdata/charts/helmchart-0.1.0.tgz"
|
||||
)
|
||||
|
||||
// Load a test chart
|
||||
chartData, err := os.ReadFile(chartPath)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Upload the test chart
|
||||
metadata, err := loadTestChartToOCI(chartData, server, "", "", "")
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
cachedArtifact := &sourcev1.Artifact{
|
||||
Revision: "0.1.0",
|
||||
Path: metadata.Name + "-" + metadata.Version + ".tgz",
|
||||
}
|
||||
g.Expect(storage.CopyFromPath(cachedArtifact, "testdata/charts/helmchart-0.1.0.tgz")).To(Succeed())
|
||||
|
||||
certTuple := testhelper.GetRSASelfSignedSigningCertTuple("notation self-signed certs for testing")
|
||||
certs := []*x509.Certificate{certTuple.Cert}
|
||||
|
||||
signer, err := signer.New(certTuple.PrivateKey, certs)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
policyDocument := trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelStrict.Name, Override: map[trustpolicy.ValidationType]trustpolicy.ValidationAction{trustpolicy.TypeRevocation: trustpolicy.ActionSkip}},
|
||||
TrustStores: []string{"ca:valid-trust-store"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
shouldSign bool
|
||||
beforeFunc func(obj *helmv1.HelmChart)
|
||||
want sreconcile.Result
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
addMultipleCerts bool
|
||||
provideNoCert bool
|
||||
provideNoPolicy bool
|
||||
assertConditions []metav1.Condition
|
||||
cleanFunc func(g *WithT, build *chart.Build)
|
||||
}{
|
||||
{
|
||||
name: "unsigned charts should not pass verification",
|
||||
beforeFunc: func(obj *helmv1.HelmChart) {
|
||||
obj.Spec.Chart = metadata.Name
|
||||
obj.Spec.Version = metadata.Version
|
||||
obj.Spec.Verify = &helmv1.OCIRepositoryVerification{
|
||||
Provider: "notation",
|
||||
SecretRef: &meta.LocalObjectReference{Name: "notation-config"},
|
||||
}
|
||||
},
|
||||
want: sreconcile.ResultEmpty,
|
||||
wantErr: true,
|
||||
wantErrMsg: "chart verification error: failed to verify <url>: no signature",
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.BuildFailedCondition, "ChartVerificationError", "chart verification error: failed to verify <url>: no signature"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "chart verification error: failed to verify <url>: no signature"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "signed charts should pass verification",
|
||||
shouldSign: true,
|
||||
beforeFunc: func(obj *helmv1.HelmChart) {
|
||||
obj.Spec.Chart = metadata.Name
|
||||
obj.Spec.Version = metadata.Version
|
||||
obj.Spec.Verify = &helmv1.OCIRepositoryVerification{
|
||||
Provider: "notation",
|
||||
SecretRef: &meta.LocalObjectReference{Name: "notation-config"},
|
||||
}
|
||||
},
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of version <version>"),
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<version>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<version>'"),
|
||||
},
|
||||
cleanFunc: func(g *WithT, build *chart.Build) {
|
||||
g.Expect(os.Remove(build.Path)).To(Succeed())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple certs should still pass verification",
|
||||
addMultipleCerts: true,
|
||||
beforeFunc: func(obj *helmv1.HelmChart) {
|
||||
obj.Spec.Chart = metadata.Name
|
||||
obj.Spec.Version = metadata.Version
|
||||
obj.Spec.Verify = &helmv1.OCIRepositoryVerification{
|
||||
Provider: "notation",
|
||||
SecretRef: &meta.LocalObjectReference{Name: "notation-config"},
|
||||
}
|
||||
},
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of version <version>"),
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<version>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<version>'"),
|
||||
},
|
||||
cleanFunc: func(g *WithT, build *chart.Build) {
|
||||
g.Expect(os.Remove(build.Path)).To(Succeed())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verify failed before, removed from spec, remove condition",
|
||||
beforeFunc: func(obj *helmv1.HelmChart) {
|
||||
obj.Spec.Chart = metadata.Name
|
||||
obj.Spec.Version = metadata.Version
|
||||
obj.Spec.Verify = nil
|
||||
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, "VerifyFailed", "fail msg")
|
||||
obj.Status.Artifact = &sourcev1.Artifact{Path: metadata.Name + "-" + metadata.Version + ".tgz"}
|
||||
},
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled '<name>' chart with version '<version>'"),
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<version>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<version>'"),
|
||||
},
|
||||
cleanFunc: func(g *WithT, build *chart.Build) {
|
||||
g.Expect(os.Remove(build.Path)).To(Succeed())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no cert provided should not pass verification",
|
||||
beforeFunc: func(obj *helmv1.HelmChart) {
|
||||
obj.Spec.Chart = metadata.Name
|
||||
obj.Spec.Version = metadata.Version
|
||||
obj.Spec.Verify = &helmv1.OCIRepositoryVerification{
|
||||
Provider: "notation",
|
||||
SecretRef: &meta.LocalObjectReference{Name: "notation-config"},
|
||||
}
|
||||
},
|
||||
wantErr: true,
|
||||
provideNoCert: true,
|
||||
// no namespace but the namespace name should appear before the /notation-config
|
||||
wantErrMsg: "failed to verify the signature using provider 'notation': no certificates found in secret '/notation-config'",
|
||||
want: sreconcile.ResultEmpty,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "failed to verify the signature using provider 'notation': no certificates found in secret '/notation-config'"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider 'notation': no certificates found in secret '/notation-config'"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty string should fail verification",
|
||||
beforeFunc: func(obj *helmv1.HelmChart) {
|
||||
obj.Spec.Chart = metadata.Name
|
||||
obj.Spec.Version = metadata.Version
|
||||
obj.Spec.Verify = &helmv1.OCIRepositoryVerification{
|
||||
Provider: "notation",
|
||||
SecretRef: &meta.LocalObjectReference{Name: "notation-config"},
|
||||
}
|
||||
},
|
||||
provideNoPolicy: true,
|
||||
wantErr: true,
|
||||
wantErrMsg: fmt.Sprintf("failed to verify the signature using provider 'notation': '%s' not found in secret '/notation-config'", snotation.DefaultTrustPolicyKey),
|
||||
want: sreconcile.ResultEmpty,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", fmt.Sprintf("failed to verify the signature using provider 'notation': '%s' not found in secret '/notation-config'", snotation.DefaultTrustPolicyKey)),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, fmt.Sprintf("failed to verify the signature using provider 'notation': '%s' not found in secret '/notation-config'", snotation.DefaultTrustPolicyKey)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
clientBuilder := fakeclient.NewClientBuilder()
|
||||
|
||||
repository := &helmv1.HelmRepository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "helmrepository-",
|
||||
},
|
||||
Spec: helmv1.HelmRepositorySpec{
|
||||
URL: fmt.Sprintf("oci://%s/testrepo", server.registryHost),
|
||||
Timeout: &metav1.Duration{Duration: timeout},
|
||||
Provider: helmv1.GenericOCIProvider,
|
||||
Type: helmv1.HelmRepositoryTypeOCI,
|
||||
Insecure: true,
|
||||
},
|
||||
}
|
||||
|
||||
policy, err := json.Marshal(policyDocument)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
data := map[string][]byte{}
|
||||
|
||||
if tt.addMultipleCerts {
|
||||
data["a.crt"] = testhelper.GetRSASelfSignedSigningCertTuple("a not used for signing").Cert.Raw
|
||||
data["b.crt"] = testhelper.GetRSASelfSignedSigningCertTuple("b not used for signing").Cert.Raw
|
||||
data["c.crt"] = testhelper.GetRSASelfSignedSigningCertTuple("c not used for signing").Cert.Raw
|
||||
}
|
||||
|
||||
if !tt.provideNoCert {
|
||||
data["notation.crt"] = certTuple.Cert.Raw
|
||||
}
|
||||
|
||||
if !tt.provideNoPolicy {
|
||||
data["trustpolicy.json"] = policy
|
||||
}
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "notation-config",
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
|
||||
caSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "valid-trust-store",
|
||||
Generation: 1,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"ca.crt": tlsCA,
|
||||
},
|
||||
}
|
||||
|
||||
clientBuilder.WithObjects(repository, secret, caSecret)
|
||||
|
||||
r := &HelmChartReconciler{
|
||||
Client: clientBuilder.Build(),
|
||||
EventRecorder: record.NewFakeRecorder(32),
|
||||
Getters: testGetters,
|
||||
Storage: storage,
|
||||
RegistryClientGenerator: registry.ClientGenerator,
|
||||
patchOptions: getPatchOptions(helmChartReadyCondition.Owned, "sc"),
|
||||
}
|
||||
|
||||
obj := &helmv1.HelmChart{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "helmchart-",
|
||||
},
|
||||
Spec: helmv1.HelmChartSpec{
|
||||
SourceRef: helmv1.LocalHelmChartSourceReference{
|
||||
Kind: helmv1.HelmRepositoryKind,
|
||||
Name: repository.Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
chartUrl := fmt.Sprintf("oci://%s/testrepo/%s:%s", server.registryHost, metadata.Name, metadata.Version)
|
||||
|
||||
if tt.beforeFunc != nil {
|
||||
tt.beforeFunc(obj)
|
||||
}
|
||||
|
||||
if tt.shouldSign {
|
||||
artifact := fmt.Sprintf("%s/testrepo/%s:%s", server.registryHost, metadata.Name, metadata.Version)
|
||||
|
||||
remoteRepo, err := oras.NewRepository(artifact)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
remoteRepo.PlainHTTP = true
|
||||
|
||||
repo := nr.NewRepository(remoteRepo)
|
||||
|
||||
signatureMediaType := cose.MediaTypeEnvelope
|
||||
|
||||
signOptions := notation.SignOptions{
|
||||
SignerSignOptions: notation.SignerSignOptions{
|
||||
SignatureMediaType: signatureMediaType,
|
||||
},
|
||||
ArtifactReference: artifact,
|
||||
}
|
||||
|
||||
_, err = notation.Sign(ctx, signer, repo, signOptions)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
assertConditions := tt.assertConditions
|
||||
for k := range assertConditions {
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<name>", metadata.Name)
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<version>", metadata.Version)
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<url>", chartUrl)
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<provider>", "notation")
|
||||
}
|
||||
|
||||
var b chart.Build
|
||||
if tt.cleanFunc != nil {
|
||||
defer tt.cleanFunc(g, &b)
|
||||
}
|
||||
|
||||
g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred())
|
||||
defer func() {
|
||||
g.Expect(r.Client.Delete(context.TODO(), obj)).ToNot(HaveOccurred())
|
||||
}()
|
||||
|
||||
sp := patch.NewSerialPatcher(obj, r.Client)
|
||||
|
||||
got, err := r.reconcileSource(ctx, sp, obj, &b)
|
||||
if tt.wantErr {
|
||||
tt.wantErrMsg = strings.ReplaceAll(tt.wantErrMsg, "<url>", chartUrl)
|
||||
g.Expect(err).ToNot(BeNil())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErrMsg))
|
||||
} else {
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmChartReconciler_reconcileSourceFromOCI_verifySignatureCosign(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
|
|
@ -19,6 +19,7 @@ package controller
|
|||
import (
|
||||
"context"
|
||||
cryptotls "crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -35,6 +36,7 @@ import (
|
|||
"github.com/google/go-containerregistry/pkg/name"
|
||||
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/sigstore/cosign/v2/pkg/cosign"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
@ -68,6 +70,8 @@ import (
|
|||
ociv1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||
serror "github.com/fluxcd/source-controller/internal/error"
|
||||
soci "github.com/fluxcd/source-controller/internal/oci"
|
||||
scosign "github.com/fluxcd/source-controller/internal/oci/cosign"
|
||||
"github.com/fluxcd/source-controller/internal/oci/notation"
|
||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
|
||||
"github.com/fluxcd/source-controller/internal/tls"
|
||||
|
@ -430,10 +434,10 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
|
|||
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
|
||||
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) {
|
||||
|
||||
err := r.verifySignature(ctx, obj, ref, opts...)
|
||||
result, err := r.verifySignature(ctx, obj, ref, keychain, auth, opts...)
|
||||
if err != nil {
|
||||
provider := obj.Spec.Verify.Provider
|
||||
if obj.Spec.Verify.SecretRef == nil {
|
||||
if obj.Spec.Verify.SecretRef == nil && obj.Spec.Verify.Provider == "cosign" {
|
||||
provider = fmt.Sprintf("%s keyless", provider)
|
||||
}
|
||||
e := serror.NewGeneric(
|
||||
|
@ -444,7 +448,9 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
|
|||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision %s", revision)
|
||||
if result == soci.VerificationResultSuccess {
|
||||
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision %s", revision)
|
||||
}
|
||||
}
|
||||
|
||||
// Skip pulling if the artifact revision and the source configuration has
|
||||
|
@ -609,38 +615,42 @@ func (r *OCIRepositoryReconciler) digestFromRevision(revision string) string {
|
|||
}
|
||||
|
||||
// verifySignature verifies the authenticity of the given image reference URL.
|
||||
// It supports two different verification providers: cosign and notation.
|
||||
// First, it tries to use a key if a Secret with a valid public key is provided.
|
||||
// If not, it falls back to a keyless approach for verification.
|
||||
func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *ociv1.OCIRepository, ref name.Reference, opt ...remote.Option) error {
|
||||
// If not, when using cosign it falls back to a keyless approach for verification.
|
||||
// When notation is used, a trust policy is required to verify the image.
|
||||
// The verification result is returned as a VerificationResult and any error encountered.
|
||||
func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *ociv1.OCIRepository, ref name.Reference, keychain authn.Keychain, auth authn.Authenticator, opt ...remote.Option) (soci.VerificationResult, error) {
|
||||
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
|
||||
defer cancel()
|
||||
|
||||
provider := obj.Spec.Verify.Provider
|
||||
switch provider {
|
||||
case "cosign":
|
||||
defaultCosignOciOpts := []soci.Options{
|
||||
soci.WithRemoteOptions(opt...),
|
||||
defaultCosignOciOpts := []scosign.Options{
|
||||
scosign.WithRemoteOptions(opt...),
|
||||
}
|
||||
|
||||
// get the public keys from the given secret
|
||||
if secretRef := obj.Spec.Verify.SecretRef; secretRef != nil {
|
||||
certSecretName := types.NamespacedName{
|
||||
|
||||
verifySecret := types.NamespacedName{
|
||||
Namespace: obj.Namespace,
|
||||
Name: secretRef.Name,
|
||||
}
|
||||
|
||||
var pubSecret corev1.Secret
|
||||
if err := r.Get(ctxTimeout, certSecretName, &pubSecret); err != nil {
|
||||
return err
|
||||
pubSecret, err := r.retrieveSecret(ctxTimeout, verifySecret)
|
||||
if err != nil {
|
||||
return soci.VerificationResultFailed, err
|
||||
}
|
||||
|
||||
signatureVerified := false
|
||||
for k, data := range pubSecret.Data {
|
||||
// search for public keys in the secret
|
||||
if strings.HasSuffix(k, ".pub") {
|
||||
verifier, err := soci.NewCosignVerifier(ctxTimeout, append(defaultCosignOciOpts, soci.WithPublicKey(data))...)
|
||||
verifier, err := scosign.NewCosignVerifier(ctxTimeout, append(defaultCosignOciOpts, scosign.WithPublicKey(data))...)
|
||||
if err != nil {
|
||||
return err
|
||||
return soci.VerificationResultFailed, err
|
||||
}
|
||||
|
||||
signatures, _, err := verifier.VerifyImageSignatures(ctxTimeout, ref)
|
||||
|
@ -656,10 +666,10 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *ociv
|
|||
}
|
||||
|
||||
if !signatureVerified {
|
||||
return fmt.Errorf("no matching signatures were found for '%s'", ref)
|
||||
return soci.VerificationResultFailed, fmt.Errorf("no matching signatures were found for '%s'", ref)
|
||||
}
|
||||
|
||||
return nil
|
||||
return soci.VerificationResultSuccess, nil
|
||||
}
|
||||
|
||||
// if no secret is provided, try keyless verification
|
||||
|
@ -672,26 +682,105 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *ociv
|
|||
SubjectRegExp: match.Subject,
|
||||
})
|
||||
}
|
||||
defaultCosignOciOpts = append(defaultCosignOciOpts, soci.WithIdentities(identities))
|
||||
defaultCosignOciOpts = append(defaultCosignOciOpts, scosign.WithIdentities(identities))
|
||||
|
||||
verifier, err := soci.NewCosignVerifier(ctxTimeout, defaultCosignOciOpts...)
|
||||
verifier, err := scosign.NewCosignVerifier(ctxTimeout, defaultCosignOciOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
return soci.VerificationResultFailed, err
|
||||
}
|
||||
|
||||
signatures, _, err := verifier.VerifyImageSignatures(ctxTimeout, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
return soci.VerificationResultFailed, err
|
||||
}
|
||||
|
||||
if len(signatures) > 0 {
|
||||
return nil
|
||||
return soci.VerificationResultSuccess, nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("no matching signatures were found for '%s'", ref)
|
||||
}
|
||||
return soci.VerificationResultFailed, fmt.Errorf("no matching signatures were found for '%s'", ref)
|
||||
|
||||
return nil
|
||||
case "notation":
|
||||
// get the public keys from the given secret
|
||||
secretRef := obj.Spec.Verify.SecretRef
|
||||
|
||||
if secretRef == nil {
|
||||
return soci.VerificationResultFailed, fmt.Errorf("verification secret cannot be empty: '%s'", ref)
|
||||
}
|
||||
|
||||
verifySecret := types.NamespacedName{
|
||||
Namespace: obj.Namespace,
|
||||
Name: secretRef.Name,
|
||||
}
|
||||
|
||||
pubSecret, err := r.retrieveSecret(ctxTimeout, verifySecret)
|
||||
if err != nil {
|
||||
return soci.VerificationResultFailed, err
|
||||
}
|
||||
|
||||
data, ok := pubSecret.Data[notation.DefaultTrustPolicyKey]
|
||||
if !ok {
|
||||
return soci.VerificationResultFailed, fmt.Errorf("'%s' not found in secret '%s'", notation.DefaultTrustPolicyKey, verifySecret.String())
|
||||
}
|
||||
|
||||
var doc trustpolicy.Document
|
||||
|
||||
if err := json.Unmarshal(data, &doc); err != nil {
|
||||
return soci.VerificationResultFailed, fmt.Errorf("error occurred while parsing %s: %w", notation.DefaultTrustPolicyKey, err)
|
||||
}
|
||||
|
||||
var certs [][]byte
|
||||
|
||||
for k, data := range pubSecret.Data {
|
||||
if strings.HasSuffix(k, ".crt") || strings.HasSuffix(k, ".pem") {
|
||||
certs = append(certs, data)
|
||||
}
|
||||
}
|
||||
|
||||
if certs == nil {
|
||||
return soci.VerificationResultFailed, fmt.Errorf("no certificates found in secret '%s'", verifySecret.String())
|
||||
}
|
||||
|
||||
trustPolicy := notation.CleanTrustPolicy(&doc, ctrl.LoggerFrom(ctx))
|
||||
defaultNotationOciOpts := []notation.Options{
|
||||
notation.WithTrustPolicy(trustPolicy),
|
||||
notation.WithRemoteOptions(opt...),
|
||||
notation.WithAuth(auth),
|
||||
notation.WithKeychain(keychain),
|
||||
notation.WithInsecureRegistry(obj.Spec.Insecure),
|
||||
notation.WithLogger(ctrl.LoggerFrom(ctx)),
|
||||
notation.WithRootCertificates(certs),
|
||||
}
|
||||
|
||||
verifier, err := notation.NewNotationVerifier(defaultNotationOciOpts...)
|
||||
if err != nil {
|
||||
return soci.VerificationResultFailed, err
|
||||
}
|
||||
|
||||
result, err := verifier.Verify(ctxTimeout, ref)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if result == soci.VerificationResultFailed {
|
||||
return soci.VerificationResultFailed, fmt.Errorf("no matching signatures were found for '%s'", ref)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
default:
|
||||
return soci.VerificationResultFailed, fmt.Errorf("unsupported verification provider: %s", obj.Spec.Verify.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
// retrieveSecret retrieves a secret from the specified namespace with the given secret name.
|
||||
// It returns the retrieved secret and any error encountered during the retrieval process.
|
||||
func (r *OCIRepositoryReconciler) retrieveSecret(ctx context.Context, verifySecret types.NamespacedName) (corev1.Secret, error) {
|
||||
var pubSecret corev1.Secret
|
||||
|
||||
if err := r.Get(ctx, verifySecret, &pubSecret); err != nil {
|
||||
return corev1.Secret{}, err
|
||||
}
|
||||
return pubSecret, nil
|
||||
}
|
||||
|
||||
// parseRepository validates and extracts the repository URL.
|
||||
|
|
|
@ -19,6 +19,7 @@ package controller
|
|||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -35,7 +36,14 @@ import (
|
|||
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/notaryproject/notation-core-go/signature/cose"
|
||||
"github.com/notaryproject/notation-core-go/testhelper"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/registry"
|
||||
"github.com/notaryproject/notation-go/signer"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
. "github.com/onsi/gomega"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
coptions "github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
|
||||
"github.com/sigstore/cosign/v2/pkg/cosign"
|
||||
|
@ -44,6 +52,7 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/utils/ptr"
|
||||
oras "oras.land/oras-go/v2/registry/remote"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
@ -62,6 +71,7 @@ import (
|
|||
ociv1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||
intdigest "github.com/fluxcd/source-controller/internal/digest"
|
||||
serror "github.com/fluxcd/source-controller/internal/error"
|
||||
snotation "github.com/fluxcd/source-controller/internal/oci/notation"
|
||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||
)
|
||||
|
||||
|
@ -1167,7 +1177,715 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
|
||||
func TestOCIRepository_reconcileSource_verifyOCISourceSignatureNotation(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reference *ociv1.OCIRepositoryRef
|
||||
insecure bool
|
||||
want sreconcile.Result
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
shouldSign bool
|
||||
useDigest bool
|
||||
addMultipleCerts bool
|
||||
provideNoCert bool
|
||||
beforeFunc func(obj *ociv1.OCIRepository, tag, revision string)
|
||||
assertConditions []metav1.Condition
|
||||
}{
|
||||
{
|
||||
name: "signed image should pass verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.4",
|
||||
},
|
||||
shouldSign: true,
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision <revision>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsigned image should not pass verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.5",
|
||||
},
|
||||
wantErr: true,
|
||||
useDigest: true,
|
||||
wantErrMsg: "failed to verify the signature using provider 'notation': no signature is associated with \"<url>\"",
|
||||
want: sreconcile.ResultEmpty,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider '<provider>': no signature is associated with \"<url>\", make sure the artifact was signed successfully"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verify failed before, removed from spec, remove condition",
|
||||
reference: &ociv1.OCIRepositoryRef{Tag: "6.1.4"},
|
||||
beforeFunc: func(obj *ociv1.OCIRepository, tag, revision string) {
|
||||
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, "VerifyFailed", "fail msg")
|
||||
obj.Spec.Verify = nil
|
||||
obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s@%s", tag, revision)}
|
||||
},
|
||||
want: sreconcile.ResultSuccess,
|
||||
},
|
||||
{
|
||||
name: "same artifact, verified before, change in obj gen verify again",
|
||||
reference: &ociv1.OCIRepositoryRef{Tag: "6.1.4"},
|
||||
shouldSign: true,
|
||||
beforeFunc: func(obj *ociv1.OCIRepository, tag, revision string) {
|
||||
obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s@%s", tag, revision)}
|
||||
// Set Verified with old observed generation and different reason/message.
|
||||
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Verified", "verified")
|
||||
// Set new object generation.
|
||||
obj.SetGeneration(3)
|
||||
},
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision <revision>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no verify for already verified, verified condition remains the same",
|
||||
reference: &ociv1.OCIRepositoryRef{Tag: "6.1.4"},
|
||||
shouldSign: true,
|
||||
beforeFunc: func(obj *ociv1.OCIRepository, tag, revision string) {
|
||||
// Artifact present and custom verified condition reason/message.
|
||||
obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s@%s", tag, revision)}
|
||||
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Verified", "verified")
|
||||
},
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, "Verified", "verified"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "signed image on an insecure registry passes verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.6",
|
||||
},
|
||||
shouldSign: true,
|
||||
insecure: true,
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision <revision>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "signed image on an insecure registry using digest as reference passes verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.6",
|
||||
},
|
||||
shouldSign: true,
|
||||
insecure: true,
|
||||
useDigest: true,
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision <revision>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verification level audit and correct trust identity should pass verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.6",
|
||||
},
|
||||
shouldSign: true,
|
||||
insecure: true,
|
||||
useDigest: true,
|
||||
want: sreconcile.ResultSuccess,
|
||||
addMultipleCerts: true,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision <revision>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no cert provided should not pass verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.5",
|
||||
},
|
||||
wantErr: true,
|
||||
useDigest: true,
|
||||
provideNoCert: true,
|
||||
// no namespace but the namespace name should appear before the /notation-config
|
||||
wantErrMsg: "failed to verify the signature using provider 'notation': no certificates found in secret '/notation-config'",
|
||||
want: sreconcile.ResultEmpty,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider '<provider>': no certificates found in secret '/notation-config'"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
clientBuilder := fakeclient.NewClientBuilder().
|
||||
WithScheme(testEnv.GetScheme()).
|
||||
WithStatusSubresource(&ociv1.OCIRepository{})
|
||||
|
||||
r := &OCIRepositoryReconciler{
|
||||
Client: clientBuilder.Build(),
|
||||
EventRecorder: record.NewFakeRecorder(32),
|
||||
Storage: testStorage,
|
||||
patchOptions: getPatchOptions(ociRepositoryReadyCondition.Owned, "sc"),
|
||||
}
|
||||
|
||||
certTuple := testhelper.GetRSASelfSignedSigningCertTuple("notation self-signed certs for testing")
|
||||
certs := []*x509.Certificate{certTuple.Cert}
|
||||
|
||||
signer, err := signer.New(certTuple.PrivateKey, certs)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
policyDocument := trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelStrict.Name, Override: map[trustpolicy.ValidationType]trustpolicy.ValidationAction{trustpolicy.TypeRevocation: trustpolicy.ActionSkip}},
|
||||
TrustStores: []string{"ca:valid-trust-store"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
policy, err := json.Marshal(policyDocument)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
caSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "valid-trust-store",
|
||||
Generation: 1,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"ca.crt": tlsCA,
|
||||
},
|
||||
}
|
||||
|
||||
g.Expect(r.Create(ctx, caSecret)).ToNot(HaveOccurred())
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
workspaceDir := t.TempDir()
|
||||
regOpts := registryOptions{
|
||||
withTLS: !tt.insecure,
|
||||
}
|
||||
server, err := setupRegistryServer(ctx, workspaceDir, regOpts)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
t.Cleanup(func() {
|
||||
server.Close()
|
||||
})
|
||||
|
||||
obj := &ociv1.OCIRepository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "verify-oci-source-signature-",
|
||||
Generation: 1,
|
||||
},
|
||||
Spec: ociv1.OCIRepositorySpec{
|
||||
URL: fmt.Sprintf("oci://%s/podinfo", server.registryHost),
|
||||
Verify: &ociv1.OCIRepositoryVerification{
|
||||
Provider: "notation",
|
||||
},
|
||||
Interval: metav1.Duration{Duration: interval},
|
||||
Timeout: &metav1.Duration{Duration: timeout},
|
||||
},
|
||||
}
|
||||
|
||||
data := map[string][]byte{}
|
||||
|
||||
if tt.addMultipleCerts {
|
||||
data["a.crt"] = testhelper.GetRSASelfSignedSigningCertTuple("a not used for signing").Cert.Raw
|
||||
data["b.crt"] = testhelper.GetRSASelfSignedSigningCertTuple("b not used for signing").Cert.Raw
|
||||
data["c.crt"] = testhelper.GetRSASelfSignedSigningCertTuple("c not used for signing").Cert.Raw
|
||||
}
|
||||
|
||||
if !tt.provideNoCert {
|
||||
data["notation.crt"] = certTuple.Cert.Raw
|
||||
}
|
||||
|
||||
data["trustpolicy.json"] = policy
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "notation-config",
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
|
||||
g.Expect(r.Create(ctx, secret)).NotTo(HaveOccurred())
|
||||
|
||||
if tt.insecure {
|
||||
obj.Spec.Insecure = true
|
||||
} else {
|
||||
obj.Spec.CertSecretRef = &meta.LocalObjectReference{
|
||||
Name: "valid-trust-store",
|
||||
}
|
||||
}
|
||||
|
||||
obj.Spec.Verify.SecretRef = &meta.LocalObjectReference{Name: "notation-config"}
|
||||
|
||||
if tt.reference != nil {
|
||||
obj.Spec.Reference = tt.reference
|
||||
}
|
||||
|
||||
podinfoVersions, err := pushMultiplePodinfoImages(server.registryHost, tt.insecure, tt.reference.Tag)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
if tt.useDigest {
|
||||
obj.Spec.Reference.Digest = podinfoVersions[tt.reference.Tag].digest.String()
|
||||
}
|
||||
|
||||
keychain, err := r.keychain(ctx, obj)
|
||||
if err != nil {
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
opts := makeRemoteOptions(ctx, makeTransport(true), keychain, nil)
|
||||
|
||||
artifactRef, err := r.getArtifactRef(obj, opts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
if tt.shouldSign {
|
||||
remoteRepo, err := oras.NewRepository(artifactRef.String())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
if tt.insecure {
|
||||
remoteRepo.PlainHTTP = true
|
||||
}
|
||||
|
||||
repo := registry.NewRepository(remoteRepo)
|
||||
|
||||
signatureMediaType := cose.MediaTypeEnvelope
|
||||
|
||||
signOptions := notation.SignOptions{
|
||||
SignerSignOptions: notation.SignerSignOptions{
|
||||
SignatureMediaType: signatureMediaType,
|
||||
},
|
||||
ArtifactReference: artifactRef.String(),
|
||||
}
|
||||
|
||||
_, err = notation.Sign(ctx, signer, repo, signOptions)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
image := podinfoVersions[tt.reference.Tag]
|
||||
assertConditions := tt.assertConditions
|
||||
for k := range assertConditions {
|
||||
if tt.useDigest {
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<revision>", image.digest.String())
|
||||
} else {
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<revision>", fmt.Sprintf("%s@%s", tt.reference.Tag, image.digest.String()))
|
||||
}
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<url>", artifactRef.String())
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<provider>", "notation")
|
||||
}
|
||||
|
||||
if tt.beforeFunc != nil {
|
||||
tt.beforeFunc(obj, image.tag, image.digest.String())
|
||||
}
|
||||
|
||||
g.Expect(r.Client.Create(ctx, obj)).ToNot(HaveOccurred())
|
||||
defer func() {
|
||||
g.Expect(r.Client.Delete(ctx, obj)).ToNot(HaveOccurred())
|
||||
g.Expect(r.Delete(ctx, secret)).NotTo(HaveOccurred())
|
||||
}()
|
||||
|
||||
sp := patch.NewSerialPatcher(obj, r.Client)
|
||||
|
||||
artifact := &sourcev1.Artifact{}
|
||||
got, err := r.reconcileSource(ctx, sp, obj, artifact, tmpDir)
|
||||
if tt.wantErr {
|
||||
tt.wantErrMsg = strings.ReplaceAll(tt.wantErrMsg, "<url>", artifactRef.String())
|
||||
g.Expect(err).ToNot(BeNil())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErrMsg))
|
||||
} else {
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCIRepository_reconcileSource_verifyOCISourceTrustPolicyNotation(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reference *ociv1.OCIRepositoryRef
|
||||
insecure bool
|
||||
signatureVerification trustpolicy.SignatureVerification
|
||||
trustedIdentities []string
|
||||
trustStores []string
|
||||
want sreconcile.Result
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
useDigest bool
|
||||
usePolicyJson bool
|
||||
provideNoPolicy bool
|
||||
policyJson string
|
||||
beforeFunc func(obj *ociv1.OCIRepository, tag, revision string)
|
||||
assertConditions []metav1.Condition
|
||||
}{
|
||||
{
|
||||
name: "verification level audit and incorrect trust identity should fail verification but not error",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.4",
|
||||
},
|
||||
signatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelAudit.Name},
|
||||
trustedIdentities: []string{"x509.subject: C=US, ST=WA, L=Seattle, O=Notary, CN=example.com"},
|
||||
trustStores: []string{"ca:valid-trust-store"},
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verification level permissive and incorrect trust identity should fail verification and error",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.4",
|
||||
},
|
||||
signatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelPermissive.Name},
|
||||
trustedIdentities: []string{"x509.subject: C=US, ST=WA, L=Seattle, O=Notary, CN=example.com"},
|
||||
trustStores: []string{"ca:valid-trust-store"},
|
||||
useDigest: true,
|
||||
want: sreconcile.ResultEmpty,
|
||||
wantErr: true,
|
||||
wantErrMsg: "failed to verify the signature using provider 'notation': signature verification failed\nfailed to verify signature with digest <sigrevision>, signing certificate from the digital signature does not match the X.509 trusted identities [map[\"C\":\"US\" \"CN\":\"example.com\" \"L\":\"Seattle\" \"O\":\"Notary\" \"ST\":\"WA\"]] defined in the trust policy \"test-statement-name\"",
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider 'notation': signature verification failed\nfailed to verify signature with digest <sigrevision>, signing certificate from the digital signature does not match the X.509 trusted identities [map[\"C\":\"US\" \"CN\":\"example.com\" \"L\":\"Seattle\" \"O\":\"Notary\" \"ST\":\"WA\"]] defined in the trust policy \"test-statement-name\""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verification level permissive and correct trust identity should pass verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.4",
|
||||
},
|
||||
signatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelPermissive.Name},
|
||||
trustedIdentities: []string{"*"},
|
||||
trustStores: []string{"ca:valid-trust-store"},
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision <revision>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verification level audit and correct trust identity should pass verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.4",
|
||||
},
|
||||
signatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelAudit.Name},
|
||||
trustedIdentities: []string{"*"},
|
||||
trustStores: []string{"ca:valid-trust-store"},
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision <revision>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verification level skip and should not be marked as verified",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.4",
|
||||
},
|
||||
signatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelSkip.Name},
|
||||
trustedIdentities: []string{},
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid json but empty policy json should fail verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.4",
|
||||
},
|
||||
usePolicyJson: true,
|
||||
policyJson: "{}",
|
||||
wantErr: true,
|
||||
wantErrMsg: "trust policy document is missing or has empty version, it must be specified",
|
||||
want: sreconcile.ResultEmpty,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "trust policy document is missing or has empty version, it must be specified"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty string should fail verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.4",
|
||||
},
|
||||
usePolicyJson: true,
|
||||
policyJson: "",
|
||||
wantErr: true,
|
||||
wantErrMsg: fmt.Sprintf("error occurred while parsing %s: unexpected end of JSON input", snotation.DefaultTrustPolicyKey),
|
||||
want: sreconcile.ResultEmpty,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, fmt.Sprintf("error occurred while parsing %s: unexpected end of JSON input", snotation.DefaultTrustPolicyKey)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid character in string should fail verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.4",
|
||||
},
|
||||
usePolicyJson: true,
|
||||
policyJson: "{\"version\": \"1.0\u000A\", \"trust_policies\": []}",
|
||||
wantErr: true,
|
||||
wantErrMsg: fmt.Sprintf("error occurred while parsing %s: invalid character '\\n' in string literal", snotation.DefaultTrustPolicyKey),
|
||||
want: sreconcile.ResultEmpty,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, fmt.Sprintf("error occurred while parsing %s: invalid character '\\n' in string literal", snotation.DefaultTrustPolicyKey)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty string should fail verification",
|
||||
reference: &ociv1.OCIRepositoryRef{
|
||||
Tag: "6.1.4",
|
||||
},
|
||||
provideNoPolicy: true,
|
||||
wantErr: true,
|
||||
wantErrMsg: fmt.Sprintf("failed to verify the signature using provider 'notation': '%s' not found in secret '/notation'", snotation.DefaultTrustPolicyKey),
|
||||
want: sreconcile.ResultEmpty,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, fmt.Sprintf("failed to verify the signature using provider 'notation': '%s' not found in secret '/notation'", snotation.DefaultTrustPolicyKey)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
clientBuilder := fakeclient.NewClientBuilder().
|
||||
WithScheme(testEnv.GetScheme()).
|
||||
WithStatusSubresource(&ociv1.OCIRepository{})
|
||||
|
||||
r := &OCIRepositoryReconciler{
|
||||
Client: clientBuilder.Build(),
|
||||
EventRecorder: record.NewFakeRecorder(32),
|
||||
Storage: testStorage,
|
||||
patchOptions: getPatchOptions(ociRepositoryReadyCondition.Owned, "sc"),
|
||||
}
|
||||
|
||||
certTuple := testhelper.GetRSASelfSignedSigningCertTuple("notation self-signed certs for testing")
|
||||
certs := []*x509.Certificate{certTuple.Cert}
|
||||
|
||||
signer, err := signer.New(certTuple.PrivateKey, certs)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
caSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "valid-trust-store",
|
||||
Generation: 1,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"ca.crt": tlsCA,
|
||||
},
|
||||
}
|
||||
|
||||
g.Expect(r.Create(ctx, caSecret)).ToNot(HaveOccurred())
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
workspaceDir := t.TempDir()
|
||||
regOpts := registryOptions{
|
||||
withTLS: !tt.insecure,
|
||||
}
|
||||
server, err := setupRegistryServer(ctx, workspaceDir, regOpts)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
t.Cleanup(func() {
|
||||
server.Close()
|
||||
})
|
||||
|
||||
obj := &ociv1.OCIRepository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "verify-oci-source-signature-",
|
||||
Generation: 1,
|
||||
},
|
||||
Spec: ociv1.OCIRepositorySpec{
|
||||
URL: fmt.Sprintf("oci://%s/podinfo", server.registryHost),
|
||||
Verify: &ociv1.OCIRepositoryVerification{
|
||||
Provider: "notation",
|
||||
},
|
||||
Interval: metav1.Duration{Duration: interval},
|
||||
Timeout: &metav1.Duration{Duration: timeout},
|
||||
},
|
||||
}
|
||||
|
||||
var policy []byte
|
||||
|
||||
if !tt.usePolicyJson {
|
||||
policyDocument := trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: tt.signatureVerification,
|
||||
TrustStores: tt.trustStores,
|
||||
TrustedIdentities: tt.trustedIdentities,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
policy, err = json.Marshal(policyDocument)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
} else {
|
||||
policy = []byte(tt.policyJson)
|
||||
}
|
||||
|
||||
data := map[string][]byte{}
|
||||
|
||||
if !tt.provideNoPolicy {
|
||||
data["trustpolicy.json"] = policy
|
||||
}
|
||||
|
||||
data["notation.crt"] = certTuple.Cert.Raw
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "notation",
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
|
||||
g.Expect(r.Create(ctx, secret)).NotTo(HaveOccurred())
|
||||
|
||||
if tt.insecure {
|
||||
obj.Spec.Insecure = true
|
||||
} else {
|
||||
obj.Spec.CertSecretRef = &meta.LocalObjectReference{
|
||||
Name: "valid-trust-store",
|
||||
}
|
||||
}
|
||||
|
||||
obj.Spec.Verify.SecretRef = &meta.LocalObjectReference{Name: "notation"}
|
||||
|
||||
if tt.reference != nil {
|
||||
obj.Spec.Reference = tt.reference
|
||||
}
|
||||
|
||||
podinfoVersions, err := pushMultiplePodinfoImages(server.registryHost, tt.insecure, tt.reference.Tag)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
if tt.useDigest {
|
||||
obj.Spec.Reference.Digest = podinfoVersions[tt.reference.Tag].digest.String()
|
||||
}
|
||||
|
||||
keychain, err := r.keychain(ctx, obj)
|
||||
if err != nil {
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
opts := makeRemoteOptions(ctx, makeTransport(true), keychain, nil)
|
||||
|
||||
artifactRef, err := r.getArtifactRef(obj, opts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
remoteRepo, err := oras.NewRepository(artifactRef.String())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
if tt.insecure {
|
||||
remoteRepo.PlainHTTP = true
|
||||
}
|
||||
|
||||
repo := registry.NewRepository(remoteRepo)
|
||||
|
||||
signatureMediaType := cose.MediaTypeEnvelope
|
||||
|
||||
signOptions := notation.SignOptions{
|
||||
SignerSignOptions: notation.SignerSignOptions{
|
||||
SignatureMediaType: signatureMediaType,
|
||||
},
|
||||
ArtifactReference: artifactRef.String(),
|
||||
}
|
||||
|
||||
_, err = notation.Sign(ctx, signer, repo, signOptions)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
image := podinfoVersions[tt.reference.Tag]
|
||||
signatureDigest := ""
|
||||
|
||||
artifactDescriptor, err := repo.Resolve(ctx, image.tag)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
_ = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error {
|
||||
g.Expect(len(signatureManifests)).Should(Equal(1))
|
||||
signatureDigest = signatureManifests[0].Digest.String()
|
||||
return nil
|
||||
})
|
||||
|
||||
assertConditions := tt.assertConditions
|
||||
for k := range assertConditions {
|
||||
if tt.useDigest {
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<revision>", image.digest.String())
|
||||
} else {
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<revision>", fmt.Sprintf("%s@%s", tt.reference.Tag, image.digest.String()))
|
||||
}
|
||||
|
||||
if signatureDigest != "" {
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<sigrevision>", signatureDigest)
|
||||
}
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<url>", artifactRef.String())
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<provider>", "notation")
|
||||
}
|
||||
|
||||
if tt.beforeFunc != nil {
|
||||
tt.beforeFunc(obj, image.tag, image.digest.String())
|
||||
}
|
||||
|
||||
g.Expect(r.Client.Create(ctx, obj)).ToNot(HaveOccurred())
|
||||
defer func() {
|
||||
g.Expect(r.Client.Delete(ctx, obj)).ToNot(HaveOccurred())
|
||||
}()
|
||||
|
||||
sp := patch.NewSerialPatcher(obj, r.Client)
|
||||
|
||||
artifact := &sourcev1.Artifact{}
|
||||
got, err := r.reconcileSource(ctx, sp, obj, artifact, tmpDir)
|
||||
g.Expect(r.Delete(ctx, secret)).NotTo(HaveOccurred())
|
||||
if tt.wantErr {
|
||||
tt.wantErrMsg = strings.ReplaceAll(tt.wantErrMsg, "<url>", artifactRef.String())
|
||||
if signatureDigest != "" {
|
||||
tt.wantErrMsg = strings.ReplaceAll(tt.wantErrMsg, "<sigrevision>", signatureDigest)
|
||||
}
|
||||
g.Expect(err).ToNot(BeNil())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErrMsg))
|
||||
} else {
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCIRepository_reconcileSource_verifyOCISourceSignatureCosign(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tests := []struct {
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
|
||||
"github.com/fluxcd/source-controller/internal/fs"
|
||||
"github.com/fluxcd/source-controller/internal/oci"
|
||||
)
|
||||
|
||||
// Reference holds information to locate a chart.
|
||||
|
@ -146,6 +147,9 @@ type Build struct {
|
|||
// This can for example be false if ValuesFiles is empty and the chart
|
||||
// source was already packaged.
|
||||
Packaged bool
|
||||
// VerifiedResult indicates the results of verifying the chart.
|
||||
// If no verification was performed, this field should be VerificationResultIgnored.
|
||||
VerifiedResult oci.VerificationResult
|
||||
}
|
||||
|
||||
// Summary returns a human-readable summary of the Build.
|
||||
|
|
|
@ -35,6 +35,7 @@ import (
|
|||
"github.com/fluxcd/source-controller/internal/fs"
|
||||
"github.com/fluxcd/source-controller/internal/helm/chart/secureloader"
|
||||
"github.com/fluxcd/source-controller/internal/helm/repository"
|
||||
"github.com/fluxcd/source-controller/internal/oci"
|
||||
)
|
||||
|
||||
type remoteChartBuilder struct {
|
||||
|
@ -141,9 +142,11 @@ func (b *remoteChartBuilder) downloadFromRepository(ctx context.Context, remote
|
|||
return nil, nil, &BuildError{Reason: reason, Err: err}
|
||||
}
|
||||
|
||||
verifiedResult := oci.VerificationResultIgnored
|
||||
|
||||
// Verify the chart if necessary
|
||||
if opts.Verify {
|
||||
if err := remote.VerifyChart(ctx, cv); err != nil {
|
||||
if verifiedResult, err = remote.VerifyChart(ctx, cv); err != nil {
|
||||
return nil, nil, &BuildError{Reason: ErrChartVerification, Err: err}
|
||||
}
|
||||
}
|
||||
|
@ -153,6 +156,8 @@ func (b *remoteChartBuilder) downloadFromRepository(ctx context.Context, remote
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
result.VerifiedResult = verifiedResult
|
||||
|
||||
if shouldReturn {
|
||||
return nil, result, nil
|
||||
}
|
||||
|
@ -173,6 +178,7 @@ func generateBuildResult(cv *repo.ChartVersion, opts BuildOptions) (*Build, bool
|
|||
result := &Build{}
|
||||
result.Version = cv.Version
|
||||
result.Name = cv.Name
|
||||
result.VerifiedResult = oci.VerificationResultIgnored
|
||||
|
||||
// Set build specific metadata if instructed
|
||||
if opts.VersionMetadata != "" {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import "strings"
|
||||
|
||||
// StringResource is there to satisfy the github.com/google/go-containerregistry/pkg/authn.Resource interface.
|
||||
// It merely wraps a given string and returns it for all of the interface's methods.
|
||||
type StringResource struct {
|
||||
Registry string
|
||||
}
|
||||
|
||||
// String returns a string representation of the StringResource.
|
||||
// It converts the StringResource object to a string.
|
||||
// The returned string contains the value of the StringResource.
|
||||
func (r StringResource) String() string {
|
||||
return r.Registry
|
||||
}
|
||||
|
||||
// RegistryStr returns the string representation of the registry resource.
|
||||
// It converts the StringResource object to a string that represents the registry resource.
|
||||
// The returned string can be used to interact with the registry resource.
|
||||
func (r StringResource) RegistryStr() string {
|
||||
return strings.Split(r.Registry, "/")[0]
|
||||
}
|
|
@ -54,6 +54,7 @@ type ClientOpts struct {
|
|||
RegLoginOpts []helmreg.LoginOption
|
||||
TlsConfig *tls.Config
|
||||
GetterOpts []helmgetter.Option
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
// MustLoginToRegistry returns true if the client options contain at least
|
||||
|
@ -172,6 +173,8 @@ func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmReposit
|
|||
err = ErrDeprecatedTLSConfig
|
||||
}
|
||||
|
||||
hrOpts.Insecure = obj.Spec.Insecure
|
||||
|
||||
return hrOpts, dir, err
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ func TestGetClientOpts(t *testing.T) {
|
|||
authSecret *corev1.Secret
|
||||
afterFunc func(t *WithT, hcOpts *ClientOpts)
|
||||
oci bool
|
||||
insecure bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
|
@ -109,9 +110,27 @@ func TestGetClientOpts(t *testing.T) {
|
|||
t.Expect(err).ToNot(HaveOccurred())
|
||||
t.Expect(config.Username).To(Equal("user"))
|
||||
t.Expect(config.Password).To(Equal("pass"))
|
||||
t.Expect(hcOpts.Insecure).To(BeFalse())
|
||||
},
|
||||
oci: true,
|
||||
},
|
||||
{
|
||||
name: "OCI HelmRepository with insecure repository",
|
||||
authSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "auth-oci",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"username": []byte("user"),
|
||||
"password": []byte("pass"),
|
||||
},
|
||||
},
|
||||
afterFunc: func(t *WithT, hcOpts *ClientOpts) {
|
||||
t.Expect(hcOpts.Insecure).To(BeTrue())
|
||||
},
|
||||
oci: true,
|
||||
insecure: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -123,6 +142,7 @@ func TestGetClientOpts(t *testing.T) {
|
|||
Timeout: &metav1.Duration{
|
||||
Duration: time.Second,
|
||||
},
|
||||
Insecure: tt.insecure,
|
||||
},
|
||||
}
|
||||
if tt.oci {
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
"github.com/fluxcd/source-controller/internal/helm/common"
|
||||
"github.com/fluxcd/source-controller/internal/oci"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"helm.sh/helm/v3/pkg/registry"
|
||||
|
@ -95,7 +96,7 @@ func KeychainAdaptHelper(keyChain authn.Keychain) func(string) (registry.LoginOp
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse registry URL '%s'", registryURL)
|
||||
}
|
||||
authenticator, err := keyChain.Resolve(stringResource{parsedURL.Host})
|
||||
authenticator, err := keyChain.Resolve(common.StringResource{Registry: parsedURL.Host})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve credentials for registry '%s': %w", registryURL, err)
|
||||
}
|
||||
|
@ -126,20 +127,6 @@ func AuthAdaptHelper(auth authn.Authenticator) (registry.LoginOption, error) {
|
|||
return registry.LoginOptBasicAuth(username, password), nil
|
||||
}
|
||||
|
||||
// stringResource is there to satisfy the github.com/google/go-containerregistry/pkg/authn.Resource interface.
|
||||
// It merely wraps a given string and returns it for all of the interface's methods.
|
||||
type stringResource struct {
|
||||
registry string
|
||||
}
|
||||
|
||||
func (r stringResource) String() string {
|
||||
return r.registry
|
||||
}
|
||||
|
||||
func (r stringResource) RegistryStr() string {
|
||||
return r.registry
|
||||
}
|
||||
|
||||
// NewLoginOption returns a registry login option for the given HelmRepository.
|
||||
// If the HelmRepository does not specify a secretRef, a nil login option is returned.
|
||||
func NewLoginOption(auth authn.Authenticator, keychain authn.Keychain, registryURL string) (registry.LoginOption, error) {
|
||||
|
|
|
@ -40,6 +40,7 @@ import (
|
|||
"github.com/fluxcd/pkg/version"
|
||||
|
||||
"github.com/fluxcd/source-controller/internal/helm"
|
||||
"github.com/fluxcd/source-controller/internal/oci"
|
||||
"github.com/fluxcd/source-controller/internal/transport"
|
||||
)
|
||||
|
||||
|
@ -465,9 +466,9 @@ func (r *ChartRepository) invalidate() {
|
|||
|
||||
// VerifyChart verifies the chart against a signature.
|
||||
// It returns an error on failure.
|
||||
func (r *ChartRepository) VerifyChart(_ context.Context, _ *repo.ChartVersion) error {
|
||||
func (r *ChartRepository) VerifyChart(_ context.Context, _ *repo.ChartVersion) (oci.VerificationResult, error) {
|
||||
// this is a no-op because this is not implemented yet.
|
||||
return fmt.Errorf("not implemented")
|
||||
return oci.VerificationResultIgnored, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// jsonOrYamlUnmarshal unmarshals the given byte slice containing JSON or YAML
|
||||
|
|
|
@ -357,15 +357,16 @@ func getLastMatchingVersionOrConstraint(cvs []string, ver string) (string, error
|
|||
}
|
||||
|
||||
// VerifyChart verifies the chart against a signature.
|
||||
// If no signature is provided, a keyless verification is performed.
|
||||
// It returns an error on failure.
|
||||
func (r *OCIChartRepository) VerifyChart(ctx context.Context, chart *repo.ChartVersion) error {
|
||||
// Supports signature verification using either cosign or notation providers.
|
||||
// If no signature is provided, when cosign is used, a keyless verification is performed.
|
||||
// The verification result is returned as a VerificationResult and any error encountered.
|
||||
func (r *OCIChartRepository) VerifyChart(ctx context.Context, chart *repo.ChartVersion) (oci.VerificationResult, error) {
|
||||
if len(r.verifiers) == 0 {
|
||||
return fmt.Errorf("no verifiers available")
|
||||
return oci.VerificationResultFailed, fmt.Errorf("no verifiers available")
|
||||
}
|
||||
|
||||
if len(chart.URLs) == 0 {
|
||||
return fmt.Errorf("chart '%s' has no downloadable URLs", chart.Name)
|
||||
return oci.VerificationResultFailed, fmt.Errorf("chart '%s' has no downloadable URLs", chart.Name)
|
||||
}
|
||||
|
||||
var nameOpts []name.Option
|
||||
|
@ -375,17 +376,26 @@ func (r *OCIChartRepository) VerifyChart(ctx context.Context, chart *repo.ChartV
|
|||
|
||||
ref, err := name.ParseReference(strings.TrimPrefix(chart.URLs[0], fmt.Sprintf("%s://", registry.OCIScheme)), nameOpts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid chart reference: %s", err)
|
||||
return oci.VerificationResultFailed, fmt.Errorf("invalid chart reference: %s", err)
|
||||
}
|
||||
|
||||
verificationResult := oci.VerificationResultFailed
|
||||
|
||||
// verify the chart
|
||||
for _, verifier := range r.verifiers {
|
||||
if verified, err := verifier.Verify(ctx, ref); err != nil {
|
||||
return fmt.Errorf("failed to verify %s: %w", chart.URLs[0], err)
|
||||
} else if verified {
|
||||
return nil
|
||||
result, err := verifier.Verify(ctx, ref)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to verify %s: %w", chart.URLs[0], err)
|
||||
}
|
||||
if result == oci.VerificationResultSuccess {
|
||||
return result, nil
|
||||
}
|
||||
verificationResult = result
|
||||
}
|
||||
|
||||
return fmt.Errorf("no matching signatures were found for '%s'", ref.Name())
|
||||
if verificationResult == oci.VerificationResultIgnored {
|
||||
return verificationResult, nil
|
||||
}
|
||||
|
||||
return oci.VerificationResultFailed, fmt.Errorf("no matching signatures were found for '%s'", ref.Name())
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ import (
|
|||
"context"
|
||||
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
|
||||
"github.com/fluxcd/source-controller/internal/oci"
|
||||
)
|
||||
|
||||
// Downloader is used to download a chart from a remote Helm repository or OCI Helm repository.
|
||||
|
@ -31,7 +33,7 @@ type Downloader interface {
|
|||
// DownloadChart downloads a chart from the remote Helm repository or OCI Helm repository.
|
||||
DownloadChart(chart *repo.ChartVersion) (*bytes.Buffer, error)
|
||||
// VerifyChart verifies the chart against a signature.
|
||||
VerifyChart(ctx context.Context, chart *repo.ChartVersion) error
|
||||
VerifyChart(ctx context.Context, chart *repo.ChartVersion) (oci.VerificationResult, error)
|
||||
// Clear removes all temporary files created by the downloader, caching the files if the cache is configured,
|
||||
// and calling garbage collector to remove unused files.
|
||||
Clear() error
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cosign
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio"
|
||||
coptions "github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor"
|
||||
"github.com/sigstore/cosign/v2/pkg/cosign"
|
||||
"github.com/sigstore/cosign/v2/pkg/oci"
|
||||
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
|
||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
|
||||
soci "github.com/fluxcd/source-controller/internal/oci"
|
||||
)
|
||||
|
||||
// options is a struct that holds options for verifier.
|
||||
type options struct {
|
||||
publicKey []byte
|
||||
rOpt []remote.Option
|
||||
identities []cosign.Identity
|
||||
}
|
||||
|
||||
// Options is a function that configures the options applied to a Verifier.
|
||||
type Options func(opts *options)
|
||||
|
||||
// WithPublicKey sets the public key.
|
||||
func WithPublicKey(publicKey []byte) Options {
|
||||
return func(opts *options) {
|
||||
opts.publicKey = publicKey
|
||||
}
|
||||
}
|
||||
|
||||
// WithRemoteOptions is a functional option for overriding the default
|
||||
// remote options used by the verifier.
|
||||
func WithRemoteOptions(opts ...remote.Option) Options {
|
||||
return func(o *options) {
|
||||
o.rOpt = opts
|
||||
}
|
||||
}
|
||||
|
||||
// WithIdentities specifies the identity matchers that have to be met
|
||||
// for the signature to be deemed valid.
|
||||
func WithIdentities(identities []cosign.Identity) Options {
|
||||
return func(opts *options) {
|
||||
opts.identities = identities
|
||||
}
|
||||
}
|
||||
|
||||
// CosignVerifier is a struct which is responsible for executing verification logic.
|
||||
type CosignVerifier struct {
|
||||
opts *cosign.CheckOpts
|
||||
}
|
||||
|
||||
// NewCosignVerifier initializes a new CosignVerifier.
|
||||
func NewCosignVerifier(ctx context.Context, opts ...Options) (*CosignVerifier, error) {
|
||||
o := options{}
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
checkOpts := &cosign.CheckOpts{}
|
||||
|
||||
ro := coptions.RegistryOptions{}
|
||||
co, err := ro.ClientOpts(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
checkOpts.Identities = o.identities
|
||||
if o.rOpt != nil {
|
||||
co = append(co, ociremote.WithRemoteOptions(o.rOpt...))
|
||||
}
|
||||
|
||||
checkOpts.RegistryClientOpts = co
|
||||
|
||||
// If a public key is provided, it will use it to verify the signature.
|
||||
// If there is no public key provided, it will try keyless verification.
|
||||
// https://github.com/sigstore/cosign/blob/main/KEYLESS.md.
|
||||
if len(o.publicKey) > 0 {
|
||||
checkOpts.Offline = true
|
||||
// TODO(hidde): this is an oversight in our implementation. As it is
|
||||
// theoretically possible to have a custom PK, without disabling tlog.
|
||||
checkOpts.IgnoreTlog = true
|
||||
|
||||
pubKeyRaw, err := cryptoutils.UnmarshalPEMToPublicKey(o.publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
checkOpts.SigVerifier, err = signature.LoadVerifier(pubKeyRaw, crypto.SHA256)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
checkOpts.RekorClient, err = rekor.NewClient(coptions.DefaultRekorURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create Rekor client: %w", err)
|
||||
}
|
||||
|
||||
// This performs an online fetch of the Rekor public keys, but this is needed
|
||||
// for verifying tlog entries (both online and offline).
|
||||
// TODO(hidde): above note is important to keep in mind when we implement
|
||||
// "offline" tlog above.
|
||||
if checkOpts.RekorPubKeys, err = cosign.GetRekorPubs(ctx); err != nil {
|
||||
return nil, fmt.Errorf("unable to get Rekor public keys: %w", err)
|
||||
}
|
||||
|
||||
checkOpts.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get CTLog public keys: %w", err)
|
||||
}
|
||||
|
||||
if checkOpts.RootCerts, err = fulcio.GetRoots(); err != nil {
|
||||
return nil, fmt.Errorf("unable to get Fulcio root certs: %w", err)
|
||||
}
|
||||
|
||||
if checkOpts.IntermediateCerts, err = fulcio.GetIntermediates(); err != nil {
|
||||
return nil, fmt.Errorf("unable to get Fulcio intermediate certs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &CosignVerifier{
|
||||
opts: checkOpts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyImageSignatures verify the authenticity of the given ref OCI image.
|
||||
func (v *CosignVerifier) VerifyImageSignatures(ctx context.Context, ref name.Reference) ([]oci.Signature, bool, error) {
|
||||
return cosign.VerifyImageSignatures(ctx, ref, v.opts)
|
||||
}
|
||||
|
||||
// Verify verifies the authenticity of the given ref OCI image.
|
||||
// It returns a boolean indicating if the verification was successful.
|
||||
// It returns an error if the verification fails, nil otherwise.
|
||||
func (v *CosignVerifier) Verify(ctx context.Context, ref name.Reference) (soci.VerificationResult, error) {
|
||||
signatures, _, err := v.VerifyImageSignatures(ctx, ref)
|
||||
if err != nil {
|
||||
return soci.VerificationResultFailed, err
|
||||
}
|
||||
|
||||
if len(signatures) == 0 {
|
||||
return soci.VerificationResultFailed, nil
|
||||
}
|
||||
|
||||
return soci.VerificationResultSuccess, nil
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
package cosign
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
@ -38,15 +38,15 @@ func TestOptions(t *testing.T) {
|
|||
name: "signature option",
|
||||
opts: []Options{WithPublicKey([]byte("foo"))},
|
||||
want: &options{
|
||||
PublicKey: []byte("foo"),
|
||||
ROpt: nil,
|
||||
publicKey: []byte("foo"),
|
||||
rOpt: nil,
|
||||
},
|
||||
}, {
|
||||
name: "keychain option",
|
||||
opts: []Options{WithRemoteOptions(remote.WithAuthFromKeychain(authn.DefaultKeychain))},
|
||||
want: &options{
|
||||
PublicKey: nil,
|
||||
ROpt: []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain)},
|
||||
publicKey: nil,
|
||||
rOpt: []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain)},
|
||||
},
|
||||
}, {
|
||||
name: "keychain and authenticator option",
|
||||
|
@ -55,8 +55,8 @@ func TestOptions(t *testing.T) {
|
|||
remote.WithAuthFromKeychain(authn.DefaultKeychain),
|
||||
)},
|
||||
want: &options{
|
||||
PublicKey: nil,
|
||||
ROpt: []remote.Option{
|
||||
publicKey: nil,
|
||||
rOpt: []remote.Option{
|
||||
remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
|
||||
remote.WithAuthFromKeychain(authn.DefaultKeychain),
|
||||
},
|
||||
|
@ -69,8 +69,8 @@ func TestOptions(t *testing.T) {
|
|||
remote.WithTransport(http.DefaultTransport),
|
||||
)},
|
||||
want: &options{
|
||||
PublicKey: nil,
|
||||
ROpt: []remote.Option{
|
||||
publicKey: nil,
|
||||
rOpt: []remote.Option{
|
||||
remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
|
||||
remote.WithAuthFromKeychain(authn.DefaultKeychain),
|
||||
remote.WithTransport(http.DefaultTransport),
|
||||
|
@ -89,7 +89,7 @@ func TestOptions(t *testing.T) {
|
|||
},
|
||||
})},
|
||||
want: &options{
|
||||
Identities: []cosign.Identity{
|
||||
identities: []cosign.Identity{
|
||||
{
|
||||
SubjectRegExp: "test-user",
|
||||
IssuerRegExp: "^https://token.actions.githubusercontent.com$",
|
||||
|
@ -109,20 +109,20 @@ func TestOptions(t *testing.T) {
|
|||
for _, opt := range test.opts {
|
||||
opt(&o)
|
||||
}
|
||||
if !reflect.DeepEqual(o.PublicKey, test.want.PublicKey) {
|
||||
t.Errorf("got %#v, want %#v", &o.PublicKey, test.want.PublicKey)
|
||||
if !reflect.DeepEqual(o.publicKey, test.want.publicKey) {
|
||||
t.Errorf("got %#v, want %#v", &o.publicKey, test.want.publicKey)
|
||||
}
|
||||
|
||||
if test.want.ROpt != nil {
|
||||
if len(o.ROpt) != len(test.want.ROpt) {
|
||||
t.Errorf("got %d remote options, want %d", len(o.ROpt), len(test.want.ROpt))
|
||||
if test.want.rOpt != nil {
|
||||
if len(o.rOpt) != len(test.want.rOpt) {
|
||||
t.Errorf("got %d remote options, want %d", len(o.rOpt), len(test.want.rOpt))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if test.want.ROpt == nil {
|
||||
if len(o.ROpt) != 0 {
|
||||
t.Errorf("got %d remote options, want %d", len(o.ROpt), 0)
|
||||
if test.want.rOpt == nil {
|
||||
if len(o.rOpt) != 0 {
|
||||
t.Errorf("got %d remote options, want %d", len(o.rOpt), 0)
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,388 @@
|
|||
/*
|
||||
Copyright 2023 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package notation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
_ "github.com/notaryproject/notation-core-go/signature/cose"
|
||||
_ "github.com/notaryproject/notation-core-go/signature/jws"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/registry"
|
||||
verifier "github.com/notaryproject/notation-go/verifier"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/notaryproject/notation-go/verifier/truststore"
|
||||
oras "oras.land/oras-go/v2/registry/remote"
|
||||
oauth "oras.land/oras-go/v2/registry/remote/auth"
|
||||
retryhttp "oras.land/oras-go/v2/registry/remote/retry"
|
||||
|
||||
"github.com/fluxcd/source-controller/internal/helm/common"
|
||||
"github.com/fluxcd/source-controller/internal/oci"
|
||||
)
|
||||
|
||||
// name of the trustpolicy file defined in the Secret containing
|
||||
// notation public keys.
|
||||
const DefaultTrustPolicyKey = "trustpolicy.json"
|
||||
|
||||
// options is a struct that holds options for verifier.
|
||||
type options struct {
|
||||
rootCertificates [][]byte
|
||||
rOpt []remote.Option
|
||||
trustPolicy *trustpolicy.Document
|
||||
auth authn.Authenticator
|
||||
keychain authn.Keychain
|
||||
insecure bool
|
||||
logger logr.Logger
|
||||
}
|
||||
|
||||
// Options is a function that configures the options applied to a Verifier.
|
||||
type Options func(opts *options)
|
||||
|
||||
// WithInsecureRegistry sets notation to verify against insecure registry.
|
||||
func WithInsecureRegistry(insecure bool) Options {
|
||||
return func(opts *options) {
|
||||
opts.insecure = insecure
|
||||
}
|
||||
}
|
||||
|
||||
// WithTrustPolicy sets the trust policy configuration.
|
||||
func WithTrustPolicy(trustPolicy *trustpolicy.Document) Options {
|
||||
return func(opts *options) {
|
||||
opts.trustPolicy = trustPolicy
|
||||
}
|
||||
}
|
||||
|
||||
// WithRootCertificates is a functional option for overriding the default
|
||||
// rootCertificate options used by the verifier to set the root CA certificate for notary.
|
||||
// It takes in a list of certificate data as an array of byte slices.
|
||||
// The function returns a options function option that sets the public certificate
|
||||
// in the notation options.
|
||||
func WithRootCertificates(data [][]byte) Options {
|
||||
return func(opts *options) {
|
||||
opts.rootCertificates = data
|
||||
}
|
||||
}
|
||||
|
||||
// WithRemoteOptions is a functional option for overriding the default
|
||||
// remote options used by the verifier
|
||||
func WithRemoteOptions(opts ...remote.Option) Options {
|
||||
return func(o *options) {
|
||||
o.rOpt = opts
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuth is a functional option for overriding the default
|
||||
// authenticator options used by the verifier
|
||||
func WithAuth(auth authn.Authenticator) Options {
|
||||
return func(o *options) {
|
||||
o.auth = auth
|
||||
}
|
||||
}
|
||||
|
||||
// WithKeychain is a functional option for overriding the default
|
||||
// keychain options used by the verifier
|
||||
func WithKeychain(key authn.Keychain) Options {
|
||||
return func(o *options) {
|
||||
o.keychain = key
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger is a function that returns an Options function to set the logger for the options.
|
||||
// The logger is used for logging purposes within the options.
|
||||
func WithLogger(logger logr.Logger) Options {
|
||||
return func(o *options) {
|
||||
o.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
// NotationVerifier is a struct which is responsible for executing verification logic
|
||||
type NotationVerifier struct {
|
||||
auth authn.Authenticator
|
||||
keychain authn.Keychain
|
||||
verifier *notation.Verifier
|
||||
opts []remote.Option
|
||||
insecure bool
|
||||
logger logr.Logger
|
||||
}
|
||||
|
||||
var _ truststore.X509TrustStore = &trustStore{}
|
||||
|
||||
// trustStore is used by notation-go/verifier to retrieve the root certificate for notary.
|
||||
// The default behaviour is to read the certificate from disk and return it as a byte slice.
|
||||
// The reason for implementing the interface here is to avoid reading the certificate from disk
|
||||
// as the certificate is already available in memory.
|
||||
type trustStore struct {
|
||||
certs [][]byte
|
||||
}
|
||||
|
||||
// GetCertificates implements truststore.X509TrustStore.
|
||||
func (s trustStore) GetCertificates(ctx context.Context, storeType truststore.Type, namedStore string) ([]*x509.Certificate, error) {
|
||||
certs := []*x509.Certificate{}
|
||||
for _, data := range s.certs {
|
||||
raw := data
|
||||
block, _ := pem.Decode(raw)
|
||||
if block != nil {
|
||||
raw = block.Bytes
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificates(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse certificate '%s': %s", namedStore, err)
|
||||
}
|
||||
|
||||
certs = append(certs, cert...)
|
||||
}
|
||||
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
// NewNotationVerifier initializes a new Verifier
|
||||
func NewNotationVerifier(opts ...Options) (*NotationVerifier, error) {
|
||||
o := options{}
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
store := &trustStore{
|
||||
certs: o.rootCertificates,
|
||||
}
|
||||
|
||||
trustpolicy := o.trustPolicy
|
||||
if trustpolicy == nil {
|
||||
return nil, fmt.Errorf("trust policy cannot be empty")
|
||||
}
|
||||
|
||||
verifier, err := verifier.New(trustpolicy, store, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &NotationVerifier{
|
||||
auth: o.auth,
|
||||
keychain: o.keychain,
|
||||
verifier: &verifier,
|
||||
opts: o.rOpt,
|
||||
insecure: o.insecure,
|
||||
logger: o.logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CleanTrustPolicy cleans the given trust policy by removing trust stores and trusted identities
|
||||
// for trust policy statements that are set to skip signature verification but still have configured trust stores and/or trusted identities.
|
||||
// It takes a pointer to a trustpolicy.Document and a logger from the logr package as input parameters.
|
||||
// If the trustPolicy is nil, it returns nil.
|
||||
// Otherwise, it iterates over the trustPolicy.TrustPolicies and checks if each trust policy statement's
|
||||
// SignatureVerification.VerificationLevel is set to trustpolicy.LevelSkip.Name.
|
||||
// If it is, it logs a warning message and removes the trust stores and trusted identities for that trust policy statement.
|
||||
// Finally, it returns the modified trustPolicy.
|
||||
func CleanTrustPolicy(trustPolicy *trustpolicy.Document, logger logr.Logger) *trustpolicy.Document {
|
||||
if trustPolicy == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, j := range trustPolicy.TrustPolicies {
|
||||
if j.SignatureVerification.VerificationLevel == trustpolicy.LevelSkip.Name {
|
||||
if len(j.TrustStores) > 0 || len(j.TrustedIdentities) > 0 {
|
||||
logger.Info(fmt.Sprintf("warning: trust policy statement '%s' is set to skip signature verification but configured with trust stores and/or trusted identities. Ignoring trust stores and trusted identities", j.Name))
|
||||
}
|
||||
trustPolicy.TrustPolicies[i].TrustStores = []string{}
|
||||
trustPolicy.TrustPolicies[i].TrustedIdentities = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
return trustPolicy
|
||||
}
|
||||
|
||||
// Verify verifies the authenticity of the given ref OCI image.
|
||||
// It returns a boolean indicating if the verification was successful.
|
||||
// It returns an error if the verification fails, nil otherwise.
|
||||
func (v *NotationVerifier) Verify(ctx context.Context, ref name.Reference) (oci.VerificationResult, error) {
|
||||
url := ref.Name()
|
||||
|
||||
remoteRepo, err := v.remoteRepo(url)
|
||||
if err != nil {
|
||||
return oci.VerificationResultFailed, err
|
||||
}
|
||||
|
||||
repo := registry.NewRepository(remoteRepo)
|
||||
|
||||
repoUrl, err := v.repoUrlWithDigest(url, ref)
|
||||
if err != nil {
|
||||
return oci.VerificationResultFailed, err
|
||||
}
|
||||
|
||||
verifyOptions := notation.VerifyOptions{
|
||||
ArtifactReference: repoUrl,
|
||||
MaxSignatureAttempts: 3,
|
||||
}
|
||||
|
||||
_, outcomes, err := notation.Verify(ctx, *v.verifier, repo, verifyOptions)
|
||||
if err != nil {
|
||||
return oci.VerificationResultFailed, err
|
||||
}
|
||||
|
||||
return v.checkOutcome(outcomes, url)
|
||||
}
|
||||
|
||||
// checkOutcome checks the verification outcomes for a given URL and returns the corresponding OCI verification result.
|
||||
// It takes a slice of verification outcomes and a URL as input parameters.
|
||||
// If there are no verification outcomes, it returns a failed verification result with an error message.
|
||||
// If the first verification outcome has a verification level of "trustpolicy.LevelSkip", it returns an ignored verification result.
|
||||
// This function assumes that "trustpolicy.TypeIntegrity" is always enforced. It will return a successful validation result if "trustpolicy.TypeAuthenticity" is successful too.
|
||||
// If any of the verification results have an error, it logs the error message and sets the "ignore" flag to true if the error type is "trustpolicy.TypeAuthenticity".
|
||||
// If the "ignore" flag is true, it returns an ignored verification result.
|
||||
// Otherwise, it returns a successful verification result.
|
||||
// The function returns the OCI verification result and an error, if any.
|
||||
func (v *NotationVerifier) checkOutcome(outcomes []*notation.VerificationOutcome, url string) (oci.VerificationResult, error) {
|
||||
if len(outcomes) == 0 {
|
||||
return oci.VerificationResultFailed, fmt.Errorf("signature verification failed for all the signatures associated with %s", url)
|
||||
}
|
||||
|
||||
// should only ever be one item in the outcomes slice
|
||||
outcome := outcomes[0]
|
||||
|
||||
// if the verification level is set to skip, we ignore the verification result
|
||||
// as there should be no verification results in outcome and we do not want
|
||||
// to mark the result as verified
|
||||
if outcome.VerificationLevel == trustpolicy.LevelSkip {
|
||||
return oci.VerificationResultIgnored, nil
|
||||
}
|
||||
|
||||
ignore := false
|
||||
|
||||
// loop through verification results to check for errors
|
||||
for _, i := range outcome.VerificationResults {
|
||||
// error if action is not marked as `skip` and there is an error
|
||||
if i.Error != nil {
|
||||
// flag to ignore the verification result if the error is related to type `authenticity`
|
||||
if i.Type == trustpolicy.TypeAuthenticity {
|
||||
ignore = true
|
||||
}
|
||||
// log results of error
|
||||
v.logger.Info(fmt.Sprintf("verification check for type '%s' failed for '%s' with message: '%s'", i.Type, url, i.Error.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// if the ignore flag is set, we ignore the verification result so not to mark as verified
|
||||
if ignore {
|
||||
return oci.VerificationResultIgnored, nil
|
||||
}
|
||||
|
||||
// result is okay to mark as verified
|
||||
return oci.VerificationResultSuccess, nil
|
||||
}
|
||||
|
||||
// remoteRepo is a function that creates a remote repository object for the given repository URL.
|
||||
// It initializes the repository with the provided URL and sets the PlainHTTP flag based on the value of the 'insecure' field in the Verifier struct.
|
||||
// It also sets up the credential provider based on the authentication configuration provided in the Verifier struct.
|
||||
// If authentication is required, it retrieves the authentication credentials and sets up the repository client with the appropriate headers and credentials.
|
||||
// Finally, it returns the remote repository object and any error encountered during the process.
|
||||
func (v *NotationVerifier) remoteRepo(repoUrl string) (*oras.Repository, error) {
|
||||
remoteRepo, err := oras.NewRepository(repoUrl)
|
||||
if err != nil {
|
||||
return &oras.Repository{}, err
|
||||
}
|
||||
|
||||
remoteRepo.PlainHTTP = v.insecure
|
||||
|
||||
credentialProvider := func(ctx context.Context, registry string) (oauth.Credential, error) {
|
||||
return oauth.EmptyCredential, nil
|
||||
}
|
||||
|
||||
auth := authn.Anonymous
|
||||
|
||||
if v.auth != nil {
|
||||
auth = v.auth
|
||||
} else if v.keychain != nil {
|
||||
source := common.StringResource{Registry: repoUrl}
|
||||
|
||||
auth, err = v.keychain.Resolve(source)
|
||||
if err != nil {
|
||||
return &oras.Repository{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if auth != authn.Anonymous {
|
||||
authConfig, err := auth.Authorization()
|
||||
if err != nil {
|
||||
return &oras.Repository{}, err
|
||||
}
|
||||
|
||||
credentialProvider = func(ctx context.Context, registry string) (oauth.Credential, error) {
|
||||
if authConfig.Username != "" || authConfig.Password != "" || authConfig.IdentityToken != "" || authConfig.RegistryToken != "" {
|
||||
return oauth.Credential{
|
||||
Username: authConfig.Username,
|
||||
Password: authConfig.Password,
|
||||
RefreshToken: authConfig.IdentityToken,
|
||||
AccessToken: authConfig.RegistryToken,
|
||||
}, nil
|
||||
}
|
||||
return oauth.EmptyCredential, nil
|
||||
}
|
||||
}
|
||||
|
||||
repoClient := &oauth.Client{
|
||||
Client: retryhttp.DefaultClient,
|
||||
Header: http.Header{
|
||||
"User-Agent": {"flux"},
|
||||
},
|
||||
Credential: credentialProvider,
|
||||
}
|
||||
|
||||
remoteRepo.Client = repoClient
|
||||
|
||||
return remoteRepo, nil
|
||||
}
|
||||
|
||||
// repoUrlWithDigest takes a repository URL and a reference and returns the repository URL with the digest appended to it.
|
||||
// If the repository URL does not contain a tag or digest, it returns an error.
|
||||
func (v *NotationVerifier) repoUrlWithDigest(repoUrl string, ref name.Reference) (string, error) {
|
||||
if !strings.Contains(repoUrl, "@") {
|
||||
image, err := remote.Image(ref, v.opts...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
digest, err := image.Digest()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lastIndex := strings.LastIndex(repoUrl, ":")
|
||||
if lastIndex == -1 {
|
||||
return "", fmt.Errorf("url %s does not contain tag or digest", repoUrl)
|
||||
}
|
||||
|
||||
firstPart := repoUrl[:lastIndex]
|
||||
|
||||
if s := strings.Split(repoUrl, ":"); len(s) >= 2 {
|
||||
repoUrl = fmt.Sprintf("%s@%s", firstPart, digest)
|
||||
} else {
|
||||
return "", fmt.Errorf("url %s does not contain tag or digest", repoUrl)
|
||||
}
|
||||
}
|
||||
return repoUrl, nil
|
||||
}
|
|
@ -0,0 +1,591 @@
|
|||
/*
|
||||
Copyright 2023 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package notation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/fluxcd/source-controller/internal/oci"
|
||||
)
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts []Options
|
||||
want *options
|
||||
}{
|
||||
{
|
||||
name: "no options",
|
||||
want: &options{},
|
||||
},
|
||||
{
|
||||
name: "signature option",
|
||||
opts: []Options{WithRootCertificates([][]byte{[]byte("foo")})},
|
||||
want: &options{
|
||||
rootCertificates: [][]byte{[]byte("foo")},
|
||||
rOpt: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keychain option",
|
||||
opts: []Options{
|
||||
WithRemoteOptions(remote.WithAuthFromKeychain(authn.DefaultKeychain)),
|
||||
WithKeychain(authn.DefaultKeychain),
|
||||
},
|
||||
want: &options{
|
||||
rootCertificates: nil,
|
||||
rOpt: []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain)},
|
||||
keychain: authn.DefaultKeychain,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keychain and authenticator option",
|
||||
opts: []Options{
|
||||
WithRemoteOptions(
|
||||
remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
|
||||
remote.WithAuthFromKeychain(authn.DefaultKeychain),
|
||||
),
|
||||
WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
|
||||
WithKeychain(authn.DefaultKeychain),
|
||||
},
|
||||
want: &options{
|
||||
rootCertificates: nil,
|
||||
rOpt: []remote.Option{
|
||||
remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
|
||||
remote.WithAuthFromKeychain(authn.DefaultKeychain),
|
||||
},
|
||||
auth: &authn.Basic{Username: "foo", Password: "bar"},
|
||||
keychain: authn.DefaultKeychain,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keychain, authenticator and transport option",
|
||||
opts: []Options{
|
||||
WithRemoteOptions(
|
||||
remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
|
||||
remote.WithAuthFromKeychain(authn.DefaultKeychain),
|
||||
remote.WithTransport(http.DefaultTransport),
|
||||
),
|
||||
WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
|
||||
WithKeychain(authn.DefaultKeychain),
|
||||
},
|
||||
want: &options{
|
||||
rootCertificates: nil,
|
||||
rOpt: []remote.Option{
|
||||
remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
|
||||
remote.WithAuthFromKeychain(authn.DefaultKeychain),
|
||||
remote.WithTransport(http.DefaultTransport),
|
||||
},
|
||||
auth: &authn.Basic{Username: "foo", Password: "bar"},
|
||||
keychain: authn.DefaultKeychain,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "truststore, empty document",
|
||||
opts: []Options{WithTrustPolicy(&trustpolicy.Document{})},
|
||||
want: &options{
|
||||
rootCertificates: nil,
|
||||
rOpt: nil,
|
||||
trustPolicy: &trustpolicy.Document{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "truststore, dummy document",
|
||||
opts: []Options{WithTrustPolicy(dummyPolicyDocument())},
|
||||
want: &options{
|
||||
rootCertificates: nil,
|
||||
rOpt: nil,
|
||||
trustPolicy: dummyPolicyDocument(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "insecure, false",
|
||||
opts: []Options{WithInsecureRegistry(false)},
|
||||
want: &options{
|
||||
rootCertificates: nil,
|
||||
rOpt: nil,
|
||||
trustPolicy: nil,
|
||||
insecure: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "insecure, true",
|
||||
opts: []Options{WithInsecureRegistry(true)},
|
||||
want: &options{
|
||||
rootCertificates: nil,
|
||||
rOpt: nil,
|
||||
trustPolicy: nil,
|
||||
insecure: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "insecure, default",
|
||||
opts: []Options{},
|
||||
want: &options{
|
||||
rootCertificates: nil,
|
||||
rOpt: nil,
|
||||
trustPolicy: nil,
|
||||
insecure: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Run the test cases
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
o := options{}
|
||||
for _, opt := range tc.opts {
|
||||
opt(&o)
|
||||
}
|
||||
if !reflect.DeepEqual(o.rootCertificates, tc.want.rootCertificates) {
|
||||
t.Errorf("got %#v, want %#v", &o.rootCertificates, tc.want.rootCertificates)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(o.trustPolicy, tc.want.trustPolicy) {
|
||||
t.Errorf("got %#v, want %#v", &o.trustPolicy, tc.want.trustPolicy)
|
||||
}
|
||||
|
||||
if tc.want.rOpt != nil {
|
||||
if len(o.rOpt) != len(tc.want.rOpt) {
|
||||
t.Errorf("got %d remote options, want %d", len(o.rOpt), len(tc.want.rOpt))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if tc.want.rOpt == nil {
|
||||
if len(o.rOpt) != 0 {
|
||||
t.Errorf("got %d remote options, want %d", len(o.rOpt), 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanTrustPolicy(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
policy []trustpolicy.TrustPolicy
|
||||
want *trustpolicy.Document
|
||||
wantLogMessage string
|
||||
}{
|
||||
{
|
||||
name: "no trust policy",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "trust policy verification level set to strict and should not be cleaned",
|
||||
policy: []trustpolicy.TrustPolicy{{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"test"},
|
||||
TrustedIdentities: nil,
|
||||
}},
|
||||
want: &trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"test"},
|
||||
TrustedIdentities: nil,
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trust policy with multiple policies and should not be cleaned",
|
||||
policy: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"test"},
|
||||
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
|
||||
},
|
||||
{
|
||||
Name: "test-statement-name-2",
|
||||
RegistryScopes: []string{"example.com/podInfo"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"test"},
|
||||
TrustedIdentities: nil,
|
||||
},
|
||||
},
|
||||
want: &trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"test"},
|
||||
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
|
||||
},
|
||||
{
|
||||
Name: "test-statement-name-2",
|
||||
RegistryScopes: []string{"example.com/podInfo"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"test"},
|
||||
TrustedIdentities: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trust policy verification level skip should be cleaned",
|
||||
policy: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "skip"},
|
||||
TrustStores: []string{"test"},
|
||||
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
|
||||
},
|
||||
},
|
||||
want: &trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "skip"},
|
||||
TrustStores: []string{},
|
||||
TrustedIdentities: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantLogMessage: "warning: trust policy statement 'test-statement-name' is set to skip signature verification but configured with trust stores and/or trusted identities. Ignoring trust stores and trusted identities",
|
||||
},
|
||||
{
|
||||
name: "trust policy with multiple policies and mixture of verification levels including skip",
|
||||
policy: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"test"},
|
||||
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
|
||||
},
|
||||
{
|
||||
Name: "test-statement-name-2",
|
||||
RegistryScopes: []string{"example.com/podInfo"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "skip"},
|
||||
TrustStores: []string{"test"},
|
||||
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
|
||||
},
|
||||
},
|
||||
want: &trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"test"},
|
||||
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
|
||||
},
|
||||
{
|
||||
Name: "test-statement-name-2",
|
||||
RegistryScopes: []string{"example.com/podInfo"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "skip"},
|
||||
TrustStores: []string{},
|
||||
TrustedIdentities: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantLogMessage: "warning: trust policy statement 'test-statement-name-2' is set to skip signature verification but configured with trust stores and/or trusted identities. Ignoring trust stores and trusted identities",
|
||||
},
|
||||
}
|
||||
|
||||
// Run the test cases
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
l := &testLogger{[]string{}, logr.RuntimeInfo{CallDepth: 1}}
|
||||
logger := logr.New(l)
|
||||
|
||||
var policy *trustpolicy.Document
|
||||
|
||||
if tc.policy != nil {
|
||||
policy = &trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: tc.policy,
|
||||
}
|
||||
}
|
||||
|
||||
cleanedPolicy := CleanTrustPolicy(policy, logger)
|
||||
|
||||
if !reflect.DeepEqual(cleanedPolicy, tc.want) {
|
||||
t.Errorf("got %#v, want %#v", cleanedPolicy, tc.want)
|
||||
}
|
||||
|
||||
if tc.wantLogMessage != "" {
|
||||
g.Expect(len(l.Output)).Should(Equal(1))
|
||||
g.Expect(l.Output[0]).Should(Equal(tc.wantLogMessage))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutcomeChecker(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
outcome []*notation.VerificationOutcome
|
||||
wantErrMessage string
|
||||
wantLogMessage []string
|
||||
wantVerificationResult oci.VerificationResult
|
||||
}{
|
||||
{
|
||||
name: "no outcome failed with error message",
|
||||
wantVerificationResult: oci.VerificationResultFailed,
|
||||
wantErrMessage: "signature verification failed for all the signatures associated with example.com/podInfo",
|
||||
},
|
||||
{
|
||||
name: "verification result ignored with log message",
|
||||
outcome: []*notation.VerificationOutcome{
|
||||
{
|
||||
VerificationLevel: trustpolicy.LevelAudit,
|
||||
VerificationResults: []*notation.ValidationResult{
|
||||
{
|
||||
Type: trustpolicy.TypeAuthenticity,
|
||||
Action: trustpolicy.ActionLog,
|
||||
Error: fmt.Errorf("123"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantVerificationResult: oci.VerificationResultIgnored,
|
||||
wantLogMessage: []string{"verification check for type 'authenticity' failed for 'example.com/podInfo' with message: '123'"},
|
||||
},
|
||||
{
|
||||
name: "verification result ignored with no log message (skip)",
|
||||
outcome: []*notation.VerificationOutcome{
|
||||
{
|
||||
VerificationLevel: trustpolicy.LevelSkip,
|
||||
VerificationResults: []*notation.ValidationResult{},
|
||||
},
|
||||
},
|
||||
wantVerificationResult: oci.VerificationResultIgnored,
|
||||
},
|
||||
{
|
||||
name: "verification result success with log message",
|
||||
outcome: []*notation.VerificationOutcome{
|
||||
{
|
||||
VerificationLevel: trustpolicy.LevelAudit,
|
||||
VerificationResults: []*notation.ValidationResult{
|
||||
{
|
||||
Type: trustpolicy.TypeAuthenticTimestamp,
|
||||
Action: trustpolicy.ActionLog,
|
||||
Error: fmt.Errorf("456"),
|
||||
},
|
||||
{
|
||||
Type: trustpolicy.TypeExpiry,
|
||||
Action: trustpolicy.ActionLog,
|
||||
Error: fmt.Errorf("789"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantVerificationResult: oci.VerificationResultSuccess,
|
||||
wantLogMessage: []string{
|
||||
"verification check for type 'authenticTimestamp' failed for 'example.com/podInfo' with message: '456'",
|
||||
"verification check for type 'expiry' failed for 'example.com/podInfo' with message: '789'",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verification result success with no log message",
|
||||
outcome: []*notation.VerificationOutcome{
|
||||
{
|
||||
VerificationLevel: trustpolicy.LevelAudit,
|
||||
VerificationResults: []*notation.ValidationResult{},
|
||||
},
|
||||
},
|
||||
wantVerificationResult: oci.VerificationResultSuccess,
|
||||
},
|
||||
}
|
||||
|
||||
// Run the test cases
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
l := &testLogger{[]string{}, logr.RuntimeInfo{CallDepth: 1}}
|
||||
logger := logr.New(l)
|
||||
|
||||
v := NotationVerifier{
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
result, err := v.checkOutcome(tc.outcome, "example.com/podInfo")
|
||||
|
||||
if tc.wantErrMessage != "" {
|
||||
g.Expect(err).ToNot(BeNil())
|
||||
g.Expect(err.Error()).Should(Equal(tc.wantErrMessage))
|
||||
} else {
|
||||
g.Expect(err).To(BeNil())
|
||||
}
|
||||
|
||||
g.Expect(result).Should(Equal(tc.wantVerificationResult))
|
||||
g.Expect(len(l.Output)).Should(Equal(len(tc.wantLogMessage)))
|
||||
|
||||
for i, j := range tc.wantLogMessage {
|
||||
g.Expect(l.Output[i]).Should(Equal(j))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoUrlWithDigest(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
repoUrl string
|
||||
digest string
|
||||
tag string
|
||||
wantResultUrl string
|
||||
wantErrMessage string
|
||||
passUrlWithoutTag bool
|
||||
}{
|
||||
{
|
||||
name: "valid repo url with digest",
|
||||
repoUrl: "ghcr.io/stefanprodan/charts/podinfo",
|
||||
digest: "sha256:cdd538a0167e4b51152b71a477e51eb6737553510ce8797dbcc537e1342311bb",
|
||||
wantResultUrl: "ghcr.io/stefanprodan/charts/podinfo@sha256:cdd538a0167e4b51152b71a477e51eb6737553510ce8797dbcc537e1342311bb",
|
||||
wantErrMessage: "",
|
||||
},
|
||||
{
|
||||
name: "valid repo url with tag",
|
||||
repoUrl: "ghcr.io/stefanprodan/charts/podinfo",
|
||||
tag: "6.6.0",
|
||||
wantResultUrl: "ghcr.io/stefanprodan/charts/podinfo@sha256:cdd538a0167e4b51152b71a477e51eb6737553510ce8797dbcc537e1342311bb",
|
||||
wantErrMessage: "",
|
||||
},
|
||||
{
|
||||
name: "valid repo url without tag",
|
||||
repoUrl: "ghcr.io/stefanprodan/charts/podinfo",
|
||||
tag: "6.6.0",
|
||||
wantResultUrl: "ghcr.io/stefanprodan/charts/podinfo@sha256:cdd538a0167e4b51152b71a477e51eb6737553510ce8797dbcc537e1342311bb",
|
||||
wantErrMessage: "url ghcr.io/stefanprodan/charts/podinfo does not contain tag or digest",
|
||||
passUrlWithoutTag: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Run the test cases
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
l := &testLogger{[]string{}, logr.RuntimeInfo{CallDepth: 1}}
|
||||
logger := logr.New(l)
|
||||
|
||||
v := NotationVerifier{
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
var url string
|
||||
repo, _ := name.NewRepository(tc.repoUrl)
|
||||
var ref name.Reference
|
||||
if tc.digest != "" {
|
||||
ref = repo.Digest(tc.digest)
|
||||
url = fmt.Sprintf("%s@%s", tc.repoUrl, tc.digest)
|
||||
} else if tc.tag != "" {
|
||||
ref = repo.Tag(tc.tag)
|
||||
if !tc.passUrlWithoutTag {
|
||||
url = fmt.Sprintf("%s:%s", tc.repoUrl, tc.tag)
|
||||
} else {
|
||||
url = tc.repoUrl
|
||||
}
|
||||
} else {
|
||||
ref = repo.Tag(name.DefaultTag)
|
||||
url = fmt.Sprintf("%s:%s", tc.repoUrl, name.DefaultTag)
|
||||
}
|
||||
|
||||
result, err := v.repoUrlWithDigest(url, ref)
|
||||
|
||||
if tc.wantErrMessage != "" {
|
||||
g.Expect(err).ToNot(BeNil())
|
||||
g.Expect(err.Error()).Should(Equal(tc.wantErrMessage))
|
||||
} else {
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(result).Should(Equal(tc.wantResultUrl))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func dummyPolicyDocument() (policyDoc *trustpolicy.Document) {
|
||||
policyDoc = &trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{dummyPolicyStatement()},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
|
||||
policyStatement = trustpolicy.TrustPolicy{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
|
||||
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// mocking LogSink to capture log messages. Source: https://stackoverflow.com/a/71425740
|
||||
type testLogger struct {
|
||||
Output []string
|
||||
r logr.RuntimeInfo
|
||||
}
|
||||
|
||||
func (t *testLogger) doLog(msg string) {
|
||||
t.Output = append(t.Output, msg)
|
||||
}
|
||||
|
||||
func (t *testLogger) Init(info logr.RuntimeInfo) {
|
||||
t.r = info
|
||||
}
|
||||
|
||||
func (t *testLogger) Enabled(level int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *testLogger) Info(level int, msg string, keysAndValues ...interface{}) {
|
||||
t.doLog(msg)
|
||||
}
|
||||
|
||||
func (t *testLogger) Error(err error, msg string, keysAndValues ...interface{}) {
|
||||
t.doLog(msg)
|
||||
}
|
||||
|
||||
func (t *testLogger) WithValues(keysAndValues ...interface{}) logr.LogSink {
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *testLogger) WithName(name string) logr.LogSink {
|
||||
return t
|
||||
}
|
|
@ -18,154 +18,25 @@ package oci
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio"
|
||||
coptions "github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor"
|
||||
"github.com/sigstore/cosign/v2/pkg/cosign"
|
||||
"github.com/sigstore/cosign/v2/pkg/oci"
|
||||
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
|
||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
)
|
||||
|
||||
// VerificationResult represents the result of a verification process.
|
||||
type VerificationResult string
|
||||
|
||||
const (
|
||||
// VerificationResultSuccess indicates that the artifact has been verified.
|
||||
VerificationResultSuccess VerificationResult = "verified"
|
||||
// VerificationResultFailed indicates that the artifact could not be verified.
|
||||
VerificationResultFailed VerificationResult = "unverified"
|
||||
// VerificationResultIgnored indicates that the artifact has not been verified
|
||||
// but is allowed to proceed. This is used primarily when notation is used
|
||||
// as the verifier.
|
||||
VerificationResultIgnored VerificationResult = "ignored"
|
||||
)
|
||||
|
||||
// Verifier is an interface for verifying the authenticity of an OCI image.
|
||||
type Verifier interface {
|
||||
Verify(ctx context.Context, ref name.Reference) (bool, error)
|
||||
}
|
||||
|
||||
// options is a struct that holds options for verifier.
|
||||
type options struct {
|
||||
PublicKey []byte
|
||||
ROpt []remote.Option
|
||||
Identities []cosign.Identity
|
||||
}
|
||||
|
||||
// Options is a function that configures the options applied to a Verifier.
|
||||
type Options func(opts *options)
|
||||
|
||||
// WithPublicKey sets the public key.
|
||||
func WithPublicKey(publicKey []byte) Options {
|
||||
return func(opts *options) {
|
||||
opts.PublicKey = publicKey
|
||||
}
|
||||
}
|
||||
|
||||
// WithRemoteOptions is a functional option for overriding the default
|
||||
// remote options used by the verifier.
|
||||
func WithRemoteOptions(opts ...remote.Option) Options {
|
||||
return func(o *options) {
|
||||
o.ROpt = opts
|
||||
}
|
||||
}
|
||||
|
||||
// WithIdentities specifies the identity matchers that have to be met
|
||||
// for the signature to be deemed valid.
|
||||
func WithIdentities(identities []cosign.Identity) Options {
|
||||
return func(opts *options) {
|
||||
opts.Identities = identities
|
||||
}
|
||||
}
|
||||
|
||||
// CosignVerifier is a struct which is responsible for executing verification logic.
|
||||
type CosignVerifier struct {
|
||||
opts *cosign.CheckOpts
|
||||
}
|
||||
|
||||
// NewCosignVerifier initializes a new CosignVerifier.
|
||||
func NewCosignVerifier(ctx context.Context, opts ...Options) (*CosignVerifier, error) {
|
||||
o := options{}
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
checkOpts := &cosign.CheckOpts{}
|
||||
|
||||
ro := coptions.RegistryOptions{}
|
||||
co, err := ro.ClientOpts(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
checkOpts.Identities = o.Identities
|
||||
if o.ROpt != nil {
|
||||
co = append(co, ociremote.WithRemoteOptions(o.ROpt...))
|
||||
}
|
||||
|
||||
checkOpts.RegistryClientOpts = co
|
||||
|
||||
// If a public key is provided, it will use it to verify the signature.
|
||||
// If there is no public key provided, it will try keyless verification.
|
||||
// https://github.com/sigstore/cosign/blob/main/KEYLESS.md.
|
||||
if len(o.PublicKey) > 0 {
|
||||
checkOpts.Offline = true
|
||||
// TODO(hidde): this is an oversight in our implementation. As it is
|
||||
// theoretically possible to have a custom PK, without disabling tlog.
|
||||
checkOpts.IgnoreTlog = true
|
||||
|
||||
pubKeyRaw, err := cryptoutils.UnmarshalPEMToPublicKey(o.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
checkOpts.SigVerifier, err = signature.LoadVerifier(pubKeyRaw, crypto.SHA256)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
checkOpts.RekorClient, err = rekor.NewClient(coptions.DefaultRekorURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create Rekor client: %w", err)
|
||||
}
|
||||
|
||||
// This performs an online fetch of the Rekor public keys, but this is needed
|
||||
// for verifying tlog entries (both online and offline).
|
||||
// TODO(hidde): above note is important to keep in mind when we implement
|
||||
// "offline" tlog above.
|
||||
if checkOpts.RekorPubKeys, err = cosign.GetRekorPubs(ctx); err != nil {
|
||||
return nil, fmt.Errorf("unable to get Rekor public keys: %w", err)
|
||||
}
|
||||
|
||||
checkOpts.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get CTLog public keys: %w", err)
|
||||
}
|
||||
|
||||
if checkOpts.RootCerts, err = fulcio.GetRoots(); err != nil {
|
||||
return nil, fmt.Errorf("unable to get Fulcio root certs: %w", err)
|
||||
}
|
||||
|
||||
if checkOpts.IntermediateCerts, err = fulcio.GetIntermediates(); err != nil {
|
||||
return nil, fmt.Errorf("unable to get Fulcio intermediate certs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &CosignVerifier{
|
||||
opts: checkOpts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyImageSignatures verify the authenticity of the given ref OCI image.
|
||||
func (v *CosignVerifier) VerifyImageSignatures(ctx context.Context, ref name.Reference) ([]oci.Signature, bool, error) {
|
||||
return cosign.VerifyImageSignatures(ctx, ref, v.opts)
|
||||
}
|
||||
|
||||
// Verify verifies the authenticity of the given ref OCI image.
|
||||
// It returns a boolean indicating if the verification was successful.
|
||||
// It returns an error if the verification fails, nil otherwise.
|
||||
func (v *CosignVerifier) Verify(ctx context.Context, ref name.Reference) (bool, error) {
|
||||
signatures, _, err := v.VerifyImageSignatures(ctx, ref)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(signatures) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
Verify(ctx context.Context, ref name.Reference) (VerificationResult, error)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue