// This defines the top-level policy evaluation API. // To the extent possible, the interface of the fuctions provided // here is intended to be completely unambiguous, and stable for users // to rely on. package signature import ( "context" "github.com/containers/image/types" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) // PolicyRequirementError is an explanatory text for rejecting a signature or an image. type PolicyRequirementError string func (err PolicyRequirementError) Error() string { return string(err) } // signatureAcceptanceResult is the principal value returned by isSignatureAuthorAccepted. type signatureAcceptanceResult string const ( sarAccepted signatureAcceptanceResult = "sarAccepted" sarRejected signatureAcceptanceResult = "sarRejected" sarUnknown signatureAcceptanceResult = "sarUnknown" ) // PolicyRequirement is a rule which must be satisfied by at least one of the signatures of an image. // The type is public, but its definition is private. type PolicyRequirement interface { // FIXME: For speed, we should support creating per-context state (not stored in the PolicyRequirement), to cache // costly initialization like creating temporary GPG home directories and reading files. // Setup() (someState, error) // Then, the operations below would be done on the someState object, not directly on a PolicyRequirement. // isSignatureAuthorAccepted, given an image and a signature blob, returns: // - sarAccepted if the signature has been verified against the appropriate public key // (where "appropriate public key" may depend on the contents of the signature); // in that case a parsed Signature should be returned. // - sarRejected if the signature has not been verified; // in that case error must be non-nil, and should be an PolicyRequirementError if evaluation // succeeded but the result was rejection. // - sarUnknown if if this PolicyRequirement does not deal with signatures. // NOTE: sarUnknown should not be returned if this PolicyRequirement should make a decision but something failed. // Returning sarUnknown and a non-nil error value is invalid. // WARNING: This makes the signature contents acceptable for futher processing, // but it does not necessarily mean that the contents of the signature are // consistent with local policy. // For example: // - Do not use a true value to determine whether to run // a container based on this image; use IsRunningImageAllowed instead. // - Just because a signature is accepted does not automatically mean the contents of the // signature are authorized to run code as root, or to affect system or cluster configuration. isSignatureAuthorAccepted(image types.UnparsedImage, sig []byte) (signatureAcceptanceResult, *Signature, error) // isRunningImageAllowed returns true if the requirement allows running an image. // If it returns false, err must be non-nil, and should be an PolicyRequirementError if evaluation // succeeded but the result was rejection. // WARNING: This validates signatures and the manifest, but does not download or validate the // layers. Users must validate that the layers match their expected digests. isRunningImageAllowed(image types.UnparsedImage) (bool, error) } // PolicyReferenceMatch specifies a set of image identities accepted in PolicyRequirement. // The type is public, but its implementation is private. type PolicyReferenceMatch interface { // matchesDockerReference decides whether a specific image identity is accepted for an image // (or, usually, for the image's Reference().DockerReference()). Note that // image.Reference().DockerReference() may be nil. matchesDockerReference(image types.UnparsedImage, signatureDockerReference string) bool } // PolicyContext encapsulates a policy and possible cached state // for speeding up its evaluation. type PolicyContext struct { Policy *Policy state policyContextState // Internal consistency checking } // policyContextState is used internally to verify the users are not misusing a PolicyContext. type policyContextState string const ( pcInvalid policyContextState = "" pcInitializing policyContextState = "Initializing" pcReady policyContextState = "Ready" pcInUse policyContextState = "InUse" pcDestroying policyContextState = "Destroying" pcDestroyed policyContextState = "Destroyed" ) // changeContextState changes pc.state, or fails if the state is unexpected func (pc *PolicyContext) changeState(expected, new policyContextState) error { if pc.state != expected { return errors.Errorf(`"Invalid PolicyContext state, expected "%s", found "%s"`, expected, pc.state) } pc.state = new return nil } // NewPolicyContext sets up and initializes a context for the specified policy. // The policy must not be modified while the context exists. FIXME: make a deep copy? // If this function succeeds, the caller should call PolicyContext.Destroy() when done. func NewPolicyContext(policy *Policy) (*PolicyContext, error) { pc := &PolicyContext{Policy: policy, state: pcInitializing} // FIXME: initialize if err := pc.changeState(pcInitializing, pcReady); err != nil { // Huh?! This should never fail, we didn't give the pointer to anybody. // Just give up and leave unclean state around. return nil, err } return pc, nil } // Destroy should be called when the user of the context is done with it. func (pc *PolicyContext) Destroy() error { if err := pc.changeState(pcReady, pcDestroying); err != nil { return err } // FIXME: destroy return pc.changeState(pcDestroying, pcDestroyed) } // policyIdentityLogName returns a string description of the image identity for policy purposes. // ONLY use this for log messages, not for any decisions! func policyIdentityLogName(ref types.ImageReference) string { return ref.Transport().Name() + ":" + ref.PolicyConfigurationIdentity() } // requirementsForImageRef selects the appropriate requirements for ref. func (pc *PolicyContext) requirementsForImageRef(ref types.ImageReference) PolicyRequirements { // Do we have a PolicyTransportScopes for this transport? transportName := ref.Transport().Name() if transportScopes, ok := pc.Policy.Transports[transportName]; ok { // Look for a full match. identity := ref.PolicyConfigurationIdentity() if req, ok := transportScopes[identity]; ok { logrus.Debugf(` Using transport "%s" policy section %s`, transportName, identity) return req } // Look for a match of the possible parent namespaces. for _, name := range ref.PolicyConfigurationNamespaces() { if req, ok := transportScopes[name]; ok { logrus.Debugf(` Using transport "%s" specific policy section %s`, transportName, name) return req } } // Look for a default match for the transport. if req, ok := transportScopes[""]; ok { logrus.Debugf(` Using transport "%s" policy section ""`, transportName) return req } } logrus.Debugf(" Using default policy section") return pc.Policy.Default } // GetSignaturesWithAcceptedAuthor returns those signatures from an image // for which the policy accepts the author (and which have been successfully // verified). // NOTE: This may legitimately return an empty list and no error, if the image // has no signatures or only invalid signatures. // WARNING: This makes the signature contents acceptable for futher processing, // but it does not necessarily mean that the contents of the signature are // consistent with local policy. // For example: // - Do not use a an existence of an accepted signature to determine whether to run // a container based on this image; use IsRunningImageAllowed instead. // - Just because a signature is accepted does not automatically mean the contents of the // signature are authorized to run code as root, or to affect system or cluster configuration. func (pc *PolicyContext) GetSignaturesWithAcceptedAuthor(image types.UnparsedImage) (sigs []*Signature, finalErr error) { if err := pc.changeState(pcReady, pcInUse); err != nil { return nil, err } defer func() { if err := pc.changeState(pcInUse, pcReady); err != nil { sigs = nil finalErr = err } }() logrus.Debugf("GetSignaturesWithAcceptedAuthor for image %s", policyIdentityLogName(image.Reference())) reqs := pc.requirementsForImageRef(image.Reference()) // FIXME: rename Signatures to UnverifiedSignatures // FIXME: pass context.Context unverifiedSignatures, err := image.Signatures(context.TODO()) if err != nil { return nil, err } res := make([]*Signature, 0, len(unverifiedSignatures)) for sigNumber, sig := range unverifiedSignatures { var acceptedSig *Signature // non-nil if accepted rejected := false // FIXME? Say more about the contents of the signature, i.e. parse it even before verification?! logrus.Debugf("Evaluating signature %d:", sigNumber) interpretingReqs: for reqNumber, req := range reqs { // FIXME: Log the requirement itself? For now, we use just the number. // FIXME: supply state switch res, as, err := req.isSignatureAuthorAccepted(image, sig); res { case sarAccepted: if as == nil { // Coverage: this should never happen logrus.Debugf(" Requirement %d: internal inconsistency: sarAccepted but no parsed contents", reqNumber) rejected = true break interpretingReqs } logrus.Debugf(" Requirement %d: signature accepted", reqNumber) if acceptedSig == nil { acceptedSig = as } else if *as != *acceptedSig { // Coverage: this should never happen // Huh?! Two ways of verifying the same signature blob resulted in two different parses of its already accepted contents? logrus.Debugf(" Requirement %d: internal inconsistency: sarAccepted but different parsed contents", reqNumber) rejected = true acceptedSig = nil break interpretingReqs } case sarRejected: logrus.Debugf(" Requirement %d: signature rejected: %s", reqNumber, err.Error()) rejected = true break interpretingReqs case sarUnknown: if err != nil { // Coverage: this should never happen logrus.Debugf(" Requirement %d: internal inconsistency: sarUnknown but an error message %s", reqNumber, err.Error()) rejected = true break interpretingReqs } logrus.Debugf(" Requirement %d: signature state unknown, continuing", reqNumber) default: // Coverage: this should never happen logrus.Debugf(" Requirement %d: internal inconsistency: unknown result %#v", reqNumber, string(res)) rejected = true break interpretingReqs } } // This also handles the (invalid) case of empty reqs, by rejecting the signature. if acceptedSig != nil && !rejected { logrus.Debugf(" Overall: OK, signature accepted") res = append(res, acceptedSig) } else { logrus.Debugf(" Overall: Signature not accepted") } } return res, nil } // IsRunningImageAllowed returns true iff the policy allows running the image. // If it returns false, err must be non-nil, and should be an PolicyRequirementError if evaluation // succeeded but the result was rejection. // WARNING: This validates signatures and the manifest, but does not download or validate the // layers. Users must validate that the layers match their expected digests. func (pc *PolicyContext) IsRunningImageAllowed(image types.UnparsedImage) (res bool, finalErr error) { if err := pc.changeState(pcReady, pcInUse); err != nil { return false, err } defer func() { if err := pc.changeState(pcInUse, pcReady); err != nil { res = false finalErr = err } }() logrus.Debugf("IsRunningImageAllowed for image %s", policyIdentityLogName(image.Reference())) reqs := pc.requirementsForImageRef(image.Reference()) if len(reqs) == 0 { return false, PolicyRequirementError("List of verification policy requirements must not be empty") } for reqNumber, req := range reqs { // FIXME: supply state allowed, err := req.isRunningImageAllowed(image) if !allowed { logrus.Debugf("Requirement %d: denied, done", reqNumber) return false, err } logrus.Debugf(" Requirement %d: allowed", reqNumber) } // We have tested that len(reqs) != 0, so at least one req must have explicitly allowed this image. logrus.Debugf("Overall: allowed") return true, nil }