Skip to content
Draft
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
74 changes: 73 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Supported keys include:
|:-------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|
| `storage.gcs.bucket` | The GCS bucket for storage | | |
| `storage.oci.repository` | The OCI repo to store OCI signatures and attestation in | If left undefined _and_ one of `artifacts.{oci,taskrun}.storage` includes `oci` storage, attestations will be stored alongside the stored OCI artifact itself. ([example on GCP](../images/attestations-in-artifact-registry.png)) Defining this value results in the OCI bundle stored in the designated location _instead of_ alongside the image. See [cosign documentation](https://github.com/sigstore/cosign#specifying-registry) for additional information. | |
| `storage.oci.format` | Storage format for OCI signatures and attestations. Controls both the storage mechanism and serialization format used for storing cryptographic artifacts. | `legacy` - Tag-based storage with DSSE format for full backward compatibility<br/>`referrers-api` - OCI 1.1 referrers API with DSSE format for reduced tag proliferation<br/>`protobuf-bundle` - OCI 1.1 referrers API with protobuf bundle format for experimental cosign features | `legacy` |
| `storage.docdb.url` | The go-cloud URI reference to a docstore collection | `firestore://projects/[PROJECT]/databases/(default)/documents/[COLLECTION]?name_field=name` | |
| `storage.docdb.mongo-server-url` (optional) | The value of MONGO_SERVER_URL env var with the MongoDB connection URI | Example: `mongodb://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]` | |
| `storage.docdb.mongo-server-url-dir` (optional) | The path of the directory that contains the file named MONGO_SERVER_URL that stores the value of MONGO_SERVER_URL env var | If the file `/mnt/mongo-creds-secret/MONGO_SERVER_URL` has the value of MONGO_SERVER_URL, then set `storage.docdb.mongo-server-url-dir: /mnt/mongo-creds-secret` | |
Expand All @@ -75,6 +76,77 @@ Supported keys include:
| `storage.grafeas.notehint` (optional) | This field is used to set the [human_readable_name](https://github.com/grafeas/grafeas/blob/cd23d4dc1bef740d6d6d90d5007db5c9a2431c41/proto/v1/attestation.proto#L49) field in the Grafeas ATTESTATION note. If it is not provided, the default `This attestation note was generated by Tekton Chains` will be used. | | |
| `storage.archivista.url` | The URL endpoint for the Archivista service. | A valid HTTPS URL pointing to your Archivista instance (e.g. `https://archivista.testifysec.io`). | None |

#### OCI Storage Formats

The `storage.oci.format` configuration supports three distinct storage formats, each designed for different use cases:

##### Legacy Format (`legacy`)
- **Storage Mechanism**: Tag-based storage using `<image>:sha256-<digest>.sig` and `<image>:sha256-<digest>.att` tags
- **Serialization Format**: DSSE (Dead Simple Signing Envelope) format
- **Compatibility**: Full backward compatibility with existing tooling and deployments
- **Registry Impact**: Creates additional tags in the registry for each signature and attestation
- **Use Case**: Production deployments requiring maximum compatibility

**Example Configuration:**
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: chains-config
namespace: tekton-chains
data:
storage.oci.format: "legacy"
```

##### Referrers API Format (`referrers-api`)
- **Storage Mechanism**: OCI 1.1 Referrers API for artifact relationships
- **Serialization Format**: DSSE (Dead Simple Signing Envelope) format - same as legacy
- **Compatibility**: Compatible with OCI 1.1 registries and DSSE-aware tooling
- **Registry Impact**: Uses referrers API, significantly reducing tag proliferation
- **Use Case**: Modern OCI 1.1 registries where tag reduction is desired while maintaining DSSE compatibility

**Example Configuration:**
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: chains-config
namespace: tekton-chains
data:
storage.oci.format: "referrers-api"
```

##### Protobuf Bundle Format (`protobuf-bundle`)
- **Storage Mechanism**: OCI 1.1 Referrers API for artifact relationships
- **Serialization Format**: Protobuf bundle format for experimental cosign features
- **Compatibility**: Requires cosign experimental features and latest tooling
- **Registry Impact**: Uses referrers API with experimental serialization
- **Use Case**: Testing new cosign features and experimental workflows

**Example Configuration:**
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: chains-config
namespace: tekton-chains
data:
storage.oci.format: "protobuf-bundle"
```

**Important Notes:**
- The default format is `legacy` to ensure backward compatibility
- Referrers API formats (`referrers-api` and `protobuf-bundle`) require OCI 1.1 compatible registries
- Format changes affect both signatures and attestations
- The `protobuf-bundle` format is experimental and may change in future releases

**Migration from Deprecated Configuration:**
The deprecated `storage.oci.referrers-api` boolean configuration is automatically migrated:
- `storage.oci.referrers-api: false` → `storage.oci.format: "legacy"`
- `storage.oci.referrers-api: true` → `storage.oci.format: "protobuf-bundle"`

See the [OCI Format Migration Guide](oci-format-migration.md) for detailed migration instructions.

#### docstore

You can read about the go-cloud docstore URI format [here](https://gocloud.dev/howto/docstore/). Tekton Chains supports the following docstore services:
Expand Down Expand Up @@ -189,4 +261,4 @@ To restrict the controller to the dev and test namespaces, you would start the c
```shell
--namespace=dev,test
```
In this example, the controller will only monitor resources (pipelinesruns and taskruns) within the dev and test namespaces.
In this example, the controller will only monitor resources (pipelinesruns and taskruns) within the dev and test namespaces.
10 changes: 10 additions & 0 deletions pkg/chains/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,21 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject)
continue
}

// Extract public key from signer for storage backends that need it
pubKey, err := signer.PublicKey()
if err != nil {
logger.Errorf("Failed to extract public key from signer: %v", err)
o.recordError(ctx, signableType.Type(), metrics.SigningError)
merr = multierror.Append(merr, err)
continue
}

storageOpts := config.StorageOpts{
ShortKey: signableType.ShortKey(obj),
FullKey: signableType.FullKey(obj),
Cert: signer.Cert(),
Chain: signer.Chain(),
PublicKey: pubKey,
PayloadFormat: payloadFormat,
}
if err := b.StorePayload(ctx, tektonObj, rawPayload, string(signature), storageOpts); err != nil {
Expand Down
4 changes: 4 additions & 0 deletions pkg/chains/signing/iface.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ limitations under the License.
package signing

import (
"crypto"

"github.com/sigstore/sigstore/pkg/signature"
)

Expand Down Expand Up @@ -41,4 +43,6 @@ type Bundle struct {
Cert []byte
// Cert is an optional PEM encoded x509 certificate chain, if one was used for signing.
Chain []byte
// PublicKey is the public key from the signer, available for storage backends that need it.
PublicKey crypto.PublicKey
}
203 changes: 203 additions & 0 deletions pkg/chains/signing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@
import (
"bytes"
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"testing"

"github.com/google/go-cmp/cmp"
intoto "github.com/in-toto/attestation/go/v1"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/tektoncd/chains/pkg/chains/objects"
"github.com/tektoncd/chains/pkg/chains/signing"
"github.com/tektoncd/chains/pkg/chains/storage"
Expand Down Expand Up @@ -543,3 +548,201 @@
func (b *mockBackend) RetrieveSignatures(ctx context.Context, _ objects.TektonObject, opts config.StorageOpts) (map[string][]string, error) {
return nil, fmt.Errorf("not implemented")
}

// Additional tests for protobuf bundle fix

func TestBundle_PublicKey(t *testing.T) {
// Test Bundle struct with PublicKey field
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Failed to generate RSA key: %v", err)
}
publicKey := &privateKey.PublicKey

tests := []struct {
name string
bundle signing.Bundle
want crypto.PublicKey
}{
{
name: "Bundle with PublicKey set",
bundle: signing.Bundle{
Content: []byte("test-content"),
Signature: []byte("test-signature"),
Cert: []byte("test-cert"),
Chain: []byte("test-chain"),
PublicKey: publicKey,
},
want: publicKey,
},
{
name: "Bundle with nil PublicKey",
bundle: signing.Bundle{
Content: []byte("test-content"),
Signature: []byte("test-signature"),
Cert: []byte("test-cert"),
Chain: []byte("test-chain"),
PublicKey: nil,
},
want: nil,
},
{
name: "Empty Bundle",

Check failure on line 590 in pkg/chains/signing_test.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (goimports)

Check failure on line 590 in pkg/chains/signing_test.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gofmt)
bundle: signing.Bundle{},
want: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.bundle.PublicKey != tt.want {
t.Errorf("Bundle.PublicKey = %v, want %v", tt.bundle.PublicKey, tt.want)
}

// Test that PublicKey field can be set and retrieved
if tt.want != nil {
// Verify the key is the expected type
if _, ok := tt.bundle.PublicKey.(*rsa.PublicKey); !ok {
t.Errorf("Expected PublicKey to be *rsa.PublicKey, got %T", tt.bundle.PublicKey)
}

// Verify the key matches our test key
rsaKey, ok := tt.bundle.PublicKey.(*rsa.PublicKey)
if !ok {
t.Fatalf("PublicKey is not *rsa.PublicKey")
}
expectedRSAKey, ok := tt.want.(*rsa.PublicKey)
if !ok {
t.Fatalf("Expected key is not *rsa.PublicKey")
}

if rsaKey.N.Cmp(expectedRSAKey.N) != 0 || rsaKey.E != expectedRSAKey.E {
t.Errorf("PublicKey does not match expected key")
}
}
})
}
}

// Mock signer for testing public key extraction
type mockSignerWithPublicKey struct {
publicKey crypto.PublicKey
cert string
chain string
shouldErr bool
errOnPubKey bool
}

func (m *mockSignerWithPublicKey) SignMessage(msg io.Reader, opts ...signature.SignOption) ([]byte, error) {
if m.shouldErr {
return nil, errors.New("mock signing error")
}
return []byte("mock-signature"), nil
}

func (m *mockSignerWithPublicKey) VerifySignature(signature, message io.Reader, opts ...signature.VerifyOption) error {
return nil
}

func (m *mockSignerWithPublicKey) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) {
if m.errOnPubKey {
return nil, errors.New("mock public key error")
}
return m.publicKey, nil
}

func (m *mockSignerWithPublicKey) Type() string {
return "mock"
}

func (m *mockSignerWithPublicKey) Cert() string {
return m.cert
}

func (m *mockSignerWithPublicKey) Chain() string {
return m.chain
}

func TestSigner_PublicKeyExtraction(t *testing.T) {
// Test that the signing loop includes public key extraction logic
// This test verifies the behavior exists but doesn't require complex mocking

// Test that we can create a StorageOpts with public key
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Failed to generate RSA key: %v", err)
}
publicKey := &privateKey.PublicKey

// Test that StorageOpts can hold public key correctly
opts := config.StorageOpts{
FullKey: "test-full",
ShortKey: "test-short",
Cert: "test-cert",
Chain: "test-chain",
PublicKey: publicKey,
PayloadFormat: "in-toto",
}

if opts.PublicKey != publicKey {
t.Error("StorageOpts should preserve public key")
}

// Test that mockBackendWithCapture can capture StorageOpts correctly
var capturedOpts config.StorageOpts
backend := &mockBackendWithCapture{
backendType: "test",
capturedOpts: &capturedOpts,
}

ctx := context.Background()
tro := objects.NewTaskRunObjectV1(&v1.TaskRun{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
})

err = backend.StorePayload(ctx, tro, []byte("test"), "signature", opts)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

// Verify the options were captured correctly
if capturedOpts.PublicKey != publicKey {
t.Error("Backend should capture public key correctly")
}
if capturedOpts.Cert != "test-cert" {
t.Errorf("Expected cert = test-cert, got %s", capturedOpts.Cert)
}
}

// Mock backend that captures StorageOpts for testing
type mockBackendWithCapture struct {
storedPayload []byte
shouldErr bool
backendType string
capturedOpts *config.StorageOpts
}

func (b *mockBackendWithCapture) StorePayload(ctx context.Context, _ objects.TektonObject, rawPayload []byte, signature string, opts config.StorageOpts) error {
if b.shouldErr {
return errors.New("mock error storing")
}
b.storedPayload = rawPayload
if b.capturedOpts != nil {
*b.capturedOpts = opts
}
return nil
}

func (b *mockBackendWithCapture) Type() string {
return b.backendType
}

func (b *mockBackendWithCapture) RetrievePayloads(ctx context.Context, _ objects.TektonObject, opts config.StorageOpts) (map[string]string, error) {
return nil, fmt.Errorf("not implemented")
}

func (b *mockBackendWithCapture) RetrieveSignatures(ctx context.Context, _ objects.TektonObject, opts config.StorageOpts) (map[string][]string, error) {
return nil, fmt.Errorf("not implemented")
}
2 changes: 2 additions & 0 deletions pkg/chains/storage/gcs/gcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, ra
Signature: []byte(signature),
Cert: []byte(opts.Cert),
Chain: []byte(opts.Chain),
PublicKey: opts.PublicKey,
},
}); err != nil {
logger.Errorf("error writing to GCS: %w", err)
Expand All @@ -101,6 +102,7 @@ func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, ra
Signature: []byte(signature),
Cert: []byte(opts.Cert),
Chain: []byte(opts.Chain),
PublicKey: opts.PublicKey,
},
}); err != nil {
logger.Errorf("error writing to GCS: %w", err)
Expand Down
Loading
Loading