attest
- Table of Contents
- What is this?
- Features
- Installation
- Usage
- Rego Policy
- Public Key IDs
- Transparency Logging
- Verification Summary Attestation (VSA)
- API Reference
- Project Layout
- Versioning
attest
is a library for signing and verifying in-toto 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.
- 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
$ go get github.com/docker/attest
An image's attestations can be verified against a policy using the attest.Verify
function.
This function takes an oci.ImageSpec 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, 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 for more details.
The attest.Verify
function returns a VerificationSummary
object, which contains the results of the policy evaluation.
See example_verify_test.go for an example of how to verify an image against a policy.
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
See also 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.
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 under the docker
and doi
target sub-directories respectively.
attest
uses Open Policy Agent (OPA) for policy evaluation, and policies are written in Rego.
A full guide to writing Rego policies is available in the Rego documentation.
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
struct.
For example:
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 passesviolations
(set): a set of strings describing any policy violationssummary
(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 evaluatedslsa_levels
(list): a list of strings representing the SLSA levels that the policy complies withverifier
(string): the entity that verified the policypolicy_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.
The input to the policy is an object with the following fields:
digest
(string): the digest of the image being verifiedpurl
(string): the package URL of the image being verifiedplatform
(string): the platform of the image being verifiednormalized_name
(string): defaults are filled out. e.g. if the image isalpine
, this would belibrary/alpine
familiar_name
(string): short version of above (e.g.alpine
)tag
: (string): tag of the image being verified (if present)
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 IDskey
(string): the PEM-encoded public keyfrom
(string): the time from which the key is valid, ornull
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 thefrom
date is in the pastdistrust
(bool): whether the key should be distrusted (default:false
). Iftrue
, the key will be considered invalidsigning-format
(string): the format of the signing key, must bedssev1
skip_tl
(bool): whether to skip transparency log entry verification (see 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 successfulerror
: 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
.
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:
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:
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/(.*)$"
platforms: ["linux/amd64"] # optional: restrict image platforms for matching policies (default: all)
rewrite: docker.io/library/$1
platforms
in the second rule above is optional and can be used to restrict the platforms for which the policy
is evaluated. If the platforms
field is not present, the policy will be applied to all platforms.
It's important to note that the platforms
field is a filter, and is applied before the pattern
field is processed, so both platforms
and pattern
need to match in order for the policy to be selected
(or the rewrite to be processed if present).
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:
- The rules are evaluated again using the rewritten repository name until a policy is found (in this case the first rule will match); and
- 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:
// 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:
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:
version: v1
kind: policy-mapping
policies:
- id: mydoi
description: my doi policy
files:
- path: "mypolicy.rego"
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: mydoi
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:
openssl pkey -in <public-key.pem> -pubin -outform DER | openssl dgst -sha256
attest
supports transparency logging for attestation signatures.
This serves two purposes:
- 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
- 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 instance.
Another transparency log can be used by creating an implementation of the tl.TL interface and using tl.WithTL
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 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 (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 when verifying attestations on an image.
The VSA can be signed and published to the registry using the signing functions mentioned in Signing Attestations.
{
"_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",
"downloadLocation": "https://docker.github.io/tuf-staging/targets/docker/d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0.policy.rego",
"digest": {
"sha256": "d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0"
}
},
"verificationResult": "PASSED",
"verifiedLevels": ["SLSA_BUILD_LEVEL_3"]
}
}
Full API reference can be found at pkg.go.dev/github.com/docker/attest.
- pkg/ => packages that are okay to import for other projects
- internal/ => packages that are only for project internal purposes
- scripts/ => build scripts
- test/ => data for use in tests
attest
uses Semantic Versioning.
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.