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
2 changes: 2 additions & 0 deletions cmd/stamp/container_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ func init() {
_ = plugincobra.ApplyFlagGroup(containerSignCmd, flags.PasswordFlags)
_ = plugincobra.ApplyFlagGroup(containerSignCmd, flags.RekorEnableFlags)
_ = plugincobra.ApplyFlagGroup(containerSignCmd, flags.RekorServerFlags)
_ = plugincobra.ApplyFlagGroup(containerSignCmd, flags.RekorVersionFlags)
_ = plugincobra.ApplyFlagGroup(containerSignCmd, flags.TSAServerFlags)
_ = plugincobra.ApplyFlagGroup(containerSignCmd, flags.ContainerSignFlags)

containerCmd.AddCommand(containerSignCmd)
Expand Down
14 changes: 14 additions & 0 deletions pkg/config/flags/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,20 @@ const (

// RekorTemporalPolicy specifies temporal validation policy: "strict" (fail), "warn", "ignore".
RekorTemporalPolicy = "pipeline.rekor.temporal_policy"

// RekorVersion specifies the Rekor API version to speak: 1 (default) or 2.
// Rekor v2 (rekor-tiles) requires a TSA; sigstore-go enforces this.
RekorVersion = "pipeline.rekor.version"
)

// ============================================================================
// TIMESTAMP AUTHORITY (TSA) CONFIGURATION
// ============================================================================

const (
// TSAURL specifies the RFC 3161 Timestamp Authority server URL. Required
// when using Rekor v2 because integrated_time is always 0 in v2 entries.
TSAURL = "pipeline.tsa.url"
)

// ============================================================================
Expand Down
16 changes: 16 additions & 0 deletions pkg/config/flags/rekor.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ var RekorServerFlags = plugincobra.FlagGroup{
},
}

// RekorVersionFlags controls which Rekor API version stamp speaks. Default
// 1 (classic Rekor). Set to 2 for rekor-tiles. TSA is required for v2.
var RekorVersionFlags = plugincobra.FlagGroup{
"rekor-version": {
Name: "rekor-version",
ConfigPath: RekorVersion,
Type: plugincobra.IntFlag,
Default: 1,
Help: "Rekor API version (1 for classic Rekor, 2 for rekor-tiles)",
Constraints: &plugincobra.FlagConstraints{
MinValue: &[]int{1}[0],
MaxValue: &[]int{2}[0],
},
},
}

// RekorUploadFlags contains Rekor upload target configuration.
var RekorUploadFlags = plugincobra.FlagGroup{
"rekor-upload": {
Expand Down
30 changes: 30 additions & 0 deletions pkg/config/flags/tsa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2026 Thomson Reuters
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package flags

import plugincobra "github.com/thomsonreuters/stamp/plugins/cobra"

// TSAServerFlags configures the RFC 3161 Timestamp Authority endpoint.
// Required when uploading to Rekor v2 (integrated_time is 0 in v2 entries,
// so signing time comes from a TSA-signed timestamp instead).
var TSAServerFlags = plugincobra.FlagGroup{
"tsa-url": {
Name: "tsa-url",
ConfigPath: TSAURL,
Type: plugincobra.StringFlag,
Default: "",
Help: "RFC 3161 Timestamp Authority URL (required with --rekor-version=2)",
},
}
4 changes: 4 additions & 0 deletions pkg/operations/container_sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ func TestContainerSignOp_buildSignOptions_KeyMode(t *testing.T) {
cfg.On("GetString", flags.CryptographyKeyPasswordFile).Return("")
cfg.On("GetBool", flags.CryptographyKeyPasswordPrompt).Return(false)
cfg.On("GetBool", flags.TransparencyEnable).Return(false).Maybe()
cfg.On("GetString", flags.TSAURL).Return("").Maybe()

op := NewContainerSignOp(cfg, logger.NewNoop(), output.NewNoop())
opts, err := op.buildSignOptions(context.Background())
Expand Down Expand Up @@ -236,6 +237,8 @@ func TestContainerSignOp_buildSignOptions_FulcioMode(t *testing.T) {
cfg.On("GetBool", flags.Insecure).Return(false)
cfg.On("GetBool", flags.TransparencyEnable).Return(true)
cfg.On("GetString", flags.RekorURL).Return("https://rekor.example.com")
cfg.On("GetInt", flags.RekorVersion).Return(1).Maybe()
cfg.On("GetString", flags.TSAURL).Return("").Maybe()

op := NewContainerSignOp(cfg, logger.NewNoop(), output.NewNoop())
opts, err := op.buildSignOptions(context.Background())
Expand Down Expand Up @@ -267,6 +270,7 @@ func TestContainerSignOp_buildSignOptions_NoRegistryEnv(t *testing.T) {
cfg.On("GetBool", flags.UseGitHub).Return(false)
cfg.On("GetBool", flags.Insecure).Return(false)
cfg.On("GetBool", flags.TransparencyEnable).Return(false)
cfg.On("GetString", flags.TSAURL).Return("").Maybe()

op := NewContainerSignOp(cfg, logger.NewNoop(), output.NewNoop())
opts, err := op.buildSignOptions(context.Background())
Expand Down
21 changes: 20 additions & 1 deletion pkg/signing/sigstore/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ import (
)

// Options for Signer.SignBundle. Exactly one of Key or Fulcio must be set.
// Rekor is optional; nil skips transparency-log upload.
// Rekor is optional; nil skips transparency-log upload. TSA is required
// when Rekor.Version == 2.
type Options struct {
Key *KeyOptions
Fulcio *FulcioOptions
Rekor *RekorOptions
TSA *TSAOptions
}

type KeyOptions struct {
Expand All @@ -46,6 +48,15 @@ type FulcioOptions struct {

type RekorOptions struct {
URL string
// Version selects the Rekor API. 0/1 = classic Rekor, 2 = rekor-tiles.
// sigstore-go defaults to 1 when unset; v2 additionally requires a TSA.
Version uint32
}

// TSAOptions configures an RFC 3161 Timestamp Authority. Required when
// Rekor v2 is in use because v2 entries have integrated_time = 0.
type TSAOptions struct {
URL string
}

type Result struct {
Expand Down Expand Up @@ -77,6 +88,14 @@ func (o *Options) Validate() error {
if o.Rekor != nil && o.Rekor.URL == "" {
return errors.New("sigstore sign: Rekor.URL is required")
}
// Order matters: check the v2-specific rule first so its more informative
// message wins when both this rule and the general "TSA empty" rule apply.
if o.Rekor != nil && o.Rekor.Version == 2 && (o.TSA == nil || o.TSA.URL == "") {
return errors.New("sigstore sign: Rekor v2 requires TSA.URL to be set")
}
if o.TSA != nil && o.TSA.URL == "" {
return errors.New("sigstore sign: TSA.URL is required when TSA is set")
}
return nil
}

Expand Down
58 changes: 58 additions & 0 deletions pkg/signing/sigstore/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,64 @@ func TestOptions_Validate(t *testing.T) {
Rekor: &RekorOptions{URL: "https://rekor.example.com"},
},
},
// --- TSA validation (added with C1 Rekor v2 support) ---
{
name: "TSA set with empty URL, no Rekor",
opts: Options{
Key: validKey,
TSA: &TSAOptions{URL: ""},
},
wantErr: "TSA.URL is required when TSA is set",
},
{
name: "valid TSA-only, no Rekor",
opts: Options{
Key: validKey,
TSA: &TSAOptions{URL: "https://timestamp.example.com"},
},
},
{
name: "rekor v1 with empty TSA URL",
opts: Options{
Key: validKey,
Rekor: &RekorOptions{URL: "https://rekor.example.com", Version: 1},
TSA: &TSAOptions{URL: ""},
},
wantErr: "TSA.URL is required when TSA is set",
},
{
name: "rekor v1 with valid TSA",
opts: Options{
Key: validKey,
Rekor: &RekorOptions{URL: "https://rekor.example.com", Version: 1},
TSA: &TSAOptions{URL: "https://timestamp.example.com"},
},
},
{
name: "rekor v2 without TSA",
opts: Options{
Key: validKey,
Rekor: &RekorOptions{URL: "https://rekor.example.com", Version: 2},
},
wantErr: "Rekor v2 requires TSA.URL to be set",
},
{
name: "rekor v2 with empty TSA URL (v2 rule takes precedence)",
opts: Options{
Key: validKey,
Rekor: &RekorOptions{URL: "https://rekor.example.com", Version: 2},
TSA: &TSAOptions{URL: ""},
},
wantErr: "Rekor v2 requires TSA.URL to be set",
},
{
name: "valid rekor v2 with TSA",
opts: Options{
Key: validKey,
Rekor: &RekorOptions{URL: "https://rekor.example.com", Version: 2},
TSA: &TSAOptions{URL: "https://timestamp.example.com"},
},
},
}

for _, tt := range tests {
Expand Down
8 changes: 7 additions & 1 deletion pkg/signing/sigstore/optsbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ func BuildOptionsFromConfig(ctx context.Context, cfg config.ConfigurationIface)
}

if cfg.GetBool(flags.TransparencyEnable) {
opts.Rekor = &RekorOptions{URL: cfg.GetString(flags.RekorURL)}
opts.Rekor = &RekorOptions{
URL: cfg.GetString(flags.RekorURL),
Version: uint32(cfg.GetInt(flags.RekorVersion)),
}
}
if tsaURL := cfg.GetString(flags.TSAURL); tsaURL != "" {
opts.TSA = &TSAOptions{URL: tsaURL}
}
return opts, nil
}
Expand Down
17 changes: 17 additions & 0 deletions pkg/signing/sigstore/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,23 @@ func (s *Signer) SignBundle(ctx context.Context, payload []byte, payloadType str
bundleOpts.TransparencyLogs = []sign.Transparency{
sign.NewRekor(&sign.RekorOptions{
BaseURL: opts.Rekor.URL,
Version: opts.Rekor.Version,
}),
}
}
if opts.TSA != nil {
bundleOpts.TimestampAuthorities = []*sign.TimestampAuthority{
sign.NewTimestampAuthority(&sign.TimestampAuthorityOptions{
URL: opts.TSA.URL,
}),
}
}

s.logger.InfoContext(ctx, "signing bundle",
"keyless", opts.Fulcio != nil,
"rekor", opts.Rekor != nil,
"rekor_version", rekorVersionForLog(opts.Rekor),
"tsa", opts.TSA != nil,
"payload_type", payloadType,
)

Expand All @@ -92,6 +102,13 @@ func (s *Signer) SignBundle(ctx context.Context, payload []byte, payloadType str
}, nil
}

func rekorVersionForLog(r *RekorOptions) uint32 {
if r == nil {
return 0
}
return r.Version
}

// wrapSignBundleError translates sigstore-go's cryptic TextConsumer
// failure — raised when Rekor returns non-JSON (e.g. a policy denial) —
// into an actionable message.
Expand Down
Loading