Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 28 additions & 18 deletions pkg/chains/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/hashicorp/go-multierror"
intoto "github.com/in-toto/attestation/go/v1"
cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle"
"github.com/tektoncd/chains/pkg/artifacts"
"github.com/tektoncd/chains/pkg/chains/annotations"
"github.com/tektoncd/chains/pkg/chains/formats"
Expand Down Expand Up @@ -186,6 +187,32 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject)
}
measureMetrics(ctx, metrics.SignedMessagesCount, o.Recorder)

// Upload to Rekor before storage so the bundle is available for OCI attestation annotations.
var rekorBundle *cbundle.RekorBundle
if shouldUploadTlog(cfg, tektonObj) {
rekorClient, err := getRekor(cfg.Transparency.URL)
if err != nil {
return err
}

entry, err := rekorClient.UploadTlog(ctx, signer, signature, rawPayload, signer.Cert(), string(payloadFormat))
if err != nil {
logger.Warnf("error uploading entry to tlog: %v", err)
o.recordError(ctx, signableType, metrics.TlogError)
merr = multierror.Append(merr, err)
} else {
logger.Infof("Uploaded entry to %s with index %d", cfg.Transparency.URL, *entry.LogIndex)
extraAnnotations[annotations.ChainsTransparencyAnnotation] = fmt.Sprintf("%s/api/v1/log/entries?logIndex=%d", cfg.Transparency.URL, *entry.LogIndex)
rekorBundle = cbundle.EntryToBundle(entry)
if rekorBundle != nil {
logger.Infof("Resolved Rekor bundle for offline verification (logIndex: %d)", rekorBundle.Payload.LogIndex)
} else {
logger.Warn("Rekor entry missing verification data, skipping bundle for offline verification")
}
measureMetrics(ctx, metrics.PayloadUploadedCount, o.Recorder)
}
}

// Now store those!
for _, backend := range sets.List[string](signableType.StorageBackend(cfg)) {
b, ok := o.Backends[backend]
Expand All @@ -203,6 +230,7 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject)
Cert: signer.Cert(),
Chain: signer.Chain(),
PayloadFormat: payloadFormat,
RekorBundle: rekorBundle,
}
if err := b.StorePayload(ctx, tektonObj, rawPayload, string(signature), storageOpts); err != nil {
logger.Error(err)
Expand All @@ -213,24 +241,6 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject)
}
}

if shouldUploadTlog(cfg, tektonObj) {
rekorClient, err := getRekor(cfg.Transparency.URL)
if err != nil {
return err
}

entry, err := rekorClient.UploadTlog(ctx, signer, signature, rawPayload, signer.Cert(), string(payloadFormat))
if err != nil {
logger.Warnf("error uploading entry to tlog: %v", err)
o.recordError(ctx, signableType, metrics.TlogError)
merr = multierror.Append(merr, err)
} else {
logger.Infof("Uploaded entry to %s with index %d", cfg.Transparency.URL, *entry.LogIndex)
extraAnnotations[annotations.ChainsTransparencyAnnotation] = fmt.Sprintf("%s/api/v1/log/entries?logIndex=%d", cfg.Transparency.URL, *entry.LogIndex)
measureMetrics(ctx, metrics.PayloadUploadedCount, o.Recorder)
}
}

}
if merr.ErrorOrNil() != nil {
if retryErr := annotations.HandleRetry(ctx, tektonObj, o.Pipelineclientset, extraAnnotations); retryErr != nil {
Expand Down
3 changes: 3 additions & 0 deletions pkg/chains/signing/iface.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ limitations under the License.
package signing

import (
"github.com/sigstore/cosign/v2/pkg/cosign/bundle"
"github.com/sigstore/sigstore/pkg/signature"
)

Expand Down Expand Up @@ -41,4 +42,6 @@ type Bundle struct {
Cert []byte
// Cert is an optional PEM encoded x509 certificate chain, if one was used for signing.
Chain []byte
// RekorBundle is an optional Rekor transparency log bundle, populated when transparency is enabled.
RekorBundle *bundle.RekorBundle
}
158 changes: 157 additions & 1 deletion pkg/chains/signing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,152 @@ func TestSigner_Transparency(t *testing.T) {
}
}

func TestSigner_TransparencyRekorBundle(t *testing.T) {
// Verify that when transparency is enabled, the Rekor bundle is passed through to storage opts.
tests := []struct {
name string
cfg *config.Config
getNewObject func(string) objects.TektonObject
}{
{
name: "taskrun with transparency enabled",
cfg: &config.Config{
Artifacts: config.ArtifactConfigs{
TaskRuns: config.Artifact{
Format: "slsa/v1",
StorageBackend: sets.New[string]("mock"),
Signer: "x509",
},
},
Transparency: config.TransparencyConfig{
Enabled: true,
URL: "https://rekor.example.com",
},
},
getNewObject: func(name string) objects.TektonObject {
return objects.NewTaskRunObjectV1(&v1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: name},
})
},
},
{
name: "pipelinerun with transparency enabled",
cfg: &config.Config{
Artifacts: config.ArtifactConfigs{
PipelineRuns: config.Artifact{
Format: "slsa/v1",
StorageBackend: sets.New[string]("mock"),
Signer: "x509",
},
},
Transparency: config.TransparencyConfig{
Enabled: true,
URL: "https://rekor.example.com",
},
},
getNewObject: func(name string) objects.TektonObject {
return objects.NewPipelineRunObjectV1(&v1.PipelineRun{
ObjectMeta: metav1.ObjectMeta{Name: name},
})
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rekor := &mockRekor{}
backend := &mockBackend{backendType: "mock"}
cleanup := setupMocks(rekor)
defer cleanup()

ctx, _ := rtesting.SetupFakeContext(t)
ps := fakepipelineclient.Get(ctx)
ctx = config.ToContext(ctx, tt.cfg.DeepCopy())

os := &ObjectSigner{
Backends: fakeAllBackends([]*mockBackend{backend}),
SecretPath: "./signing/x509/testdata/",
Pipelineclientset: ps,
}

obj := tt.getNewObject("test-rekor-bundle")
tekton.CreateObject(t, ctx, ps, obj)

if err := os.Sign(ctx, obj); err != nil {
t.Fatalf("Signer.Sign() error = %v", err)
}

if len(rekor.entries) != 1 {
t.Fatalf("expected 1 transparency log entry, got %d", len(rekor.entries))
}

if backend.storedOpts.RekorBundle == nil {
t.Fatal("expected RekorBundle to be set in StorageOpts, got nil")
}

if string(backend.storedOpts.RekorBundle.SignedEntryTimestamp) != "signed-entry-timestamp" {
t.Errorf("unexpected SignedEntryTimestamp: %s", string(backend.storedOpts.RekorBundle.SignedEntryTimestamp))
}

if backend.storedOpts.RekorBundle.Payload.IntegratedTime != 1234567890 {
t.Errorf("unexpected IntegratedTime: %d", backend.storedOpts.RekorBundle.Payload.IntegratedTime)
}

if backend.storedOpts.RekorBundle.Payload.LogID != "test-log-id" {
t.Errorf("unexpected LogID: %s", backend.storedOpts.RekorBundle.Payload.LogID)
}
})
}
}

func TestSigner_NoRekorBundleWithoutTransparency(t *testing.T) {
// Verify that RekorBundle is nil when transparency is disabled.
backend := &mockBackend{backendType: "mock"}
rekor := &mockRekor{}
cleanup := setupMocks(rekor)
defer cleanup()

cfg := &config.Config{
Artifacts: config.ArtifactConfigs{
TaskRuns: config.Artifact{
Format: "slsa/v1",
StorageBackend: sets.New[string]("mock"),
Signer: "x509",
},
},
Transparency: config.TransparencyConfig{
Enabled: false,
},
}

ctx, _ := rtesting.SetupFakeContext(t)
ps := fakepipelineclient.Get(ctx)
ctx = config.ToContext(ctx, cfg.DeepCopy())

os := &ObjectSigner{
Backends: fakeAllBackends([]*mockBackend{backend}),
SecretPath: "./signing/x509/testdata/",
Pipelineclientset: ps,
}

obj := objects.NewTaskRunObjectV1(&v1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: "test-no-rekor-bundle"},
})
tekton.CreateObject(t, ctx, ps, obj)

if err := os.Sign(ctx, obj); err != nil {
t.Fatalf("Signer.Sign() error = %v", err)
}

if len(rekor.entries) != 0 {
t.Fatalf("expected no transparency log entries, got %d", len(rekor.entries))
}

if backend.storedOpts.RekorBundle != nil {
t.Error("expected RekorBundle to be nil when transparency is disabled")
}
}

func TestSigningObjects(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -573,13 +719,22 @@ type mockRekor struct {
func (r *mockRekor) UploadTlog(ctx context.Context, signer signing.Signer, signature, rawPayload []byte, cert, payloadFormat string) (*models.LogEntryAnon, error) {
r.entries = append(r.entries, signature)
index := int64(len(r.entries) - 1)
logID := "test-log-id"
integratedTime := int64(1234567890)
return &models.LogEntryAnon{
LogIndex: &index,
LogIndex: &index,
LogID: &logID,
IntegratedTime: &integratedTime,
Body: "dGVzdC1ib2R5", // base64("test-body")
Verification: &models.LogEntryAnonVerification{
SignedEntryTimestamp: []byte("signed-entry-timestamp"),
},
}, nil
}

type mockBackend struct {
storedPayload []byte
storedOpts config.StorageOpts
shouldErr bool
backendType string
}
Expand All @@ -590,6 +745,7 @@ func (b *mockBackend) StorePayload(ctx context.Context, _ objects.TektonObject,
return errors.New("mock error storing")
}
b.storedPayload = rawPayload
b.storedOpts = opts
return nil
}

Expand Down
3 changes: 3 additions & 0 deletions pkg/chains/storage/oci/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ func (s *AttestationStorer) Store(ctx context.Context, req *api.StoreRequest[nam
if req.Bundle.Cert != nil {
attOpts = append(attOpts, static.WithCertChain(req.Bundle.Cert, req.Bundle.Chain))
}
if req.Bundle.RekorBundle != nil {
attOpts = append(attOpts, static.WithBundle(req.Bundle.RekorBundle))
}
att, err := static.NewAttestation(req.Bundle.Signature, attOpts...)
if err != nil {
return nil, err
Expand Down
Loading
Loading