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
34 changes: 28 additions & 6 deletions signature/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ type SignedFielder interface {
type Logger interface{ Debug(f string, v ...any) }

type options struct {
env map[string]string
logger Logger
debugSigning bool
env map[string]string
logger Logger
debugSigning bool
ignoredEnvVars []string
}

type Option interface {
Expand All @@ -55,12 +56,15 @@ type Option interface {
type envOption struct{ env map[string]string }
type loggerOption struct{ logger Logger }
type debugSigningOption struct{ debugSigning bool }
type ignoringEnvVarsOption struct{ varKeys []string }

func (o envOption) apply(opts *options) { opts.env = o.env }
func (o loggerOption) apply(opts *options) { opts.logger = o.logger }
func (o debugSigningOption) apply(opts *options) { opts.debugSigning = o.debugSigning }
func (o envOption) apply(opts *options) { opts.env = o.env }
func (o loggerOption) apply(opts *options) { opts.logger = o.logger }
func (o debugSigningOption) apply(opts *options) { opts.debugSigning = o.debugSigning }
func (o ignoringEnvVarsOption) apply(opts *options) { opts.ignoredEnvVars = o.varKeys }

func WithEnv(env map[string]string) Option { return envOption{env} }
func IgnoringEnvVars(varKeys ...string) Option { return ignoringEnvVarsOption{varKeys} }
func WithLogger(logger Logger) Option { return loggerOption{logger} }
func WithDebugSigning(debugSigning bool) Option { return debugSigningOption{debugSigning} }

Expand Down Expand Up @@ -99,6 +103,15 @@ func Sign(_ context.Context, key Key, sf SignedFielder, opts ...Option) (*pipeli
// vars from signing.
objEnv, _ := values["env"].(map[string]string)

for _, ignored := range options.ignoredEnvVars {
delete(objEnv, ignored)
delete(options.env, ignored)
}

// We may have deleted the only entry in the env map, and we canonicalise empty maps to nil, so if the map is empty,
// set it to nil.
values["env"] = EmptyToNilMap(objEnv)

// Namespace the env values and include them in the values to sign.
for k, v := range options.env {
if _, has := objEnv[k]; has {
Expand Down Expand Up @@ -182,6 +195,15 @@ func Verify(ctx context.Context, s *pipeline.Signature, keySet any, sf SignedFie
// See Sign above for why we need special handling for step env.
objEnv, _ := values["env"].(map[string]string)

for _, ignored := range options.ignoredEnvVars {
delete(objEnv, ignored)
delete(options.env, ignored)
}

// We may have deleted the only entry in the env map, and we canonicalise empty maps to nil, so if the map is empty,
// set it to nil.
values["env"] = EmptyToNilMap(objEnv)

// Namespace the env values and include them in the values to sign.
for k, v := range options.env {
if _, has := objEnv[k]; has {
Expand Down
95 changes: 95 additions & 0 deletions signature/sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,101 @@ func TestSignVerifyEnv(t *testing.T) {
}
}

func TestSignVerify_IgnoredEnvVars(t *testing.T) {
t.Parallel()
ctx := context.Background()

keyStr, keyAlg := "alpacas", jwa.HS256
signer, verifier, err := jwkutil.NewSymmetricKeyPairFromString(keyID, keyStr, keyAlg)
if err != nil {
t.Fatalf("jwkutil.NewSymmetricKeyPairFromString(%q, %q, %q) error = %v", keyID, keyStr, keyAlg, err)
}

key, ok := signer.Key(0)
if !ok {
t.Fatalf("signer.Key(0) = _, false, want true")
}

toSign := &CommandStepWithInvariants{
CommandStep: pipeline.CommandStep{
Command: "llamas",
Env: map[string]string{
"CONTEXT": "cats",
// "DEPLOY": "0",
},
},
RepositoryURL: fakeRepositoryURL,
}

toVerify := &CommandStepWithInvariants{
CommandStep: pipeline.CommandStep{
Command: "llamas",
Env: map[string]string{
"CONTEXT": "dogs", // Changed from "cats"
// "DEPLOY": "0",
},
},
RepositoryURL: fakeRepositoryURL,
}

sig, err := Sign(ctx, key, toSign, IgnoringEnvVars("CONTEXT"))
if err != nil {
t.Fatalf("Sign(ctx, key, %v) error = %v", toSign, err)
}

if err := Verify(ctx, sig, verifier, toVerify, IgnoringEnvVars("CONTEXT")); err != nil {
t.Errorf("Verify(ctx, %v, verifier, %v) = %v", sig, toVerify, err)
}
}

// In this test, we have a step with no env, then sign it with a pipeline env that has an ignored env var.
// Then, we verify the step having changed the ignored env var in the step env. It should verify successfully,
// because the ignored env var is not included in the signature.
func TestSignVerify_IgnoredEnvVars_WithEnv(t *testing.T) {
t.Parallel()
ctx := context.Background()

keyStr, keyAlg := "alpacas", jwa.HS256
signer, verifier, err := jwkutil.NewSymmetricKeyPairFromString(keyID, keyStr, keyAlg)
if err != nil {
t.Fatalf("jwkutil.NewSymmetricKeyPairFromString(%q, %q, %q) error = %v", keyID, keyStr, keyAlg, err)
}

key, ok := signer.Key(0)
if !ok {
t.Fatalf("signer.Key(0) = _, false, want true")
}

toSign := &CommandStepWithInvariants{
CommandStep: pipeline.CommandStep{Command: "llamas"},
RepositoryURL: fakeRepositoryURL,
}

ignored := "ENV_VAR_TO_CHANGE"
pipelineEnv := map[string]string{
ignored: "cats",
}

toVerify := &CommandStepWithInvariants{
CommandStep: pipeline.CommandStep{
Command: "llamas",
Env: map[string]string{
ignored: "dogs",
},
},
RepositoryURL: fakeRepositoryURL,
}

sig, err := Sign(ctx, key, toSign, WithEnv(pipelineEnv), IgnoringEnvVars(ignored))
if err != nil {
t.Fatalf("Sign(ctx, key, %v) error = %v", toSign, err)
}

if err := Verify(ctx, sig, verifier, toVerify, WithEnv(pipelineEnv), IgnoringEnvVars(ignored)); err != nil {
t.Errorf("Verify(ctx, %v, verifier, %v) = %v", sig, toVerify, err)
}
}

func TestSignVerify_NilVsEmpty(t *testing.T) {
t.Parallel()
ctx := context.Background()
Expand Down