docs: first cut of a new README (#99)
Lots of this is taken from image-signer-verifier's README. The stuff on policy is all new. Co-authored-by: James Carnegie <kipz@users.noreply.github.com>
This commit is contained in:
376
README.md
376
README.md
@@ -1,16 +1,372 @@
|
||||
# attest
|
||||
library to create, verify, and evaluate policy for attestations on container images
|
||||
# `attest`
|
||||
|
||||
<div align="center">
|
||||
Library to create attestation signatures on container images, and verify images against policy.
|
||||
|
||||
[](https://pkg.go.dev/github.com/docker/attest)
|
||||
[](https://github.com/docker/attest/actions/workflows/test.yml)
|
||||
[](https://codecov.io/gh/docker/attest)
|
||||
|
||||
# usage
|
||||
## signing and verifying attestations
|
||||
See [example_sign_test.go](./pkg/attest/example_sign_test.go)
|
||||
</div>
|
||||
|
||||
See [example_verify_test.go](./pkg/attest/example_verify_test.go)
|
||||
# Table of Contents
|
||||
|
||||
## mirroring TUF repositories to OCI
|
||||
See [example_mirror_test.go](./pkg/mirror/example_mirror_test.go)
|
||||
- [`attest`](#attest)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [What is this?](#what-is-this)
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Verifying Image Attestations](#verifying-image-attestations)
|
||||
- [Signing Attestations](#signing-attestations)
|
||||
- [Rego Policy](#rego-policy)
|
||||
- [Writing Policy](#writing-policy)
|
||||
- [Input](#input)
|
||||
- [Builtin Functions](#builtin-functions)
|
||||
- [Policy Mapping](#policy-mapping)
|
||||
- [Public Key IDs](#public-key-ids)
|
||||
- [Transparency Logging](#transparency-logging)
|
||||
- [Verification Summary Attestation (VSA)](#verification-summary-attestation-vsa)
|
||||
- [Example VSA](#example-vsa)
|
||||
- [API Reference](#api-reference)
|
||||
- [Project Layout](#project-layout)
|
||||
- [Versioning](#versioning)
|
||||
|
||||
### using `go-tuf` OCI registry client
|
||||
See [example_registry_test.go](./pkg/tuf/example_registry_test.go)
|
||||
# What is this?
|
||||
|
||||
`attest` is a library for signing and verifying [in-toto](https://in-toto.io/) attestations on container images.
|
||||
Examples of attestations include statements about the provenance and SBOM of an image.
|
||||
|
||||
This library can be used to verify these attestations using Rego policy.
|
||||
Policy can be used to check whether an attestation is correctly signed, and that the contents of the attestation are correct.
|
||||
|
||||
Our overall goal with this project is adoption of the ideas into other open-source projects, rather than to create another standalone tool.
|
||||
It would be a great outcome if this library was no longer needed because the functionality was built into other tools.
|
||||
|
||||
# Features
|
||||
|
||||
- Sign in-toto attestations
|
||||
- Push attestations to container registries using OCI 1.1 compatible artifacts
|
||||
- Verify attestations on container images using Rego policy and attestations fetched using OCI 1.1 referrers
|
||||
|
||||
# Installation
|
||||
|
||||
```shell
|
||||
$ go get github.com/docker/attest
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
## Verifying Image Attestations
|
||||
|
||||
An image's attestations can be verified against a policy using the `attest.Verify` function.
|
||||
This function takes an [oci.ImageSpec](https://github.com/docker/attest/blob/781a738b54b9549c1dabfd7ea3f7ea582514ddec/pkg/oci/types.go#L35-L41) for the image to verify, and a set of options for policy resolution.
|
||||
By default, the policy is resolved from the [the Docker TUF repository](https://github.com/docker/tuf), but the options can be used to specify an alternative TUF repository, a local policy directory, and/or a policy ID to use.
|
||||
See [Policy Mapping](#policy-mapping) for more details.
|
||||
|
||||
The `attest.Verify` function returns a `VerificationSummary` object, which contains the results of the policy evaluation.
|
||||
|
||||
See [example_verify_test.go](./pkg/attest/example_verify_test.go) for an example of how to verify an image against a policy.
|
||||
|
||||
## Signing Attestations
|
||||
|
||||
in-toto statements can be signed directly using the `attestation.SignInTotoStatement` function.
|
||||
This function takes a statement and DSSE signer, and returns a signed DSSE envelope containing a copy of the original statement.
|
||||
|
||||
For the common use case of signing a statement and adding it to a manifest, e.g. for pushing to a registry as a referrer to the image being attested, the `attestation.AttestationManifest` type can be used.
|
||||
See [example_attestation_manifest_test.go](./pkg/attestation/example_attestation_manifest_test.go)
|
||||
|
||||
See also [example_sign_test.go](./pkg/attest/example_sign_test.go) for an example of how to sign all attached in-toto statements on an image, e.g. those produced by buildkit.
|
||||
|
||||
# Rego Policy
|
||||
|
||||
An image policy consists of one or more `rego` files and, optionally, `json` or `yaml` data files.
|
||||
|
||||
The policies for trusted namespaces `docker.io/docker` and `docker.io/library` are stored in [the Docker TUF root](https://github.com/docker/tuf) under the `docker` and `doi` target sub-directories respectively.
|
||||
|
||||
## Writing Policy
|
||||
|
||||
`attest` uses [Open Policy Agent](https://www.openpolicyagent.org/) (OPA) for policy evaluation, and policies are written in Rego.
|
||||
A full guide to writing Rego policies is available in the [Rego documentation](https://www.openpolicyagent.org/docs/latest/policy-language/).
|
||||
|
||||
For attest, a policy must contain at a minimum a `result` rule in a package called `attest` that returns an object matching the schema defined by the [`policy.Result`](https://github.com/docker/attest/blob/bd2c4d7d8aa497754b674412b09628be8d02fab5/pkg/policy/types.go#L23-L27) struct.
|
||||
For example:
|
||||
|
||||
```rego
|
||||
package attest
|
||||
|
||||
import rego.v1
|
||||
|
||||
result := {
|
||||
"success": true,
|
||||
"violations": set(),
|
||||
"summary": {
|
||||
"subjects": subjects,
|
||||
"slsa_levels": ["SLSA_BUILD_LEVEL_3"],
|
||||
"verifier": "docker-official-images",
|
||||
"policy_uri": "https://docker.com/official/policy/v0.1",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The meanings of the fields in the `result` object are as follows:
|
||||
|
||||
- `success` (bool): whether the policy passes
|
||||
- `violations` (set): a set of strings describing any policy violations
|
||||
- `summary` (object): a summary of the policy evaluation, used to construct a Verification Summary Attestation (VSA)
|
||||
- `subjects` (set): a set of strings representing the subjects of each attestation that was evaluated
|
||||
- `slsa_levels` (list): a list of strings representing the SLSA levels that the policy complies with
|
||||
- `verifier` (string): the entity that verified the policy
|
||||
- `policy_uri` (string): the URI of the policy
|
||||
|
||||
The `violations` set may contain policy violations even if `success` is `true`.
|
||||
This can be useful if there are attestations that are invalid, but are not required by the policy.
|
||||
|
||||
### Input
|
||||
|
||||
The input to the policy is an object with the following fields:
|
||||
|
||||
- `digest` (string): the digest of the image being verified
|
||||
- `purl` (string): the package URL of the image being verified
|
||||
- `is_canonical` (bool): whether the image being verified was referenced by a 'canonical' name, i.e. one that contains a digest
|
||||
|
||||
### Builtin Functions
|
||||
|
||||
There are two builtin functions provided by `attest` that can be used to help with policy evaluation:
|
||||
|
||||
- `attest.fetch(predicate_type)`: fetches all attestations for the input image with the given predicate type.
|
||||
For example, `attest.fetch("https://spdx.dev/Document")` will fetch all SPDX SBOM attestations for the input image.
|
||||
- `attest.verify(attestation, options)`: verifies the DSSE envelope of the given attestation, and returns the statement.
|
||||
The options object can contain the following fields:
|
||||
- `keys` (array): keys to use for signature verification. Each key contains the following fields:
|
||||
- `id` (string): the key ID as specified in [Public Key IDs](#public-key-ids)
|
||||
- `key` (string): the PEM-encoded public key
|
||||
- `from` (string): the time from which the key is valid, or `null` if the key was always valid (default: `null`)
|
||||
- `status` (string): `active` if the key is active, otherwise the reason the key is inactive.
|
||||
This is only used in error messages if the `from` date is in the past
|
||||
- `distrust` (bool): whether the key should be distrusted (default: `false`).
|
||||
If `true`, the key will be considered invalid
|
||||
- `signing-format` (string): the format of the signing key, must be `dssev1`
|
||||
- `skip_tl` (bool): whether to skip transparency log entry verification (see [Transparency Logging](#transparency-logging)) (default: `false`)
|
||||
|
||||
Both `attest.fetch` and `attest.verify` return an object with the following fields:
|
||||
|
||||
- `value`: the return value of the function if successful
|
||||
- `error`: an error message if the function failed
|
||||
|
||||
This is to allow the policy to easily construct a violation if an error occurs, which isn't usually possible with custom functions in Rego.
|
||||
|
||||
The return value of `attest.fetch` is an attestation which can be passed to `attest.verify`.
|
||||
|
||||
## Policy Mapping
|
||||
|
||||
A `mapping.yaml` file is stored at the root of TUF targets and contains the mapping from repository name to files containing the corresponding policy.
|
||||
|
||||
A simple mapping file might look like this:
|
||||
|
||||
```yaml
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
policies:
|
||||
- id: docker-official-images
|
||||
description: Docker Official Images
|
||||
files:
|
||||
- path: doi/policy.rego
|
||||
rules:
|
||||
- pattern: "^docker[.]io/library/(.*)$"
|
||||
policy-id: docker-official-images
|
||||
```
|
||||
|
||||
The `policies` section contains a list of policies, each with an `id` and a `description`, and a list of `files` containing the policy.
|
||||
The `rules` section contains a list of rules that map regex expressions to policies.
|
||||
If the `pattern` regex matches the repository name, the policy with the `policy-id` is used to evaluate the image.
|
||||
In the above example, any repository in the `docker.io/library` namespace will be evaluated against the policy in `doi/policy.rego`.
|
||||
|
||||
Sometimes it is necessary to rewrite the repository name before evaluating the policy.
|
||||
This can be useful when the repository name which is used to reference the image is different from the repository name in the attestations.
|
||||
For example, when mirroring images from a public registry to a private registry, the repository name in the attestations will be the public registry, but the image will be referenced by the name of the private registry.
|
||||
|
||||
An example of a mapping file with rewrite rules might look like this:
|
||||
|
||||
```yaml
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
policies:
|
||||
- id: docker-official-images
|
||||
description: Docker Official Images
|
||||
files:
|
||||
- path: doi/policy.rego
|
||||
rules:
|
||||
- pattern: "^docker[.]io/library/(.*)$"
|
||||
policy-id: docker-official-images
|
||||
- pattern: "^public[.]ecr[.]aws/docker/library/(.*)$"
|
||||
rewrite: docker.io/library/$1
|
||||
```
|
||||
|
||||
As before, any repository in the `docker.io/library` namespace will be evaluated against the policy in `doi/policy.rego`.
|
||||
The second rule will rewrite any repository in the `public.ecr.aws/docker/library` namespace to `docker.io/library`.
|
||||
This means two things:
|
||||
|
||||
1. The rules are evaluated again using the rewritten repository name until a policy is found (in this case the first rule will match); and
|
||||
2. The rewritten name is passed into the actual policy when it is evaluated.
|
||||
|
||||
The `rewrite` field is not a simple string replacement, but a regex replacement.
|
||||
This means that the `rewrite` field can contain capture groups that are referenced in the `pattern` field.
|
||||
For example, the `rewrite` field in the example above contains `$1`, which is a reference to the first capture group in the `pattern` field.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> It's important to remember to escape the `.` character in the `pattern` field, as it is a special character in regex.
|
||||
> This is why the `.` character is surrounded by `[]` in the example above.
|
||||
>
|
||||
> It's also important to make use of the `^` and `$` characters in the `pattern` field to ensure that the regex matches the entire repository name.
|
||||
> This is to prevent the regex from matching a subset of the repository name, e.g. `docker.io/library` matching `notdocker.io/library`.
|
||||
|
||||
Local policy can also be specified via a local `mapping.yaml`, which can be used to create new mirrors of policies described in the Docker TUF root, as well as describing entirely independent policies. For example:
|
||||
|
||||
```golang
|
||||
// configure policy options
|
||||
opts := &policy.PolicyOptions{
|
||||
TufClient: tufClient,
|
||||
LocalPolicyDir: "<policy-dir>", // overrides TUF policy for local policy files if set
|
||||
PolicyId: "<policy-id>", // set to ignore policy mapping and select a policy by id
|
||||
}
|
||||
|
||||
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// verify attestations
|
||||
result, err := attest.Verify(context.Background(), src, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
where `<policy-dir>` is a directory containing a `mapping.yaml` file, and any policy files referenced in the `mapping.yaml`. For example:
|
||||
|
||||
```
|
||||
├── myimages
|
||||
│ ├── data.yaml
|
||||
| ├── keys.yaml
|
||||
│ └── policy.rego
|
||||
└── mapping.yaml
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> `PolicyId` can also be set to select a policy by ID, completely ignoring the `rules` section of the mapping file.
|
||||
|
||||
The rules section of a local `mapping.yaml` can refer to the policies described in the `mapping.yaml` file in the Docker TUF root to specify additional mirrors to which the referenced policy can be applied.
|
||||
|
||||
For example, it might be desirable to mirror `docker.io/library` to a local registry for testing:
|
||||
|
||||
```yaml
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
rules:
|
||||
- pattern: "^localhost:5001/(.*)$"
|
||||
rewrite: docker.io/library/$1
|
||||
```
|
||||
|
||||
The rewritten repository name will match the `docker-official-images` polict in the TUF managed `mapping.yaml`.
|
||||
|
||||
> [!WARNING]
|
||||
> Local `mapping.yaml` policies take precendence over TUF managed policies, so for example, it's possible to apply a custom policy to `docker.io/library` namespace:
|
||||
>
|
||||
> ```yaml
|
||||
> version: v1
|
||||
> kind: policy-mapping
|
||||
> policies:
|
||||
> - id: mydoi
|
||||
> description: my doi policy
|
||||
> files:
|
||||
> - path: "mypolicy.rego"
|
||||
>
|
||||
> rules:
|
||||
> - pattern: "^docker[.]io/library/(.*)$"
|
||||
> policy-id: mydoi
|
||||
> ```
|
||||
|
||||
# Public Key IDs
|
||||
|
||||
When signing attestations, a key-id is generated from the public key and added to envelope.
|
||||
This is used at verification time to look up the public key.
|
||||
|
||||
To generate a key-id from a public key, use `openssl` as follows:
|
||||
|
||||
```shell
|
||||
openssl pkey -in <public-key.pem> -pubin -outform DER | openssl dgst -sha256
|
||||
```
|
||||
|
||||
# Transparency Logging
|
||||
|
||||
`attest` supports transparency logging for attestation signatures.
|
||||
This serves two purposes:
|
||||
|
||||
1. the transparency log is a mechanism to ensure that all attestations are logged in a tamper-evident way, and that the logs are publicly auditable; and
|
||||
2. the transparency log is a trusted source of timestamps for attestations, which allows signatures to be verified even if the key used to sign the attestation has expired.
|
||||
|
||||
By default, transparency logging is enabled and the logs are stored in the [public-good Rekor](https://docs.sigstore.dev/logging/overview/) instance.
|
||||
Another transparency log can be used by creating an implementation of the [tl.TL](https://github.com/docker/attest/blob/781a738b54b9549c1dabfd7ea3f7ea582514ddec/pkg/tlog/tl.go#L57-L62) interface and using [`tl.WithTL`](https://github.com/docker/attest/blob/781a738b54b9549c1dabfd7ea3f7ea582514ddec/pkg/tlog/tl.go#L37) to set in on a context.
|
||||
Alternatively, transparency logging can be disabled when signing by using `SkipTL` in the `SigningOptions`, and when verifying by using `skip_tl` in the options to `attest.verify` in the Rego policy.
|
||||
|
||||
# Verification Summary Attestation (VSA)
|
||||
|
||||
Verification of attestations can be expensive, especially when the attestations are large.
|
||||
For example, an SBOM attestation can be several megabytes in size.
|
||||
An alternative to consumers verifying the full attestation is to have a trusted entity verify the attestation and publish a [SLSA Verification Summary Attestation](https://slsa.dev/spec/v1.0/verification_summary) (VSA) to the registry.
|
||||
The VSA can then be verified by the consumer without needing to verify the full attestation, as long as the consumer trusts the entity that signed the VSA.
|
||||
This is useful when the consumer only needs to know that the attestation was verified by a trusted entity, and does not need to know the details of the attestation.
|
||||
|
||||
A useful pattern is to have apply a policy to a third-party image at initial ingress, then publish a VSA when publishing the image to an internal registry to attest that the image complies with the policy.
|
||||
The VSA can be verified very quickly, for example in a Kubernetes admission controller.
|
||||
|
||||
`attest` always generates a [SLSA VSA](https://slsa.dev/spec/v1.0/verification_summary) when verifying attestations on an image.
|
||||
The VSA can be signed and published to the registry using the signing functions mentioned in [Signing Attestations](#signing-attestations).
|
||||
|
||||
## Example VSA
|
||||
|
||||
```json
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"subject": [
|
||||
{
|
||||
"name": "pkg:docker/example.org/example-image@1.0?platform=linux%2Famd64",
|
||||
"digest": {
|
||||
"sha256": "49f717386e5462e945232569a97a05831cb83bef8c3369be3bb7ea1793686960"
|
||||
}
|
||||
}
|
||||
],
|
||||
"predicateType": "https://slsa.dev/verification_summary/v1",
|
||||
"predicate": {
|
||||
"verifier": {
|
||||
"id": "https://example.org/internal-verifier"
|
||||
},
|
||||
"timeVerified": "2024-04-19T08:00:00.01Z",
|
||||
"resourceUri": "pkg:docker/example.org/example-image@1.0?platform=linux%2Famd64&digest=sha256%3A49f717386e5462e945232569a97a05831cb83bef8c3369be3bb7ea1793686960",
|
||||
"policy": {
|
||||
"uri": "https://example.org/internal-policy/v1"
|
||||
},
|
||||
"verificationResult": "PASSED",
|
||||
"verifiedLevels": ["SLSA_BUILD_LEVEL_3"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# API Reference
|
||||
|
||||
Full API reference can be found at [pkg.go.dev/github.com/docker/attest](https://pkg.go.dev/github.com/docker/attest).
|
||||
|
||||
# Project Layout
|
||||
|
||||
- [pkg/](https://pkg.go.dev/github.com/docker/image-signer-verifier/pkg) => packages that are okay to import for other projects
|
||||
- [internal/](https://pkg.go.dev/github.com/docker/image-signer-verifier/pkg) => packages that are only for project internal purposes
|
||||
- [scripts/](scripts/) => build scripts
|
||||
- [test/](test/) => data for use in tests
|
||||
|
||||
# Versioning
|
||||
|
||||
`attest` uses [Semantic Versioning](https://semver.org/).
|
||||
As such, until `attest` reaches version 1.0.0, breaking changes may be introduced in minor versions.
|
||||
|
||||
> Anything MAY change at any time. The public API SHOULD NOT be considered stable.
|
||||
|
||||
@@ -48,11 +48,11 @@ func ExampleVerify_remote() {
|
||||
PolicyId: "", // set to ignore policy mapping and select a policy by id
|
||||
}
|
||||
|
||||
// verify attestations
|
||||
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// verify attestations
|
||||
result, err := attest.Verify(context.Background(), src, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
87
pkg/attestation/example_attestation_manifest_test.go
Normal file
87
pkg/attestation/example_attestation_manifest_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package attestation_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/docker/attest/pkg/attest"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/mirror"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/signerverifier"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
||||
)
|
||||
|
||||
func ExampleAttestationManifest() {
|
||||
// configure signerverifier
|
||||
// local signer (unsafe for production)
|
||||
signer, err := signerverifier.GenKeyPair()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// example using AWS KMS signer
|
||||
// aws_arn := "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012"
|
||||
// aws_region := "us-west-2"
|
||||
// signer, err := signerverifier.GetAWSSigner(cmd.Context(), aws_arn, aws_region)
|
||||
|
||||
// configure signing options
|
||||
opts := &attestation.SigningOptions{
|
||||
SkipTL: true, // skip trust logging to a transparency log
|
||||
}
|
||||
|
||||
ref := "docker/image-signer-verifier:latest"
|
||||
|
||||
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
desc := &v1.Descriptor{
|
||||
Digest: digest,
|
||||
Size: 1234,
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
}
|
||||
|
||||
// the in-toto statement to be signed
|
||||
statement := &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
Subject: []intoto.Subject{{Name: ref, Digest: common.DigestSet{digest.Algorithm: digest.Hex}}},
|
||||
Type: intoto.StatementInTotoV01,
|
||||
},
|
||||
Predicate: attestation.VSAPredicate{
|
||||
Verifier: attestation.VSAVerifier{
|
||||
ID: "test-verifier",
|
||||
},
|
||||
TimeVerified: time.Now().UTC().Format(time.RFC3339),
|
||||
ResourceUri: "some-uri",
|
||||
Policy: attestation.VSAPolicy{URI: "some-uri"},
|
||||
VerificationResult: "PASSED",
|
||||
VerifiedLevels: []string{"SLSA_BUILD_LEVEL_1"},
|
||||
},
|
||||
}
|
||||
|
||||
// create a new manifest to hold the attestation
|
||||
manifest, err := attest.NewAttestationManifest(desc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// sign and add the attestation to the manifest
|
||||
err = manifest.AddAttestation(context.Background(), signer, statement, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
output, err := oci.ParseImageSpecs("docker/image-signer-verifier-referrers:latest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// save the manifest to the registry as a referrers artifact
|
||||
err = mirror.SaveReferrers(manifest, output)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user