Skip to content

Commit

Permalink
Add support for multiple attestations at once
Browse files Browse the repository at this point in the history
Signed-off-by: Jon Johnson <jon.johnson@chainguard.dev>
  • Loading branch information
jonjohnsonjr committed Sep 19, 2023
1 parent 4fcd2e2 commit 5b7ff50
Show file tree
Hide file tree
Showing 4 changed files with 477 additions and 139 deletions.
28 changes: 25 additions & 3 deletions docs/resources/attest.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ This attests the provided image digest with cosign.
### Required

- `image` (String) The digest of the container image to attest.
- `predicate_type` (String) The in-toto predicate type of the claim being attested.

### Optional

- `fulcio_url` (String) Address of sigstore PKI server (default https://fulcio.sigstore.dev).
- `predicate` (String) The JSON body of the in-toto predicate's claim.
- `predicate_file` (Block List) The path and sha256 hex of the predicate to attest. (see [below for nested schema](#nestedblock--predicate_file))
- `predicate` (String, Deprecated) The JSON body of the in-toto predicate's claim.
- `predicate_file` (Block List, Deprecated) The path and sha256 hex of the predicate to attest. (see [below for nested schema](#nestedblock--predicate_file))
- `predicate_type` (String, Deprecated) The in-toto predicate type of the claim being attested.
- `predicates` (Block List) The path and sha256 hex of the predicate to attest. (see [below for nested schema](#nestedblock--predicates))
- `rekor_url` (String) Address of rekor transparency log server (default https://rekor.sigstore.dev).

### Read-Only
Expand All @@ -41,3 +42,24 @@ Optional:
- `sha256` (String) The sha256 hex hash of the predicate body.


<a id="nestedblock--predicates"></a>
### Nested Schema for `predicates`

Required:

- `predicate_type` (String) The in-toto predicate type of the claim being attested.

Optional:

- `predicate` (String) The JSON body of the in-toto predicate's claim.
- `predicate_file` (Block List) The path and sha256 hex of the predicate to attest. (see [below for nested schema](#nestedblock--predicates--predicate_file))

<a id="nestedblock--predicates--predicate_file"></a>
### Nested Schema for `predicates.predicate_file`

Optional:

- `path` (String) The path to a file containing the predicate to attest.
- `sha256` (String) The sha256 hex hash of the predicate body.


226 changes: 170 additions & 56 deletions internal/provider/resource_attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import (
"os"

"github.com/chainguard-dev/terraform-provider-cosign/internal/secant"
stypes "github.com/chainguard-dev/terraform-provider-cosign/internal/secant/types"
"github.com/chainguard-dev/terraform-provider-oci/pkg/validators"
"github.com/google/go-containerregistry/pkg/name"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
Expand Down Expand Up @@ -42,13 +44,23 @@ type AttestResource struct {
popts *ProviderOpts
}

type PredicateObject struct {
PredicateType types.String `tfsdk:"predicate_type"`
Predicate types.String `tfsdk:"predicate"`
PredicateFile types.List `tfsdk:"predicate_file"`
}

type AttestResourceModel struct {
Id types.String `tfsdk:"id"`
Image types.String `tfsdk:"image"`
Id types.String `tfsdk:"id"`
Image types.String `tfsdk:"image"`

// PredicateObject, left for backward compat.
PredicateType types.String `tfsdk:"predicate_type"`
Predicate types.String `tfsdk:"predicate"`
PredicateFile types.List `tfsdk:"predicate_file"`

Predicates types.List `tfsdk:"predicates"`

AttestedRef types.String `tfsdk:"attested_ref"`
FulcioURL types.String `tfsdk:"fulcio_url"`
RekorURL types.String `tfsdk:"rekor_url"`
Expand All @@ -59,11 +71,6 @@ func (r *AttestResource) Metadata(ctx context.Context, req resource.MetadataRequ
}

func (r *AttestResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
singlePredicate := stringvalidator.ExactlyOneOf(
path.MatchRoot("predicate"),
path.MatchRoot("predicate_file").AtListIndex(0).AtName("sha256"),
)

resp.Schema = schema.Schema{
MarkdownDescription: "This attests the provided image digest with cosign.",
Attributes: map[string]schema.Attribute{
Expand All @@ -85,8 +92,9 @@ func (r *AttestResource) Schema(ctx context.Context, req resource.SchemaRequest,
},
"predicate_type": schema.StringAttribute{
MarkdownDescription: "The in-toto predicate type of the claim being attested.",
Optional: false,
Required: true,
Optional: true,
Required: false,
DeprecationMessage: "Use predicates instead.",
Validators: []validator.String{validators.URLValidator{}},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
Expand All @@ -96,9 +104,9 @@ func (r *AttestResource) Schema(ctx context.Context, req resource.SchemaRequest,
MarkdownDescription: "The JSON body of the in-toto predicate's claim.",
Optional: true,
Required: false,
DeprecationMessage: "Use predicates instead.",
Validators: []validator.String{
validators.JSONValidator{},
singlePredicate,
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
Expand Down Expand Up @@ -127,14 +135,14 @@ func (r *AttestResource) Schema(ctx context.Context, req resource.SchemaRequest,
"predicate_file": schema.ListNestedBlock{
MarkdownDescription: "The path and sha256 hex of the predicate to attest.",
Validators: []validator.List{listvalidator.SizeBetween(1, 1)},
DeprecationMessage: "Use predicates instead.",
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"sha256": schema.StringAttribute{
MarkdownDescription: "The sha256 hex hash of the predicate body.",
Optional: true,
Required: false,
Validators: []validator.String{
singlePredicate,
stringvalidator.AlsoRequires(path.MatchRoot("predicate_file").AtListIndex(0).AtName("path")),
},
PlanModifiers: []planmodifier.String{
Expand All @@ -152,6 +160,71 @@ func (r *AttestResource) Schema(ctx context.Context, req resource.SchemaRequest,
},
},
},
"predicates": schema.ListNestedBlock{
MarkdownDescription: "The path and sha256 hex of the predicate to attest.",
Validators: []validator.List{listvalidator.SizeAtLeast(1)},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"predicate_type": schema.StringAttribute{
MarkdownDescription: "The in-toto predicate type of the claim being attested.",
Optional: false,
Required: true,
Validators: []validator.String{validators.URLValidator{}},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"predicate": schema.StringAttribute{
MarkdownDescription: "The JSON body of the in-toto predicate's claim.",
Optional: true,
Required: false,
Validators: []validator.String{
validators.JSONValidator{},
stringvalidator.ExactlyOneOf(
path.MatchRelative(),
path.MatchRelative().AtParent().AtName("predicate_file").AtListIndex(0).AtName("sha256"),
),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
Blocks: map[string]schema.Block{
"predicate_file": schema.ListNestedBlock{
MarkdownDescription: "The path and sha256 hex of the predicate to attest.",
Validators: []validator.List{listvalidator.SizeBetween(1, 1)},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"sha256": schema.StringAttribute{
MarkdownDescription: "The sha256 hex hash of the predicate body.",
Optional: true,
Required: false,
Validators: []validator.String{
stringvalidator.ExactlyOneOf(
path.MatchRelative(),
path.MatchRelative().AtParent().AtParent().AtParent().AtName("predicate"),
),
stringvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("path")),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"path": schema.StringAttribute{
MarkdownDescription: "The path to a file containing the predicate to attest.",
Optional: true,
Required: false,
Validators: []validator.String{
stringvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("sha256")),
},
},
},
},
},
},
},
},
},
}
}
Expand All @@ -170,8 +243,31 @@ func (r *AttestResource) Configure(ctx context.Context, req resource.ConfigureRe
r.popts = popts
}

func (r *AttestResource) doAttest(ctx context.Context, data *AttestResourceModel) (string, error, error) {
digest, err := name.NewDigest(data.Image.ValueString())
func toPredObjects(ctx context.Context, arm *AttestResourceModel) ([]PredicateObject, diag.Diagnostics) {
preds := []PredicateObject{}
diags := arm.Predicates.ElementsAs(ctx, &preds, false)
if obj := toPredObject(arm); obj != nil {
preds = append(preds, *obj)
}

return preds, diags
}

// TODO: Remove this when we deprecate top-level predicate.
func toPredObject(data *AttestResourceModel) *PredicateObject {
if data.Predicate.ValueString() != "" || len(data.PredicateFile.Elements()) > 0 {
return &PredicateObject{
PredicateType: data.PredicateType,
PredicateFile: data.PredicateFile,
Predicate: data.Predicate,
}
}

return nil
}

func (r *AttestResource) doAttest(ctx context.Context, arm *AttestResourceModel, preds []PredicateObject) (string, error, error) {
digest, err := name.NewDigest(arm.Image.ValueString())
if err != nil {
return "", nil, errors.New("unable to parse image digest")
}
Expand All @@ -183,66 +279,72 @@ func (r *AttestResource) doAttest(ctx context.Context, data *AttestResourceModel
return digest.String(), errors.New("no ambient credentials are available to attest with, skipping attesting"), nil
}

// Write the attestation to a temporary file.
var path string
switch {
// Write the predicate to a file to pass to attest.
case data.Predicate.ValueString() != "":
file, err := os.CreateTemp("", "")
if err != nil {
return "", nil, err
statements := []*stypes.Statement{}

for _, data := range preds {
// Write the attestation to a temporary file.
var path string
switch {
// Write the predicate to a file to pass to attest.
case data.Predicate.ValueString() != "":
file, err := os.CreateTemp("", "")
if err != nil {
return "", nil, err
}
defer os.Remove(file.Name())
if _, err := file.WriteString(data.Predicate.ValueString()); err != nil {
return "", nil, err
}
if err := file.Close(); err != nil {
return "", nil, err
}
path = file.Name()

case len(data.PredicateFile.Elements()) > 0:
attrs := data.PredicateFile.Elements()[0].(basetypes.ObjectValue).Attributes()
path = attrs["path"].(basetypes.StringValue).ValueString()
expectedHash := attrs["sha256"].(basetypes.StringValue).ValueString()

contents, err := os.ReadFile(path)
if err != nil {
return "", nil, err
}
rawHash := sha256.Sum256(contents)
if got, want := hex.EncodeToString(rawHash[:]), expectedHash; got != want {
return "", nil, fmt.Errorf("sha256(%q) = %s, expected %s", path, got, want)
}

default:
return "", nil, errors.New("one of predicate or predicate_file must be specified")
}
defer os.Remove(file.Name())
if _, err := file.WriteString(data.Predicate.ValueString()); err != nil {
return "", nil, err
}
if err := file.Close(); err != nil {
return "", nil, err
}
path = file.Name()

case len(data.PredicateFile.Elements()) > 0:
attrs := data.PredicateFile.Elements()[0].(basetypes.ObjectValue).Attributes()
path = attrs["path"].(basetypes.StringValue).ValueString()
expectedHash := attrs["sha256"].(basetypes.StringValue).ValueString()

contents, err := os.ReadFile(path)
predicate, err := os.Open(path)
if err != nil {
return "", nil, err
}
rawHash := sha256.Sum256(contents)
if got, want := hex.EncodeToString(rawHash[:]), expectedHash; got != want {
return "", nil, fmt.Errorf("sha256(%q) = %s, expected %s", path, got, want)
return "", nil, fmt.Errorf("open %q: %w", path, err)
}

default:
return "", nil, errors.New("one of predicate or predicate_file must be specified")
}

predicate, err := os.Open(path)
if err != nil {
return "", nil, fmt.Errorf("open %q: %w", path, err)
}
stmt, err := secant.NewStatement(digest, predicate, data.PredicateType.ValueString())
if err != nil {
return "", nil, fmt.Errorf("creating attestation statement: %w", err)
}

stmt, err := secant.NewStatement(digest, predicate, data.PredicateType.ValueString())
if err != nil {
return "", nil, fmt.Errorf("creating attestation statement: %w", err)
statements = append(statements, stmt)
}

sv, err := r.popts.signerVerifier(data.FulcioURL.ValueString())
sv, err := r.popts.signerVerifier(arm.FulcioURL.ValueString())
if err != nil {
return "", nil, fmt.Errorf("creating signer: %w", err)
}

rekorClient, err := r.popts.rekorClient(data.RekorURL.ValueString())
rekorClient, err := r.popts.rekorClient(arm.RekorURL.ValueString())
if err != nil {
return "", nil, fmt.Errorf("creating rekor client: %w", err)
}

ctx, cancel := context.WithTimeout(ctx, options.DefaultTimeout)
defer cancel()

if err := secant.Attest(ctx, stmt, sv, rekorClient, r.popts.ropts); err != nil {
if err := secant.Attest(ctx, statements, sv, rekorClient, r.popts.ropts); err != nil {
return "", nil, fmt.Errorf("unable to sign image: %w", err)
}

Expand All @@ -256,7 +358,13 @@ func (r *AttestResource) Create(ctx context.Context, req resource.CreateRequest,
return
}

digest, warning, err := r.doAttest(ctx, data)
preds, diags := toPredObjects(ctx, data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

digest, warning, err := r.doAttest(ctx, data, preds)
if err != nil {
resp.Diagnostics.AddError("error while attesting", err.Error())
return
Expand Down Expand Up @@ -298,7 +406,13 @@ func (r *AttestResource) Update(ctx context.Context, req resource.UpdateRequest,
return
}

digest, warning, err := r.doAttest(ctx, data)
preds, diags := toPredObjects(ctx, data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

digest, warning, err := r.doAttest(ctx, data, preds)
if err != nil {
resp.Diagnostics.AddError("error while attesting", err.Error())
return
Expand Down
Loading

0 comments on commit 5b7ff50

Please sign in to comment.