Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,22 @@ The following checks were performed on these signatures:
{"Critical":{"Identity":{"docker-reference":""},"Image":{"Docker-manifest-digest":"sha256:87ef60f558bad79beea6425a3b28989f01dd417164150ab3baab98dcbf04def8"},"Type":"cosign container image signature"},"Optional":null}
```

### Sign a container offline with a private key

To sign without contacting Sigstore network services, use the `--offline` flag with a private key:

```shell
cosign sign --key cosign.key --offline $IMAGE
```

Similarly, to sign a local file/blob offline:

```shell
cosign sign-blob --key cosign.key --offline --bundle artifact.sigstore.json artifact
```

**Note:** Offline signing requires a private key. When signing offline, key usage is not recorded in a transparency log and therefore not auditable.

### Verify a container in an air-gapped environment

**Note:** This section is out of date.
Expand Down
13 changes: 9 additions & 4 deletions cmd/cosign/cli/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ func Attest() *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := signcommon.ValidateSigningOptions(cmd.Context(), o.Offline,
o.Rekor.URL, o.Fulcio.URL, o.OIDC.Issuer, o.TSAServerURL,
o.TlogUpload, o.NewBundleFormat, "", o.Key, o.IssueCertificate,
"", "", "", "", "", ""); err != nil {
return err
}

oidcClientSecret, err := o.OIDC.ClientSecret()
if err != nil {
return err
Expand All @@ -94,6 +101,7 @@ func Attest() *cobra.Command {
OIDCClientID: o.OIDC.ClientID,
OIDCClientSecret: oidcClientSecret,
OIDCRedirectURL: o.OIDC.RedirectURL,
OIDCDisableProviders: o.OIDC.DisableAmbientProviders,
OIDCProvider: o.OIDC.Provider,
SkipConfirmation: o.SkipConfirmation,
TSAClientCACert: o.TSAClientCACert,
Expand All @@ -105,10 +113,7 @@ func Attest() *cobra.Command {
BundlePath: o.BundlePath,
NewBundleFormat: o.NewBundleFormat,
}
if err := signcommon.LoadTrustedMaterialAndSigningConfig(cmd.Context(), &ko, o.UseSigningConfig, o.SigningConfigPath,
o.Rekor.URL, o.Fulcio.URL, o.OIDC.Issuer, o.TSAServerURL, o.TrustedRootPath, o.TlogUpload,
o.NewBundleFormat, "", o.Key, o.IssueCertificate,
"", "", "", "", "", ""); err != nil {
if err := signcommon.LoadTrustedMaterialAndSigningConfig(cmd.Context(), &ko, o.Offline, o.SigningConfigPath, o.TrustedRootPath); err != nil {
return err
}

Expand Down
10 changes: 7 additions & 3 deletions cmd/cosign/cli/attest/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,13 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
if err != nil {
return fmt.Errorf("creating signing config: %w", err)
}
if !c.TlogUpload {
c.SigningConfig = c.SigningConfig.WithRekorLogURLs()
}
}
shouldUpload := signcommon.ShouldUploadToTlog(ctx, c.KeyOpts, digest, c.TlogUpload)
if !shouldUpload {
c.SigningConfig = c.SigningConfig.WithRekorLogURLs()
}
if err := signcommon.ConfirmPrivacyStatement(ctx, c.KeyOpts, shouldUpload); err != nil {
return err
}

bundleBytes, pubKey, hashAlgProto, err := signcommon.NewAttestationBundle(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, bundleOpts, c.SigningConfig, c.TrustedMaterial)
Expand Down
105 changes: 12 additions & 93 deletions cmd/cosign/cli/attest/attest_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ import (
"github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon"
"github.com/sigstore/cosign/v3/internal/ui"
"github.com/sigstore/cosign/v3/pkg/cosign/attestation"
cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle"
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
"github.com/sigstore/sigstore/pkg/signature"
"google.golang.org/protobuf/encoding/protojson"
)

// nolint
Expand All @@ -51,14 +48,7 @@ type AttestBlobCommand struct {
PredicatePath string
PredicateType string

TlogUpload bool
Timeout time.Duration

OutputSignature string
OutputAttestation string
OutputCertificate string

RekorEntryType string
Timeout time.Duration
}

// nolint
Expand All @@ -72,10 +62,6 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
return fmt.Errorf("one of --predicate or --statement must be set")
}

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

if c.Timeout != 0 {
var cancelFn context.CancelFunc
ctx, cancelFn = context.WithTimeout(ctx, c.Timeout)
Expand Down Expand Up @@ -147,92 +133,25 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
}

if c.SigningConfig == nil {
var err error
c.SigningConfig, err = signcommon.NewSigningConfigFromKeyOpts(c.KeyOpts)
if err != nil {
return fmt.Errorf("creating signing config: %w", err)
}
if !c.TlogUpload {
c.SigningConfig = c.SigningConfig.WithRekorLogURLs()
}
}

bundleBytes, _, _, err := signcommon.NewAttestationBundle(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, bundleOpts, c.SigningConfig, c.TrustedMaterial)
if err != nil {
return fmt.Errorf("creating bundle: %w", err)
}

if c.NewBundleFormat {
if err := os.WriteFile(c.BundlePath, bundleBytes, 0600); err != nil {
return fmt.Errorf("create bundle file: %w", err)
}
ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath)
return nil
c.SigningConfig = signcommon.NewEmptySigningConfig()
}

var pb protobundle.Bundle
if err := protojson.Unmarshal(bundleBytes, &pb); err != nil {
return fmt.Errorf("unmarshalling bundle: %w", err)
shouldUpload := signcommon.ShouldUploadToTlog(ctx, c.KeyOpts, nil, len(c.SigningConfig.RekorLogURLs()) > 0)
if !shouldUpload {
c.SigningConfig = c.SigningConfig.WithRekorLogURLs()
}

bundleComponents, err := signcommon.ExtractComponentsFromProtoBundle(&pb)
if err != nil {
if err := signcommon.ConfirmPrivacyStatement(ctx, c.KeyOpts, shouldUpload); err != nil {
return err
}

if c.BundlePath != "" {
contents, err := signcommon.NewLegacyBundleFromProtoBundleComponents(bundleComponents)
if err != nil {
return fmt.Errorf("creating legacy bundle: %w", err)
}

if err := os.WriteFile(c.BundlePath, contents, 0600); err != nil {
return fmt.Errorf("create bundle file: %w", err)
}
ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath)
}

if c.OutputSignature != "" {
if err := os.WriteFile(c.OutputSignature, bundleComponents.Signature, 0600); err != nil {
return fmt.Errorf("create signature file: %w", err)
}
fmt.Fprintf(os.Stderr, "Signature written in %s\n", c.OutputSignature)
} else {
fmt.Fprintln(os.Stdout, string(bundleComponents.Signature))
}

if c.OutputAttestation != "" {
if err := os.WriteFile(c.OutputAttestation, payload, 0600); err != nil {
return fmt.Errorf("create attestation file: %w", err)
}
fmt.Fprintf(os.Stderr, "Attestation written in %s\n", c.OutputAttestation)
}

if c.OutputCertificate != "" {
if len(bundleComponents.Certificates) == 0 {
return fmt.Errorf("no certificate found in bundle")
}
certPem, _ := signcommon.EncodeCertificatesToPEM(bundleComponents.Certificates)
if err := os.WriteFile(c.OutputCertificate, certPem, 0600); err != nil {
return fmt.Errorf("create certificate file: %w", err)
}
fmt.Fprintln(os.Stderr, "Certificate written to file ", c.OutputCertificate)
bundleBytes, _, _, err := signcommon.NewAttestationBundle(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, bundleOpts, c.SigningConfig, c.TrustedMaterial)
if err != nil {
return fmt.Errorf("creating bundle: %w", err)
}

if c.RFC3161TimestampPath != "" {
if len(bundleComponents.RFC3161Timestamps) == 0 {
return fmt.Errorf("no RFC3161 timestamp found in bundle")
}
legacyTimestamp := cbundle.TimestampToRFC3161Timestamp(bundleComponents.RFC3161Timestamps[0].GetSignedTimestamp())
ts, err := json.Marshal(legacyTimestamp)
if err != nil {
return fmt.Errorf("marshalling timestamp: %w", err)
}
if err := os.WriteFile(c.RFC3161TimestampPath, ts, 0600); err != nil {
return fmt.Errorf("create timestamp file: %w", err)
}
fmt.Fprintln(os.Stderr, "Timestamp wrote in the file ", c.RFC3161TimestampPath)
if err := os.WriteFile(c.BundlePath, bundleBytes, 0600); err != nil {
return fmt.Errorf("create bundle file: %w", err)
}
ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath)

return nil
}
Expand Down
89 changes: 27 additions & 62 deletions cmd/cosign/cli/attest/attest_blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,6 @@ func TestAttestBlobCmdLocalKeyAndCert(t *testing.T) {
certref: subCertPem,
errString: "public key in certificate does not match the provided public key",
},
{
name: "cert chain matches key",
keyref: keyRef,
certref: certRef,
certchainref: subCertPem,
},
{
name: "cert chain partial",
keyref: keyRef,
Expand All @@ -175,18 +169,13 @@ func TestAttestBlobCmdLocalKeyAndCert(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
keyOpts := options.KeyOpts{KeyRef: tc.keyref}
if tc.newBundle {
keyOpts.NewBundleFormat = true
keyOpts.BundlePath = filepath.Join(td, "output.bundle")
}
keyOpts := options.KeyOpts{KeyRef: tc.keyref, BundlePath: filepath.Join(td, "output.bundle")}
at := AttestBlobCommand{
KeyOpts: keyOpts,
CertPath: tc.certref,
CertChainPath: tc.certchainref,
PredicatePath: predicatePath,
PredicateType: predicateType,
RekorEntryType: "dsse",
KeyOpts: keyOpts,
CertPath: tc.certref,
CertChainPath: tc.certchainref,
PredicatePath: predicatePath,
PredicateType: predicateType,
}
err := at.Exec(ctx, blob)
if err != nil {
Expand Down Expand Up @@ -228,25 +217,33 @@ func TestAttestBlob(t *testing.T) {

for predicateType, predicatePath := range predicates {
t.Run(predicateType, func(t *testing.T) {
dssePath := filepath.Join(td, "dsse.intoto.jsonl")
bundlePath := filepath.Join(td, "bundle.json")
keyOpts := options.KeyOpts{KeyRef: keyRef, BundlePath: bundlePath}
at := AttestBlobCommand{
KeyOpts: options.KeyOpts{KeyRef: keyRef},
PredicatePath: predicatePath,
PredicateType: predicateType,
OutputSignature: dssePath,
RekorEntryType: "dsse",
KeyOpts: keyOpts,
PredicatePath: predicatePath,
PredicateType: predicateType,
}
err := at.Exec(ctx, blobPath)
if err != nil {
t.Fatal(err)
}

// Load the attestation.
dsseBytes, _ := os.ReadFile(dssePath)
env := &ssldsse.Envelope{}
if err := json.Unmarshal(dsseBytes, env); err != nil {
// Load the attestation bundle.
bundleBytes, err := os.ReadFile(bundlePath)
if err != nil {
t.Fatal(err)
}
var bundleJSON struct {
DsseEnvelope *ssldsse.Envelope `json:"dsseEnvelope"`
}
if err := json.Unmarshal(bundleBytes, &bundleJSON); err != nil {
t.Fatal(err)
}
env := bundleJSON.DsseEnvelope
if env == nil {
t.Fatal("expected dsse envelope in bundle")
}

if len(env.Signatures) != 1 {
t.Fatalf("expected 1 signature, got %d", len(env.Signatures))
Expand Down Expand Up @@ -284,38 +281,6 @@ func TestAttestBlob(t *testing.T) {
}
}

func TestBadRekorEntryType(t *testing.T) {
ctx := context.Background()
td := t.TempDir()

keys, _ := cosign.GenerateKeyPair(nil)
keyRef := writeFile(t, td, string(keys.PrivateBytes), "key.pem")

blob := []byte("foo")
blobPath := writeFile(t, td, string(blob), "foo.txt")

predicates := map[string]string{}
predicates["slsaprovenance"] = makeSLSA02PredicateFile(t, td)
predicates["slsaprovenance1"] = makeSLSA1PredicateFile(t, td)

for predicateType, predicatePath := range predicates {
t.Run(predicateType, func(t *testing.T) {
dssePath := filepath.Join(td, "dsse.intoto.jsonl")
at := AttestBlobCommand{
KeyOpts: options.KeyOpts{KeyRef: keyRef},
PredicatePath: predicatePath,
PredicateType: predicateType,
OutputSignature: dssePath,
RekorEntryType: "badvalue",
}
err := at.Exec(ctx, blobPath)
if err == nil || err.Error() != "unknown value for rekor-entry-type" {
t.Fatal("expected an error due to unknown rekor entry type")
}
})
}
}

func TestStatementPath(t *testing.T) {
ctx := context.Background()
td := t.TempDir()
Expand All @@ -340,10 +305,10 @@ func TestStatementPath(t *testing.T) {
}`
statementPath := writeFile(t, td, statement, "statement.json")

keyOpts := options.KeyOpts{KeyRef: keyRef, BundlePath: filepath.Join(td, "bundle.json")}
at := AttestBlobCommand{
KeyOpts: options.KeyOpts{KeyRef: keyRef},
StatementPath: statementPath,
RekorEntryType: "dsse",
KeyOpts: keyOpts,
StatementPath: statementPath,
}
err := at.Exec(ctx, "")
assert.NoError(t, err)
Expand Down
Loading