Skip to content

Commit 122acc0

Browse files
authored
Merge pull request #68 from buildkite/outer-env-as-invariant
Add VerifyStep, un-export CommandStepWithInvariants
2 parents 5759dc0 + 501dac7 commit 122acc0

File tree

5 files changed

+188
-126
lines changed

5 files changed

+188
-126
lines changed

signature/pipeline_invariants.go

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,53 @@ import (
77
"github.com/buildkite/go-pipeline"
88
)
99

10-
var _ SignedFielder = (*CommandStepWithInvariants)(nil)
10+
// EnvNamespacePrefix is the string that prefixes all fields in the "env"
11+
// namespace. This is used to separate signed data that came from the
12+
// environment from data that came from an object.
13+
const EnvNamespacePrefix = "env::"
1114

12-
// CommandStepWithInvariants is a CommandStep with PipelineInvariants.
13-
type CommandStepWithInvariants struct {
15+
var _ SignedFielder = (*commandStepWithInvariants)(nil)
16+
17+
// commandStepWithInvariants is a CommandStep with pipeline invariants.
18+
// Pipeline invariants are things like the repository URL (since mutating that
19+
// could cause the agent to download the wrong code to build) and pipeline-
20+
// -level env vars (since they can greatly affect how a job is run and provide
21+
// ample means of side-stepping protections e.g. shell injections).
22+
type commandStepWithInvariants struct {
1423
pipeline.CommandStep
1524
RepositoryURL string
25+
// For signing, OuterEnv is the pipeline env.
26+
// For verifying, OuterEnv is the job env.
27+
OuterEnv map[string]string
1628
}
1729

1830
// SignedFields returns the default fields for signing.
19-
func (c *CommandStepWithInvariants) SignedFields() (map[string]any, error) {
20-
return map[string]any{
31+
func (c *commandStepWithInvariants) SignedFields() (map[string]any, error) {
32+
object := map[string]any{
2133
"command": c.Command,
2234
"env": EmptyToNilMap(c.Env),
2335
"plugins": EmptyToNilSlice(c.Plugins),
2436
"matrix": EmptyToNilPtr(c.Matrix),
2537
"repository_url": c.RepositoryURL,
26-
}, nil
38+
}
39+
40+
// Step env overrides pipeline and build env:
41+
// https://buildkite.com/docs/tutorials/pipeline-upgrade#what-is-the-yaml-steps-editor-compatibility-issues
42+
// (Beware of inconsistent docs written in the time of legacy steps.)
43+
// So step env vars exclude pipeline vars from signing.
44+
// Namespace the env values and include them in the values to sign.
45+
for k, v := range c.OuterEnv {
46+
if _, has := c.Env[k]; has {
47+
continue
48+
}
49+
object[EnvNamespacePrefix+k] = v
50+
}
51+
52+
return object, nil
2753
}
2854

2955
// ValuesForFields returns the contents of fields to sign.
30-
func (c *CommandStepWithInvariants) ValuesForFields(fields []string) (map[string]any, error) {
56+
func (c *commandStepWithInvariants) ValuesForFields(fields []string) (map[string]any, error) {
3157
// Make a set of required fields. As fields is processed, mark them off by
3258
// deleting them.
3359
required := map[string]struct{}{
@@ -59,8 +85,13 @@ func (c *CommandStepWithInvariants) ValuesForFields(fields []string) (map[string
5985
out["repository_url"] = c.RepositoryURL
6086

6187
default:
62-
// All env:: values come from outside the step.
63-
if strings.HasPrefix(f, EnvNamespacePrefix) {
88+
if name, has := strings.CutPrefix(f, EnvNamespacePrefix); has {
89+
// Do we have that env var?
90+
if value, has := c.OuterEnv[name]; has {
91+
out[f] = value
92+
} else {
93+
return nil, fmt.Errorf("variable %q missing from environment", name)
94+
}
6495
break
6596
}
6697

signature/sign.go

Lines changed: 38 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ import (
1717
"github.com/lestrrat-go/jwx/v2/jws"
1818
)
1919

20-
// EnvNamespacePrefix is the string that prefixes all fields in the "env"
21-
// namespace. This is used to separate signed data that came from the
22-
// environment from data that came from an object.
23-
const EnvNamespacePrefix = "env::"
24-
2520
// SignedFielder describes types that can be signed and have signatures
2621
// verified.
2722
// Converting non-string fields into strings (in a stable, canonical way) is an
@@ -43,31 +38,51 @@ type SignedFielder interface {
4338
type Logger interface{ Debug(f string, v ...any) }
4439

4540
type options struct {
46-
env map[string]string
41+
env map[string]string // not used in Sign or Verify
4742
logger Logger
4843
debugSigning bool
4944
}
5045

46+
// Allow *options to pass through SignOrVerifyOption.
47+
func (o *options) apply(opts *options) { *opts = *o }
48+
func (*options) signOrVerifyTag() {}
49+
50+
// Option implementations provide extra parameters. This type encompasses all
51+
// options, whether or not they are allowed to be passed to Sign or Verify.
5152
type Option interface {
5253
apply(*options)
5354
}
5455

55-
type envOption struct{ env map[string]string }
56+
// SignOrVerifyOption are the subtype of options that can be passed to Sign
57+
// or to Verify.
58+
type SignOrVerifyOption interface {
59+
Option
60+
61+
// This tag ensures that options that aren't one of the specifically-tagged
62+
// options cannot be passed to Sign or Verify.
63+
signOrVerifyTag()
64+
}
65+
5666
type loggerOption struct{ logger Logger }
67+
68+
func (o loggerOption) apply(opts *options) { opts.logger = o.logger }
69+
func (loggerOption) signOrVerifyTag() {}
70+
5771
type debugSigningOption struct{ debugSigning bool }
5872

59-
func (o envOption) apply(opts *options) { opts.env = o.env }
60-
func (o loggerOption) apply(opts *options) { opts.logger = o.logger }
6173
func (o debugSigningOption) apply(opts *options) { opts.debugSigning = o.debugSigning }
74+
func (debugSigningOption) signOrVerifyTag() {}
6275

63-
func WithEnv(env map[string]string) Option { return envOption{env} }
64-
func WithLogger(logger Logger) Option { return loggerOption{logger} }
65-
func WithDebugSigning(debugSigning bool) Option { return debugSigningOption{debugSigning} }
76+
// WithLogger provides a logger to use for debug logging.
77+
func WithLogger(logger Logger) SignOrVerifyOption { return loggerOption{logger} }
6678

67-
func configureOptions(opts ...Option) options {
68-
options := options{
69-
env: make(map[string]string),
70-
}
79+
// WithDebugSigning enables or disables signing debugging. Aside from logging
80+
// verbosely, enabling this may risk disclosing information that could break the
81+
// encryption properties of the signature.
82+
func WithDebugSigning(debugSigning bool) SignOrVerifyOption { return debugSigningOption{debugSigning} }
83+
84+
func configureOptions[E Option](opts []E) options {
85+
options := options{}
7186
for _, o := range opts {
7287
o.apply(&options)
7388
}
@@ -78,11 +93,11 @@ type Key interface {
7893
Algorithm() jwa.KeyAlgorithm
7994
}
8095

81-
// Sign computes a new signature for an environment (env) combined with an
82-
// object containing values (sf) using a given key. The key can be a jwk.Key
83-
// or a crypto.Signer. If it is a jwk.Key, the public key thumbprint is logged.
84-
func Sign(_ context.Context, key Key, sf SignedFielder, opts ...Option) (*pipeline.Signature, error) {
85-
options := configureOptions(opts...)
96+
// Sign computes a new signature for object containing values (sf) using a given
97+
// key. The key can be a jwk.Key or a crypto.Signer. If it is a jwk.Key, the
98+
// public key thumbprint is logged.
99+
func Sign(_ context.Context, key Key, sf SignedFielder, opts ...SignOrVerifyOption) (*pipeline.Signature, error) {
100+
options := configureOptions(opts)
86101

87102
values, err := sf.SignedFields()
88103
if err != nil {
@@ -92,21 +107,6 @@ func Sign(_ context.Context, key Key, sf SignedFielder, opts ...Option) (*pipeli
92107
return nil, errors.New("no fields to sign")
93108
}
94109

95-
// Step env overrides pipeline and build env:
96-
// https://buildkite.com/docs/tutorials/pipeline-upgrade#what-is-the-yaml-steps-editor-compatibility-issues
97-
// (Beware of inconsistent docs written in the time of legacy steps.)
98-
// So if the thing we're signing has an env map, use it to exclude pipeline
99-
// vars from signing.
100-
objEnv, _ := values["env"].(map[string]string)
101-
102-
// Namespace the env values and include them in the values to sign.
103-
for k, v := range options.env {
104-
if _, has := objEnv[k]; has {
105-
continue
106-
}
107-
values[EnvNamespacePrefix+k] = v
108-
}
109-
110110
// Extract the names of the fields.
111111
fields := make([]string, 0, len(values))
112112
for field := range values {
@@ -166,8 +166,8 @@ func Sign(_ context.Context, key Key, sf SignedFielder, opts ...Option) (*pipeli
166166
// Verify verifies an existing signature against environment (env) combined with
167167
// the keyset. The keySet can be a jwk.Set or a crypto.Signer. If it is a jwk.Set,
168168
// the public key thumbprints are logged.
169-
func Verify(ctx context.Context, s *pipeline.Signature, keySet any, sf SignedFielder, opts ...Option) error {
170-
options := configureOptions(opts...)
169+
func Verify(ctx context.Context, s *pipeline.Signature, keySet any, sf SignedFielder, opts ...SignOrVerifyOption) error {
170+
options := configureOptions(opts)
171171

172172
if len(s.SignedFields) == 0 {
173173
return errors.New("signature covers no fields")
@@ -179,23 +179,6 @@ func Verify(ctx context.Context, s *pipeline.Signature, keySet any, sf SignedFie
179179
return fmt.Errorf("obtaining values for fields: %w", err)
180180
}
181181

182-
// See Sign above for why we need special handling for step env.
183-
objEnv, _ := values["env"].(map[string]string)
184-
185-
// Namespace the env values and include them in the values to sign.
186-
for k, v := range options.env {
187-
if _, has := objEnv[k]; has {
188-
continue
189-
}
190-
values[EnvNamespacePrefix+k] = v
191-
}
192-
193-
// env:: fields that were signed are all required from the env map.
194-
// We can't verify other env vars though - they can vary for lots of reasons
195-
// (e.g. Buildkite-provided vars added by the backend.)
196-
// This is still strong enough for a user to enforce any particular env var
197-
// exists and has a particular value - make it a part of the pipeline or
198-
// step env.
199182
required, err := requireKeys(values, s.SignedFields)
200183
if err != nil {
201184
return fmt.Errorf("obtaining required keys: %w", err)

0 commit comments

Comments
 (0)