Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fc7349d

Browse files
committedFeb 4, 2025··
Add support for attesting with complete statement
Signed-off-by: Cody Soyland <codysoyland@github.com>
1 parent 37740f0 commit fc7349d

File tree

8 files changed

+176
-80
lines changed

8 files changed

+176
-80
lines changed
 

‎cmd/cosign/cli/attest.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,11 @@ func Attest() *cobra.Command {
6363
# attach an attestation to a container image and honor the creation timestamp of the signature
6464
cosign attest --predicate <FILE> --type <TYPE> --key cosign.key --record-creation-timestamp <IMAGE>`,
6565

66-
Args: cobra.MinimumNArgs(1),
6766
PersistentPreRun: options.BindViper,
6867
RunE: func(cmd *cobra.Command, args []string) error {
68+
if o.Predicate.Statement == "" && len(args) != 1 {
69+
return cobra.ExactArgs(1)(cmd, args)
70+
}
6971
oidcClientSecret, err := o.OIDC.ClientSecret()
7072
if err != nil {
7173
return err
@@ -94,6 +96,7 @@ func Attest() *cobra.Command {
9496
CertPath: o.Cert,
9597
CertChainPath: o.CertChain,
9698
NoUpload: o.NoUpload,
99+
StatementPath: o.Predicate.Statement,
97100
PredicatePath: o.Predicate.Path,
98101
PredicateType: o.Predicate.Type,
99102
Replace: o.Replace,

‎cmd/cosign/cli/attest/attest.go

+48-29
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
_ "crypto/sha256" // for `crypto.SHA256`
2222
"encoding/json"
2323
"fmt"
24+
"io"
2425
"os"
2526
"time"
2627

@@ -74,6 +75,7 @@ type AttestCommand struct {
7475
CertPath string
7576
CertChainPath string
7677
NoUpload bool
78+
StatementPath string
7779
PredicatePath string
7880
PredicateType string
7981
Replace bool
@@ -91,18 +93,14 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
9193
return &options.KeyParseError{}
9294
}
9395

94-
if c.PredicatePath == "" {
95-
return fmt.Errorf("predicate cannot be empty")
96+
if options.NOf(c.PredicatePath, c.StatementPath) != 1 {
97+
return fmt.Errorf("one of --predicate or --statement must be set")
9698
}
9799

98100
if c.RekorEntryType != "dsse" && c.RekorEntryType != "intoto" {
99101
return fmt.Errorf("unknown value for rekor-entry-type")
100102
}
101103

102-
predicateURI, err := options.ParsePredicateType(c.PredicateType)
103-
if err != nil {
104-
return err
105-
}
106104
ref, err := name.ParseReference(imageRef, c.NameOptions()...)
107105
if err != nil {
108106
return fmt.Errorf("parsing reference: %w", err)
@@ -140,26 +138,52 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
140138
wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType)
141139
dd := cremote.NewDupeDetector(sv)
142140

143-
predicate, err := predicateReader(c.PredicatePath)
144-
if err != nil {
145-
return fmt.Errorf("getting predicate reader: %w", err)
146-
}
147-
defer predicate.Close()
141+
var payload []byte
142+
var predicateType string
148143

149-
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
150-
Predicate: predicate,
151-
Type: c.PredicateType,
152-
Digest: h.Hex,
153-
Repo: digest.Repository.String(),
154-
})
155-
if err != nil {
156-
return err
157-
}
144+
if c.StatementPath != "" {
145+
fmt.Fprintln(os.Stderr, "Using statement from:", c.StatementPath)
146+
statement, err := predicateReader(c.StatementPath)
147+
if err != nil {
148+
return fmt.Errorf("getting statement reader: %w", err)
149+
}
150+
defer statement.Close()
151+
payload, err = io.ReadAll(statement)
152+
if err != nil {
153+
return fmt.Errorf("could not read statement: %w", err)
154+
}
155+
predicateType, err = validateStatement(payload)
156+
if err != nil {
157+
return fmt.Errorf("invalid statement: %w", err)
158+
}
159+
} else {
160+
predicateType, err = options.ParsePredicateType(c.PredicateType)
161+
if err != nil {
162+
return err
163+
}
158164

159-
payload, err := json.Marshal(sh)
160-
if err != nil {
161-
return err
165+
predicate, err := predicateReader(c.PredicatePath)
166+
if err != nil {
167+
return fmt.Errorf("getting predicate reader: %w", err)
168+
}
169+
defer predicate.Close()
170+
171+
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
172+
Predicate: predicate,
173+
Type: c.PredicateType,
174+
Digest: h.Hex,
175+
Repo: digest.Repository.String(),
176+
})
177+
if err != nil {
178+
return err
179+
}
180+
181+
payload, err = json.Marshal(sh)
182+
if err != nil {
183+
return err
184+
}
162185
}
186+
163187
signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx))
164188
if err != nil {
165189
return fmt.Errorf("signing: %w", err)
@@ -192,11 +216,6 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
192216
opts = append(opts, static.WithRFC3161Timestamp(bundle))
193217
}
194218

195-
predicateType, err := options.ParsePredicateType(c.PredicateType)
196-
if err != nil {
197-
return err
198-
}
199-
200219
predicateTypeAnnotation := map[string]string{
201220
"predicateType": predicateType,
202221
}
@@ -238,7 +257,7 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
238257
}
239258

240259
if c.Replace {
241-
ro := cremote.NewReplaceOp(predicateURI)
260+
ro := cremote.NewReplaceOp(predicateType)
242261
signOpts = append(signOpts, mutate.WithReplaceOp(ro))
243262
}
244263

‎cmd/cosign/cli/attest/attest_blob.go

+72-46
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333

3434
"errors"
3535

36+
"github.com/in-toto/in-toto-golang/in_toto"
3637
"github.com/secure-systems-lab/go-securesystemslib/dsse"
3738
"google.golang.org/protobuf/encoding/protojson"
3839

@@ -62,6 +63,7 @@ type AttestBlobCommand struct {
6263

6364
ArtifactHash string
6465

66+
StatementPath string
6567
PredicatePath string
6668
PredicateType string
6769

@@ -82,8 +84,8 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
8284
return &options.KeyParseError{}
8385
}
8486

85-
if c.PredicatePath == "" {
86-
return fmt.Errorf("predicate cannot be empty")
87+
if options.NOf(c.PredicatePath, c.StatementPath) != 1 {
88+
return fmt.Errorf("one of --predicate or --statement must be set")
8789
}
8890

8991
if c.RekorEntryType != "dsse" && c.RekorEntryType != "intoto" {
@@ -100,38 +102,6 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
100102
return errors.New("expected either new bundle or an rfc3161-timestamp path when using a TSA server")
101103
}
102104

103-
var artifact []byte
104-
var hexDigest string
105-
var err error
106-
107-
if c.ArtifactHash == "" {
108-
if artifactPath == "-" {
109-
artifact, err = io.ReadAll(os.Stdin)
110-
} else {
111-
fmt.Fprintln(os.Stderr, "Using payload from:", artifactPath)
112-
artifact, err = os.ReadFile(filepath.Clean(artifactPath))
113-
}
114-
if err != nil {
115-
return err
116-
}
117-
}
118-
119-
if c.ArtifactHash == "" {
120-
digest, _, err := signature.ComputeDigestForSigning(bytes.NewReader(artifact), crypto.SHA256, []crypto.Hash{crypto.SHA256, crypto.SHA384})
121-
if err != nil {
122-
return err
123-
}
124-
hexDigest = strings.ToLower(hex.EncodeToString(digest))
125-
} else {
126-
hexDigest = c.ArtifactHash
127-
}
128-
129-
predicate, err := predicateReader(c.PredicatePath)
130-
if err != nil {
131-
return fmt.Errorf("getting predicate reader: %w", err)
132-
}
133-
defer predicate.Close()
134-
135105
sv, err := sign.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts)
136106
if err != nil {
137107
return fmt.Errorf("getting signer: %w", err)
@@ -141,19 +111,67 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
141111

142112
base := path.Base(artifactPath)
143113

144-
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
145-
Predicate: predicate,
146-
Type: c.PredicateType,
147-
Digest: hexDigest,
148-
Repo: base,
149-
})
150-
if err != nil {
151-
return err
152-
}
114+
var payload []byte
153115

154-
payload, err := json.Marshal(sh)
155-
if err != nil {
156-
return err
116+
fmt.Println("Using statement from:", c.StatementPath)
117+
118+
if c.StatementPath != "" {
119+
fmt.Fprintln(os.Stderr, "Using statement from:", c.StatementPath)
120+
statement, err := predicateReader(c.StatementPath)
121+
if err != nil {
122+
return fmt.Errorf("getting statement reader: %w", err)
123+
}
124+
defer statement.Close()
125+
payload, err = io.ReadAll(statement)
126+
if err != nil {
127+
return fmt.Errorf("could not read statement: %w", err)
128+
}
129+
if _, err := validateStatement(payload); err != nil {
130+
return fmt.Errorf("invalid statement: %w", err)
131+
}
132+
133+
} else {
134+
var artifact []byte
135+
var hexDigest string
136+
if c.ArtifactHash == "" {
137+
if artifactPath == "-" {
138+
artifact, err = io.ReadAll(os.Stdin)
139+
} else {
140+
fmt.Fprintln(os.Stderr, "Using payload from:", artifactPath)
141+
artifact, err = os.ReadFile(filepath.Clean(artifactPath))
142+
}
143+
if err != nil {
144+
return err
145+
}
146+
}
147+
148+
if c.ArtifactHash == "" {
149+
digest, _, err := signature.ComputeDigestForSigning(bytes.NewReader(artifact), crypto.SHA256, []crypto.Hash{crypto.SHA256, crypto.SHA384})
150+
if err != nil {
151+
return err
152+
}
153+
hexDigest = strings.ToLower(hex.EncodeToString(digest))
154+
} else {
155+
hexDigest = c.ArtifactHash
156+
}
157+
predicate, err := predicateReader(c.PredicatePath)
158+
if err != nil {
159+
return fmt.Errorf("getting predicate reader: %w", err)
160+
}
161+
defer predicate.Close()
162+
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
163+
Predicate: predicate,
164+
Type: c.PredicateType,
165+
Digest: hexDigest,
166+
Repo: base,
167+
})
168+
if err != nil {
169+
return err
170+
}
171+
payload, err = json.Marshal(sh)
172+
if err != nil {
173+
return err
174+
}
157175
}
158176

159177
sig, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx))
@@ -367,3 +385,11 @@ func makeNewBundle(sv *sign.SignerVerifier, rekorEntry *models.LogEntryAnon, pay
367385

368386
return contents, nil
369387
}
388+
389+
func validateStatement(payload []byte) (string, error) {
390+
var statement *in_toto.Statement
391+
if err := json.Unmarshal(payload, &statement); err != nil {
392+
return "", fmt.Errorf("invalid statement: %w", err)
393+
}
394+
return statement.PredicateType, nil
395+
}

‎cmd/cosign/cli/attest/attest_blob_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"github.com/sigstore/cosign/v2/test"
4040
"github.com/sigstore/sigstore/pkg/signature"
4141
"github.com/sigstore/sigstore/pkg/signature/dsse"
42+
"github.com/stretchr/testify/assert"
4243
)
4344

4445
// TestAttestBlobCmdLocalKeyAndSk verifies the AttestBlobCmd returns an error
@@ -307,3 +308,36 @@ func TestBadRekorEntryType(t *testing.T) {
307308
})
308309
}
309310
}
311+
312+
func TestStatementPath(t *testing.T) {
313+
ctx := context.Background()
314+
td := t.TempDir()
315+
316+
keys, _ := cosign.GenerateKeyPair(nil)
317+
keyRef := writeFile(t, td, string(keys.PrivateBytes), "key.pem")
318+
319+
statement := `{
320+
"_type": "https://in-toto.io/Statement/v1",
321+
"subject": [
322+
{
323+
"name": "foo",
324+
"digest": {
325+
"sha256": "deadbeef"
326+
}
327+
}
328+
],
329+
"predicateType": "https://example.com/CustomPredicate/v1",
330+
"predicate": {
331+
"foo": "bar"
332+
}
333+
}`
334+
statementPath := writeFile(t, td, statement, "statement.json")
335+
336+
at := AttestBlobCommand{
337+
KeyOpts: options.KeyOpts{KeyRef: keyRef},
338+
StatementPath: statementPath,
339+
RekorEntryType: "dsse",
340+
}
341+
err := at.Exec(ctx, "")
342+
assert.NoError(t, err)
343+
}

‎cmd/cosign/cli/attest_blob.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ func AttestBlob() *cobra.Command {
4747
# supply attestation via stdin
4848
echo <PAYLOAD> | cosign attest-blob --predicate - --yes`,
4949

50-
Args: cobra.ExactArgs(1),
5150
PersistentPreRun: options.BindViper,
5251
RunE: func(cmd *cobra.Command, args []string) error {
52+
if o.Predicate.Statement == "" && len(args) != 1 {
53+
return cobra.ExactArgs(1)(cmd, args)
54+
}
5355
oidcClientSecret, err := o.OIDC.ClientSecret()
5456
if err != nil {
5557
return err
@@ -83,13 +85,18 @@ func AttestBlob() *cobra.Command {
8385
TlogUpload: o.TlogUpload,
8486
PredicateType: o.Predicate.Type,
8587
PredicatePath: o.Predicate.Path,
88+
StatementPath: o.Predicate.Statement,
8689
OutputSignature: o.OutputSignature,
8790
OutputAttestation: o.OutputAttestation,
8891
OutputCertificate: o.OutputCertificate,
8992
Timeout: ro.Timeout,
9093
RekorEntryType: o.RekorEntryType,
9194
}
92-
return v.Exec(cmd.Context(), args[0])
95+
var artifactPath string
96+
if len(args) == 1 {
97+
artifactPath = args[0]
98+
}
99+
return v.Exec(cmd.Context(), artifactPath)
93100
},
94101
}
95102
o.AddFlags(cmd)

‎cmd/cosign/cli/options/predicate.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ func ParsePredicateType(t string) (string, error) {
8383
// PredicateLocalOptions is the wrapper for predicate related options.
8484
type PredicateLocalOptions struct {
8585
PredicateOptions
86-
Path string
86+
Path string
87+
Statement string
8788
}
8889

8990
var _ Interface = (*PredicateLocalOptions)(nil)
@@ -94,7 +95,11 @@ func (o *PredicateLocalOptions) AddFlags(cmd *cobra.Command) {
9495

9596
cmd.Flags().StringVar(&o.Path, "predicate", "",
9697
"path to the predicate file.")
97-
_ = cmd.MarkFlagRequired("predicate")
98+
99+
cmd.Flags().StringVar(&o.Statement, "statement", "",
100+
"path to the statement file.")
101+
102+
cmd.MarkFlagsOneRequired("predicate", "statement")
98103
}
99104

100105
// PredicateRemoteOptions is the wrapper for remote predicate related options.

‎doc/cosign_attest-blob.md

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎doc/cosign_attest.md

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.