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

Add support for signature verification predicates #285

Merged
merged 12 commits into from
May 6, 2021
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,25 @@ if:
not_matches:
- "^(docs|style|chore): (\\w| )+$"

# "has_valid_signatures" is satisfied if the commits in the pull request
# all have git commit signatures that have been verified by GitHub
has_valid_signatures: true
bluekeyes marked this conversation as resolved.
Show resolved Hide resolved

# "has_valid_signatures_by" is satisfied if the commits in the pull request
# all have git commit signatures that have been verified by GitHub, and
# the authenticated signatures are attributed to a user in the users list
# or belong to a user in any of the listed organizations or teams.
has_valid_signatures_by:
users: ["user1", "user2", ...]
organizations: ["org1", "org2", ...]
teams: ["org1/team1", "org2/team2", ...]

# "has_valid_signatures_by_keys" is satisfied if the commits in the pull request
# all have git commit signatures that have been verified by GitHub, and
# the authenticated signatures are attributed to a GPG key with an ID in the list.
has_valid_signatures_by_keys:
key_ids: ["3AA5C34371567BD2"]

# "options" specifies a set of restrictions on approvals. If the block does not
# exist, the default values are used.
options:
Expand Down
16 changes: 16 additions & 0 deletions policy/predicate/predicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type Predicates struct {
HasLabels *HasLabels `yaml:"has_labels"`

Title *Title `yaml:"title"`

HasValidSignatures *HasValidSignatures `yaml:"has_valid_signatures"`
HasValidSignaturesBy *HasValidSignaturesBy `yaml:"has_valid_signatures_by"`
HasValidSignaturesByKeys *HasValidSignaturesByKeys `yaml:"has_valid_signatures_by_keys"`
}

func (p *Predicates) Predicates() []Predicate {
Expand Down Expand Up @@ -81,5 +85,17 @@ func (p *Predicates) Predicates() []Predicate {
ps = append(ps, Predicate(p.Title))
}

if p.HasValidSignatures != nil {
ps = append(ps, Predicate(p.HasValidSignatures))
}

if p.HasValidSignaturesBy != nil {
ps = append(ps, Predicate(p.HasValidSignaturesBy))
}

if p.HasValidSignaturesByKeys != nil {
ps = append(ps, Predicate(p.HasValidSignaturesByKeys))
}

return ps
}
153 changes: 153 additions & 0 deletions policy/predicate/signature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright 2021 Palantir Technologies, Inc.
//
// 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 predicate

import (
"context"
"fmt"

"github.com/pkg/errors"

"github.com/palantir/policy-bot/policy/common"
"github.com/palantir/policy-bot/pull"
)

type HasValidSignatures bool

var _ Predicate = HasValidSignatures(false)

func (pred HasValidSignatures) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
commits, err := prctx.Commits()
if err != nil {
return false, "", errors.Wrap(err, "failed to get commits")
}

for _, c := range commits {
valid, desc := hasValidSignature(ctx, c)
if !valid {
if pred {
return false, desc, nil
}
return true, "", nil
}
}

if pred {
return true, "", nil
}
return false, "All commits are signed and have valid signatures", nil
}

func (pred HasValidSignatures) Trigger() common.Trigger {
return common.TriggerCommit
}

type HasValidSignaturesBy struct {
common.Actors `yaml:",inline"`
}

var _ Predicate = &HasValidSignaturesBy{}

func (pred *HasValidSignaturesBy) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
commits, err := prctx.Commits()
if err != nil {
return false, "", errors.Wrap(err, "failed to get commits")
}

signers := make(map[string]struct{})

for _, c := range commits {
valid, desc := hasValidSignature(ctx, c)
if !valid {
return false, desc, nil
}
asvoboda marked this conversation as resolved.
Show resolved Hide resolved
signers[c.Signature.Signer] = struct{}{}
}

for signer := range signers {
member, err := pred.IsActor(ctx, prctx, signer)
if err != nil {
return false, "", err
}
if !member {
return false, fmt.Sprintf("Contributor %q does not meet the required membership conditions for signing", signer), nil
}
}

return true, "", nil
}

func (pred *HasValidSignaturesBy) Trigger() common.Trigger {
return common.TriggerCommit
}

type HasValidSignaturesByKeys struct {
KeyIDs []string `yaml:"key_ids"`
}

var _ Predicate = &HasValidSignaturesByKeys{}

func (pred *HasValidSignaturesByKeys) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
commits, err := prctx.Commits()
if err != nil {
return false, "", errors.Wrap(err, "failed to get commits")
}

keys := make(map[string]struct{})

for _, c := range commits {
valid, desc := hasValidSignature(ctx, c)
if !valid {
return false, desc, nil
}
// Only GPG signatures are valid for this predicate
bluekeyes marked this conversation as resolved.
Show resolved Hide resolved
switch c.Signature.Type {
case pull.SignatureGpg:
keys[c.Signature.KeyID] = struct{}{}
default:
return false, fmt.Sprintf("Commit %.10s signature is not a GPG signature", c.SHA), nil
}
}

for key := range keys {
isValidKey := false
for _, acceptedKey := range pred.KeyIDs {
if key == acceptedKey {
isValidKey = true
bluekeyes marked this conversation as resolved.
Show resolved Hide resolved
break
}
}
if !isValidKey {
return false, fmt.Sprintf("Key %q does not meet the required key conditions for signing", key), nil
}
}

return true, "", nil
}

func (pred *HasValidSignaturesByKeys) Trigger() common.Trigger {
return common.TriggerCommit
}

func hasValidSignature(ctx context.Context, commit *pull.Commit) (bool, string) {
if commit.Signature == nil {
return false, fmt.Sprintf("Commit %.10s has no signature", commit.SHA)
}
if !commit.Signature.IsValid {
reason := commit.Signature.State
return false, fmt.Sprintf("Commit %.10s has an invalid signature due to %s", commit.SHA, reason)
}
return true, ""
}
Loading