Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canonicalise empty env/plugins/matrix to nil for sign/verify #45

Merged
merged 1 commit into from
Aug 26, 2024
Merged
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
12 changes: 6 additions & 6 deletions signature/pipeline_invariants.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ type CommandStepWithInvariants struct {
func (c *CommandStepWithInvariants) SignedFields() (map[string]any, error) {
return map[string]any{
"command": c.Command,
"env": c.Env,
"plugins": c.Plugins,
"matrix": c.Matrix,
"env": EmptyToNilMap(c.Env),
"plugins": EmptyToNilSlice(c.Plugins),
"matrix": EmptyToNilPtr(c.Matrix),
"repository_url": c.RepositoryURL,
}, nil
}
Expand All @@ -47,13 +47,13 @@ func (c *CommandStepWithInvariants) ValuesForFields(fields []string) (map[string
out["command"] = c.Command

case "env":
out["env"] = c.Env
out["env"] = EmptyToNilMap(c.Env)

case "plugins":
out["plugins"] = c.Plugins
out["plugins"] = EmptyToNilSlice(c.Plugins)

case "matrix":
out["matrix"] = c.Matrix
out["matrix"] = EmptyToNilPtr(c.Matrix)

case "repository_url":
out["repository_url"] = c.RepositoryURL
Expand Down
47 changes: 47 additions & 0 deletions signature/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,53 @@ func Verify(ctx context.Context, s *pipeline.Signature, keySet jwk.Set, sf Signe
return err
}

// EmptyToNilMap returns a nil map if m is empty, otherwise it returns m.
// This can be used to canonicalise empty/nil values if there is no semantic
// distinction between nil and empty.
// Sign and Verify do not apply this automatically.
// nil was chosen as the canonical value, since it is the zero value for the
// type. (A user would have to write e.g. "env: {}" to get a zero-length
// non-nil env map.)
func EmptyToNilMap[K comparable, V any, M ~map[K]V](m M) M {
if len(m) == 0 {
return nil
}
return m
}

// EmptyToNilSlice returns a nil slice if s is empty, otherwise it returns s.
// This can be used to canonicalise empty/nil values if there is no semantic
// distinction between nil and empty.
// Sign and Verify do not apply this automatically.
// nil was chosen as the canonical value, since it is the zero value for the
// type. (A user would have to write e.g. "plugins: []" to get a zero-length
// non-nil plugins slice.)
func EmptyToNilSlice[E any, S ~[]E](s S) S {
if len(s) == 0 {
return nil
}
return s
}

type pointerEmptyable[V any] interface {
~*V
IsEmpty() bool
}

// EmptyToNilPtr returns a nil pointer if p points to a variable containing
// an empty value for V, otherwise it returns p. Emptiness is determined by
// calling IsEmpty on p.
// Sign and Verify do not apply this automatically.
// nil was chosen as the canonical value since it is the zero value for pointer
// types. (A user would have to write e.g. "matrix: {}" to get an empty non-nil
// matrix specification.)
func EmptyToNilPtr[V any, P pointerEmptyable[V]](p P) P {
if p.IsEmpty() {
return nil
}
return p
}

// canonicalPayload returns a unique sequence of bytes representing the given
// algorithm and values using JCS (RFC 8785).
func canonicalPayload(alg string, values map[string]any) ([]byte, error) {
Expand Down
124 changes: 124 additions & 0 deletions signature/sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,130 @@ func TestSignVerifyEnv(t *testing.T) {
}
}

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

cases := []struct {
name string
stepSign *pipeline.CommandStep
stepVerify *pipeline.CommandStep
}{
{
name: "env both non-empty",
stepSign: &pipeline.CommandStep{
Command: "llamas",
Env: map[string]string{
"CONTEXT": "cats",
"DEPLOY": "0",
},
},
stepVerify: &pipeline.CommandStep{
Command: "llamas",
Env: map[string]string{
"CONTEXT": "cats",
"DEPLOY": "0",
},
},
},
{
name: "env sign nil verify nil",
stepSign: &pipeline.CommandStep{Command: "llamas", Env: nil},
stepVerify: &pipeline.CommandStep{Command: "llamas", Env: nil},
},
{
name: "env sign empty verify nil",
stepSign: &pipeline.CommandStep{Command: "llamas", Env: map[string]string{}},
stepVerify: &pipeline.CommandStep{Command: "llamas", Env: nil},
},
{
name: "env sign nil verify empty",
stepSign: &pipeline.CommandStep{Command: "llamas", Env: nil},
stepVerify: &pipeline.CommandStep{Command: "llamas", Env: map[string]string{}},
},
{
name: "env sign empty verify empty",
stepSign: &pipeline.CommandStep{Command: "llamas", Env: map[string]string{}},
stepVerify: &pipeline.CommandStep{Command: "llamas", Env: map[string]string{}},
},
{
name: "plugins sign nil verify nil",
stepSign: &pipeline.CommandStep{Command: "llamas", Plugins: nil},
stepVerify: &pipeline.CommandStep{Command: "llamas", Plugins: nil},
},
{
name: "plugins sign nil verify empty",
stepSign: &pipeline.CommandStep{Command: "llamas", Plugins: nil},
stepVerify: &pipeline.CommandStep{Command: "llamas", Plugins: pipeline.Plugins{}},
},
{
name: "plugins sign empty verify nil",
stepSign: &pipeline.CommandStep{Command: "llamas", Plugins: pipeline.Plugins{}},
stepVerify: &pipeline.CommandStep{Command: "llamas", Plugins: nil},
},
{
name: "plugins sign empty verify empty",
stepSign: &pipeline.CommandStep{Command: "llamas", Plugins: pipeline.Plugins{}},
stepVerify: &pipeline.CommandStep{Command: "llamas", Plugins: pipeline.Plugins{}},
},
{
name: "matrix sign nil verify nil",
stepSign: &pipeline.CommandStep{Command: "llamas", Matrix: nil},
stepVerify: &pipeline.CommandStep{Command: "llamas", Matrix: nil},
},
{
name: "matrix sign nil verify empty",
stepSign: &pipeline.CommandStep{Command: "llamas", Matrix: nil},
stepVerify: &pipeline.CommandStep{Command: "llamas", Matrix: &pipeline.Matrix{}},
},
{
name: "matrix sign empty verify nil",
stepSign: &pipeline.CommandStep{Command: "llamas", Matrix: &pipeline.Matrix{}},
stepVerify: &pipeline.CommandStep{Command: "llamas", Matrix: nil},
},
{
name: "matrix sign empty verify empty",
stepSign: &pipeline.CommandStep{Command: "llamas", Matrix: &pipeline.Matrix{}},
stepVerify: &pipeline.CommandStep{Command: "llamas", Matrix: &pipeline.Matrix{}},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

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: *tc.stepSign,
RepositoryURL: fakeRepositoryURL,
}
toVerify := &CommandStepWithInvariants{
CommandStep: *tc.stepVerify,
RepositoryURL: fakeRepositoryURL,
}

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

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

func TestSignatureStability(t *testing.T) {
t.Parallel()
ctx := context.Background()
Expand Down
6 changes: 6 additions & 0 deletions step_command_matrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ type Matrix struct {
RemainingFields map[string]any `yaml:",inline"`
}

// IsEmpty reports whether the matrix is empty (is nil, or has no setup,
// no adjustments, and no other data within it).
func (m *Matrix) IsEmpty() bool {
return m == nil || (len(m.Setup) == 0 && len(m.Adjustments) == 0 && len(m.RemainingFields) == 0)
}

// UnmarshalOrdererd unmarshals from either []any or *ordered.MapSA.
func (m *Matrix) UnmarshalOrdered(o any) error {
switch src := o.(type) {
Expand Down