-
Notifications
You must be signed in to change notification settings - Fork 2
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 pattern based from/to validity for keys #193
Changes from 7 commits
c6f4792
94ed370
6fcfbca
7b42be3
f2e7e03
47e4a96
c9964cd
c322816
9d88c87
ca5f535
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -133,11 +133,35 @@ func (v *verifier) VerifyLog(ctx context.Context, keyMeta *KeyMetadata, encPaylo | |||||
if err != nil { | ||||||
return fmt.Errorf("TL entry failed verification: %w", err) | ||||||
} | ||||||
if integratedTime.Before(keyMeta.From) { | ||||||
return fmt.Errorf("key %s was not yet valid at TL log time %s (key valid from %s)", keyMeta.ID, integratedTime, keyMeta.From) | ||||||
} | ||||||
if keyMeta.To != nil && !integratedTime.Before(*keyMeta.To) { | ||||||
return fmt.Errorf("key %s was already %s at TL log time %s (key %s at %s)", keyMeta.ID, keyMeta.Status, integratedTime, keyMeta.Status, *keyMeta.To) | ||||||
|
||||||
// important to allow an empty array here so that we don't fail open | ||||||
// search for test 'no match should fail closed' | ||||||
if keyMeta.Expiries != nil { | ||||||
// any repo expirey still on the keys must match the times | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
(there are a few of this same typo in the PR) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed. |
||||||
toMatch := false | ||||||
fromMatch := false | ||||||
// must match at least one - nil is open ended | ||||||
for _, filter := range keyMeta.Expiries { | ||||||
if filter.To == nil || (filter.To != nil && integratedTime.Before(*filter.To)) { | ||||||
toMatch = true | ||||||
} | ||||||
if filter.From == nil || (filter.From != nil && integratedTime.After(*filter.From)) { | ||||||
fromMatch = true | ||||||
} | ||||||
if toMatch && fromMatch { | ||||||
break | ||||||
} | ||||||
} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is right - this will match if the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now we return the first match. |
||||||
if !toMatch || !fromMatch { | ||||||
return fmt.Errorf("Log entry is not within the expiry range of the key: %s", keyMeta.ID) | ||||||
} | ||||||
} else { | ||||||
if keyMeta.To != nil && !integratedTime.Before(*keyMeta.To) { | ||||||
return fmt.Errorf("key %s was already %s at TL log time %s (key %s at %s)", keyMeta.ID, keyMeta.Status, integratedTime, keyMeta.Status, *keyMeta.To) | ||||||
} | ||||||
if keyMeta.From != nil && integratedTime.Before(*keyMeta.From) { | ||||||
return fmt.Errorf("key %s was not yet valid at TL log time %s (key valid from %s)", keyMeta.ID, integratedTime, keyMeta.From) | ||||||
} | ||||||
} | ||||||
return nil | ||||||
} |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -7,9 +7,12 @@ import ( | |||||||
"fmt" | ||||||||
"os" | ||||||||
"path/filepath" | ||||||||
"regexp" | ||||||||
|
||||||||
"github.com/distribution/reference" | ||||||||
"github.com/docker-library/bashbrew/manifest" | ||||||||
"github.com/docker/attest/attestation" | ||||||||
v1 "github.com/google/go-containerregistry/pkg/v1" | ||||||||
intoto "github.com/in-toto/in-toto-golang/in_toto" | ||||||||
"github.com/open-policy-agent/opa/ast" | ||||||||
"github.com/open-policy-agent/opa/rego" | ||||||||
|
@@ -259,7 +262,7 @@ func (regoOpts *RegoFnOpts) fetchInTotoAttestations(rCtx rego.BuiltinContext, pr | |||||||
return set, nil | ||||||||
} | ||||||||
|
||||||||
// because we don't control the signature here (blame rego) | ||||||||
// because we don't control the function signature here (blame rego) | ||||||||
// nolint:gocritic | ||||||||
func (regoOpts *RegoFnOpts) verifyInTotoEnvelope(rCtx rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) { | ||||||||
env := new(attestation.Envelope) | ||||||||
|
@@ -272,6 +275,11 @@ func (regoOpts *RegoFnOpts) verifyInTotoEnvelope(rCtx rego.BuiltinContext, envTe | |||||||
if err != nil { | ||||||||
return nil, fmt.Errorf("failed to cast verifier options: %w", err) | ||||||||
} | ||||||||
|
||||||||
err = regoOpts.filterRepoExpiries(rCtx.Context, opts) | ||||||||
if err != nil { | ||||||||
return nil, fmt.Errorf("failed to filter repo expiries: %w", err) | ||||||||
} | ||||||||
payload, err := attestation.VerifyDSSE(rCtx.Context, regoOpts.attestationVerifier, env, opts) | ||||||||
if err != nil { | ||||||||
return nil, fmt.Errorf("failed to verify envelope: %w", err) | ||||||||
|
@@ -302,6 +310,72 @@ func (regoOpts *RegoFnOpts) verifyInTotoEnvelope(rCtx rego.BuiltinContext, envTe | |||||||
return ast.NewTerm(value), nil | ||||||||
} | ||||||||
|
||||||||
func (regoOpts *RegoFnOpts) filterRepoExpiries(ctx context.Context, opts *attestation.VerifyOptions) error { | ||||||||
// remove any keys that match the pattern but not the platform | ||||||||
imageName, err := regoOpts.attestationResolver.ImageName(ctx) | ||||||||
if err != nil { | ||||||||
return fmt.Errorf("failed to resolve image name: %w", err) | ||||||||
} | ||||||||
parsed, err := reference.ParseNormalizedNamed(imageName) | ||||||||
if err != nil { | ||||||||
return fmt.Errorf("failed to parse image name: %w", err) | ||||||||
} | ||||||||
imageName = parsed.Name() | ||||||||
platform, err := regoOpts.attestationResolver.ImagePlatform(ctx) | ||||||||
if err != nil { | ||||||||
return fmt.Errorf("failed to get image platform: %w", err) | ||||||||
} | ||||||||
for i := range opts.Keys { | ||||||||
key := opts.Keys[i] | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason not to do
Suggested change
Is this a linting rule? |
||||||||
// if there are NO custom expiries, assume key can be checked as normal | ||||||||
if len(key.Expiries) == 0 { | ||||||||
continue | ||||||||
} | ||||||||
if key.From != nil || key.To != nil { | ||||||||
return fmt.Errorf("error key has 'from' or 'to' time set which is not supported when `expiries` is set") | ||||||||
} | ||||||||
// when there are custom expiries, we only keep those that match the image name and platform | ||||||||
// so that the log verifier can check the from/to times | ||||||||
expiries := make([]*attestation.KeyExpiry, 0) | ||||||||
for j := range key.Expiries { | ||||||||
expiry := key.Expiries[j] | ||||||||
if len(expiry.Patterns) == 0 { | ||||||||
return fmt.Errorf("error need at least one expiry pattern") | ||||||||
} | ||||||||
for _, pattern := range expiry.Patterns { | ||||||||
if pattern == "" { | ||||||||
return fmt.Errorf("error empty expiry pattern") | ||||||||
} | ||||||||
patternRegex, err := regexp.Compile(pattern) | ||||||||
if err != nil { | ||||||||
return fmt.Errorf("error failed to compile expiry repo pattern: %w", err) | ||||||||
} | ||||||||
// if there's an image match, then platforms must match too | ||||||||
if patternRegex.MatchString(imageName) { | ||||||||
// either there are no platforms, or at least one must match | ||||||||
if len(expiry.Platforms) == 0 { | ||||||||
expiries = append(expiries, expiry) | ||||||||
break | ||||||||
} | ||||||||
for _, expiryPlatform := range expiry.Platforms { | ||||||||
parsedPlatform, err := v1.ParsePlatform(expiryPlatform) | ||||||||
if err != nil { | ||||||||
return fmt.Errorf("failed to parse platform %s: %w", expiryPlatform, err) | ||||||||
} | ||||||||
if parsedPlatform.Equals(*platform) { | ||||||||
expiries = append(expiries, expiry) | ||||||||
break | ||||||||
} | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the intent here to break the outer loop, like on line 358 above? You can name the loop with a label and Assuming I've got that right, I think this answers my previous question - if every time we append to Along the same lines, it might be better not to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've refactored this whole abstract because it stinks :) |
||||||||
} | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
// if this is empty, then the time check later will fail on the key | ||||||||
key.Expiries = expiries | ||||||||
} | ||||||||
return nil | ||||||||
} | ||||||||
|
||||||||
// because we don't control the signature here (blame rego) | ||||||||
// nolint:gocritic | ||||||||
func (regoOpts *RegoFnOpts) internalParseLibraryDefinition(_ rego.BuiltinContext, definitionTerm *ast.Term) (*ast.Term, error) { | ||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
config.yaml |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
|
||
version: v1 | ||
kind: policy-mapping | ||
policies: | ||
- id: test | ||
description: "Example of dual policy with per repo key expirey" | ||
files: | ||
- path: policy.rego | ||
- path: config.yaml #auto generated | ||
attestations: | ||
style: attached | ||
rules: | ||
- pattern: "^docker[.]io/library/.*$" | ||
policy-id: test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expiries
is a strange word, at first I thought it was a spelling mistake!So this becomes an additional layer of
from
/to
values on top of the key's mainfrom
/to
value. To me this seems quite complex for key management but I am guessing that it is the only way to handle key usage for platforms and repos?It's really less of an
expiry
object or list ofexpiry
objects as much as it is anapplicability
object that determines which artifacts this key should be used for. I think that naming would also make it more clear what the purpose of it is and separate it from the key's mainfrom
/to
expiry.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm up for some new words here for sure, but I don't understand your comment about complexity. The problem we're solving here is complex, so the solution needs to account for that, and I think it does that in a clear and concise way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering if we should deprecate the to/from at the key level and only use the
expiries
(orvalidity_ranges
?) because with emptyplatforms
and a wildcard for the pattern it covers the current behaviour....