Skip to content

Commit

Permalink
Add GetApplicableTrustPolicy function (#51)
Browse files Browse the repository at this point in the history
* Add GetApplicableTrustPolicy function
* move artifact path parsing to a separate method

Signed-off-by: rgnote <5878554+rgnote@users.noreply.github.com>
  • Loading branch information
rgnote authored Jun 17, 2022
1 parent d75cdbd commit a4fdff4
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 32 deletions.
14 changes: 14 additions & 0 deletions verification/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ func isPresent(val string, values []string) bool {
return false
}

func getArtifactPathFromUri(artifactUri string) (string, error) {
// TODO support more types of URI like "domain.com/repository", "domain.com/repository:tag", "domain.com/repository@sha256:digest"
i := strings.LastIndex(artifactUri, ":")
if i < 0 {
return "", fmt.Errorf("artifact URI %q could not be parsed, make sure it is the fully qualified OCI artifact URI without the scheme/protocol. e.g domain.com:80/my/repository:digest", artifactUri)
}

artifactPath := artifactUri[:i]
if err := validateRegistryScopeFormat(artifactPath); err != nil {
return "", err
}
return artifactPath, nil
}

// validateRegistryScopeFormat validates if a scope is following the format defined in distribution spec
func validateRegistryScopeFormat(scope string) error {
// Domain and Repository regexes are adapted from distribution implementation
Expand Down
49 changes: 45 additions & 4 deletions verification/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,10 @@ func validateTrustStore(statement TrustPolicy) error {

// ValidatePolicyDocument validates a policy document according to it's version's rule set.
// if any rule is violated, returns an error
func ValidatePolicyDocument(policyDoc *PolicyDocument) error {
func (policyDoc *PolicyDocument) ValidatePolicyDocument() error {
// Constants
supportedPolicyVersions := []string{"1.0"}
supportedVerificationPresets := []string{"strict", "permissive", "audit", "skip"}
supportedVerificationLevels := []string{"strict", "permissive", "audit", "skip"}

// Validate Version
if !isPresent(policyDoc.Version, supportedPolicyVersions) {
Expand All @@ -160,8 +160,8 @@ func ValidatePolicyDocument(policyDoc *PolicyDocument) error {
}
policyStatementNameCount[statement.Name]++

// Verify signature verification preset is valid
if !isPresent(statement.SignatureVerification, supportedVerificationPresets) {
// Verify signature verification level is valid
if !isPresent(statement.SignatureVerification, supportedVerificationLevels) {
return fmt.Errorf("trust policy statement %q uses unsupported signatureVerification value %q", statement.Name, statement.SignatureVerification)
}

Expand Down Expand Up @@ -203,3 +203,44 @@ func ValidatePolicyDocument(policyDoc *PolicyDocument) error {
// No errors
return nil
}

// getApplicableTrustPolicy returns a pointer to the deep copied TrustPolicy statement that applies to the given
// registry URI. If no applicable trust policy is found, returns an error
// see https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#selecting-a-trust-policy-based-on-artifact-uri
func (policyDoc *PolicyDocument) getApplicableTrustPolicy(artifactUri string) (*TrustPolicy, error) {

artifactPath, err := getArtifactPathFromUri(artifactUri)
if err != nil {
return nil, err
}

var wildcardPolicy *TrustPolicy
var applicablePolicy *TrustPolicy
for _, policyStatement := range policyDoc.TrustPolicies {
if isPresent(wildcard, policyStatement.RegistryScopes) {
wildcardPolicy = policyStatement.deepCopy() // we need to deep copy because we can't use the loop variable address. see https://stackoverflow.com/a/45967429
} else if isPresent(artifactPath, policyStatement.RegistryScopes) {
applicablePolicy = policyStatement.deepCopy()
}
}

if applicablePolicy != nil {
// a policy with exact match for registry URI takes precedence over a wildcard (*) policy.
return applicablePolicy, nil
} else if wildcardPolicy != nil {
return wildcardPolicy, nil
} else {
return nil, fmt.Errorf("artifact %q has no applicable trust policy", artifactUri)
}
}

// deepCopy returns a pointer to the deeply copied TrustPolicy
func (t *TrustPolicy) deepCopy() *TrustPolicy {
localCopy := t
localCopy.RegistryScopes = make([]string, len(t.RegistryScopes))
copy(localCopy.RegistryScopes, t.RegistryScopes)

localCopy.TrustedIdentities = make([]string, len(t.TrustedIdentities))
copy(localCopy.TrustedIdentities, t.TrustedIdentities)
return localCopy
}
101 changes: 73 additions & 28 deletions verification/policy_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package verification

import (
"fmt"
"testing"
)

Expand Down Expand Up @@ -59,7 +60,7 @@ func TestValidateValidPolicyDocument(t *testing.T) {
policyStatement4,
policyStatement5,
}
err := ValidatePolicyDocument(&policyDoc)
err := policyDoc.ValidatePolicyDocument()
if err != nil {
t.Fatalf("validation failed on a good policy document. Error : %q", err)
}
Expand All @@ -73,7 +74,7 @@ func TestValidateTrustedIdentities(t *testing.T) {
policyStatement := dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{"C=US, ST=WA, O=wabbit-network.io, OU=org1"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err := ValidatePolicyDocument(&policyDoc)
err := policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"C=US, ST=WA, O=wabbit-network.io, OU=org1\" without an identity prefix" {
t.Fatalf("trusted identity without a prefix should return error")
}
Expand All @@ -83,7 +84,7 @@ func TestValidateTrustedIdentities(t *testing.T) {
policyStatement = dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{"unknown:my-trusted-idenity"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err != nil {
t.Fatalf("unknown identity prefix should not return an error. Error: %q", err)
}
Expand All @@ -94,7 +95,7 @@ func TestValidateTrustedIdentities(t *testing.T) {
invalidDN := "x509.subject:,,,"
policyStatement.TrustedIdentities = []string{invalidDN}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "distinguished name (DN) \",,,\" is not valid, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard" {
t.Fatalf("invalid x509.subject identity should return error. Error : %q", err)
}
Expand All @@ -105,7 +106,7 @@ func TestValidateTrustedIdentities(t *testing.T) {
invalidDN = "x509.subject:C=US,C=IN"
policyStatement.TrustedIdentities = []string{invalidDN}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "distinguished name (DN) \"C=US,C=IN\" has duplicate RDN attribute for \"C\", DN can only have unique RDN attributes" {
t.Fatalf("invalid x509.subject identity should return error. Error : %q", err)
}
Expand All @@ -116,7 +117,7 @@ func TestValidateTrustedIdentities(t *testing.T) {
invalidDN = "x509.subject:C=US,ST=WA"
policyStatement.TrustedIdentities = []string{invalidDN}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "distinguished name (DN) \"C=US,ST=WA\" has no mandatory RDN attribute for \"O\", it must contain 'C', 'ST', and 'O' RDN attributes at a minimum" {
t.Fatalf("invalid x509.subject identity should return error. Error : %q", err)
}
Expand All @@ -127,7 +128,7 @@ func TestValidateTrustedIdentities(t *testing.T) {
validDN := "x509.subject:C=US,ST=WA,O=MyOrg,CustomRDN=CustomValue"
policyStatement.TrustedIdentities = []string{validDN}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err != nil {
t.Fatalf("valid x509.subject identity should not return error. Error : %q", err)
}
Expand All @@ -141,7 +142,7 @@ func TestValidateTrustedIdentities(t *testing.T) {
validDN4 := "x509.subject:C=US,ST=WA,O=My Org,1.3.6.1.4.1.1466.0=#04024869"
policyStatement.TrustedIdentities = []string{validDN1, validDN2, validDN3, validDN4}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err != nil {
t.Fatalf("valid x509.subject identity should not return error. Error : %q", err)
}
Expand All @@ -153,7 +154,7 @@ func TestValidateTrustedIdentities(t *testing.T) {
validDN2 = "x509.subject:C=US,ST=WA,O=MyOrg,X=Y"
policyStatement.TrustedIdentities = []string{validDN1, validDN2}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has overlapping x509 trustedIdentities, \"x509.subject:C=US,ST=WA,O=MyOrg\" overlaps with \"x509.subject:C=US,ST=WA,O=MyOrg,X=Y\"" {
t.Fatalf("overlapping DNs should return error")
}
Expand All @@ -164,7 +165,7 @@ func TestValidateTrustedIdentities(t *testing.T) {
multiValduedRDN := "x509.subject:C=US+ST=WA,O=MyOrg"
policyStatement.TrustedIdentities = []string{multiValduedRDN}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "distinguished name (DN) \"C=US+ST=WA,O=MyOrg\" has multi-valued RDN attributes, remove multi-valued RDN attributes as they are not supported" {
t.Fatalf("multi-valued RDN should return error. Error : %q", err)
}
Expand All @@ -183,7 +184,7 @@ func TestInvalidRegistryScopes(t *testing.T) {
policyStatement := dummyPolicyStatement()
policyStatement.RegistryScopes = []string{scope}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err := ValidatePolicyDocument(&policyDoc)
err := policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "registry scope \""+scope+"\" is not valid, make sure it is the fully qualified registry URL without the scheme/protocol. e.g domain.com/my/repository" {
t.Fatalf("invalid registry scope should return error. Error : %q", err)
}
Expand All @@ -202,29 +203,29 @@ func TestValidRegistryScopes(t *testing.T) {
policyStatement := dummyPolicyStatement()
policyStatement.RegistryScopes = []string{scope}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err := ValidatePolicyDocument(&policyDoc)
err := policyDoc.ValidatePolicyDocument()
if err != nil {
t.Fatalf("valid registry scope should not return error. Error : %q", err)
}
}
}

// TestValidatePolicyDocument calls verification.ValidatePolicyDocument
// TestValidatePolicyDocument calls policyDoc.ValidatePolicyDocument()
// and tests various validations on policy eliments
func TestValidateInvalidPolicyDocument(t *testing.T) {

// Invalid Version
policyDoc := dummyPolicyDocument()
policyDoc.Version = "invalid"
err := ValidatePolicyDocument(&policyDoc)
err := policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy document uses unsupported version \"invalid\"" {
t.Fatalf("invalid version should return error")
}

// No Policy Satements
policyDoc = dummyPolicyDocument()
policyDoc.TrustPolicies = nil
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy document can not have zero trust policy statements" {
t.Fatalf("zero policy statements should return error")
}
Expand All @@ -234,7 +235,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement := dummyPolicyStatement()
policyStatement.Name = ""
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "a trust policy statement is missing a name, every statement requires a name" {
t.Fatalf("policy statement with no name should return an error")
}
Expand All @@ -244,7 +245,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement = dummyPolicyStatement()
policyStatement.RegistryScopes = nil
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has zero registry scopes, it must specify registry scopes with at least one value" {
t.Fatalf("policy statement with registry scopes should return error")
}
Expand All @@ -255,7 +256,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement2 := dummyPolicyStatement()
policyStatement2.Name = "test-statement-name-2"
policyDoc.TrustPolicies = []TrustPolicy{policyStatement1, policyStatement2}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "registry scope \"registry.acme-rockets.io/software/net-monitor\" is present in multiple trust policy statements, one registry scope value can only be associated with one statement" {
t.Fatalf("Policy statements with same registry scope should return error %q", err)
}
Expand All @@ -265,7 +266,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement = dummyPolicyStatement()
policyStatement.RegistryScopes = []string{"*", "registry.acme-rockets.io/software/net-monitor"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values" {
t.Fatalf("policy statement with more than a wildcard registry scope should return error")
}
Expand All @@ -275,7 +276,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement = dummyPolicyStatement()
policyStatement.SignatureVerification = "invalid"
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses unsupported signatureVerification value \"invalid\"" {
t.Fatalf("policy statement with invalid SignatureVerification should return error")
}
Expand All @@ -285,7 +286,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement = dummyPolicyStatement()
policyStatement.TrustStore = ""
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" is either missing a trust store or trusted identities, both must be specified" {
t.Fatalf("strict SignatureVerification should have a trust store")
}
Expand All @@ -295,7 +296,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement = dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" is either missing a trust store or trusted identities, both must be specified" {
t.Fatalf("strict SignatureVerification should have trusted identities")
}
Expand All @@ -305,7 +306,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement = dummyPolicyStatement()
policyStatement.SignatureVerification = "skip"
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" is set to skip signature verification but configured with a trust store or trusted identities, remove them if signature verification needs to be skipped" {
t.Fatalf("strict SignatureVerification should have trusted identities")
}
Expand All @@ -315,7 +316,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement = dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{""}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has an empty trusted identity" {
t.Fatalf("policy statement with empty trusted identity should return error")
}
Expand All @@ -326,7 +327,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement.SignatureVerification = "skip"
policyStatement.TrustStore = ""
policyStatement.TrustedIdentities = []string{}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err != nil {
t.Fatalf("skip SignatureVerification should not require a trust store or trusted identities")
}
Expand All @@ -336,7 +337,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement = dummyPolicyStatement()
policyStatement.TrustStore = "invalid:test-trust-store"
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses an unsupported trust store type \"invalid\" in trust store value \"invalid:test-trust-store\"" {
t.Fatalf("policy statement with invalid trust store type should return error")
}
Expand All @@ -346,7 +347,7 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement = dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{"*", "test-identity"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values" {
t.Fatalf("policy statement with more than a wildcard trusted identity should return error")
}
Expand All @@ -357,8 +358,52 @@ func TestValidateInvalidPolicyDocument(t *testing.T) {
policyStatement2 = dummyPolicyStatement()
policyStatement2.RegistryScopes = []string{"registry.acme-rockets.io/software/legacy/metrics"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement1, policyStatement2}
err = ValidatePolicyDocument(&policyDoc)
err = policyDoc.ValidatePolicyDocument()
if err == nil || err.Error() != "multiple trust policy statements use the same name \"test-statement-name\", statement names must be unique" {
t.Fatalf("policy statements with same name should return error")
}
}

// TestApplicableTrustPolicy tests filtering policies against registry scopes
func TestApplicableTrustPolicy(t *testing.T) {
policyDoc := dummyPolicyDocument()

policyStatement := dummyPolicyStatement()
policyStatement.Name = "test-statement-name-1"
registryScope := "registry.wabbit-networks.io/software/unsigned/net-utils"
registryUri := fmt.Sprintf("%s:hash", registryScope)
policyStatement.RegistryScopes = []string{registryScope}
policyStatement.SignatureVerification = "strict"

policyDoc.TrustPolicies = []TrustPolicy{
policyStatement,
}
// existing Registry Scope
policy, err := policyDoc.getApplicableTrustPolicy(registryUri)
if policy.Name != policyStatement.Name || err != nil {
t.Fatalf("getApplicableTrustPolicy should return %q for registry scope %q", policyStatement.Name, registryScope)
}

// non-existing Registry Scope
policy, err = policyDoc.getApplicableTrustPolicy("non.existing.scope/repo:hash")
if policy != nil || err == nil || err.Error() != "artifact \"non.existing.scope/repo:hash\" has no applicable trust policy" {
t.Fatalf("getApplicableTrustPolicy should return nil for non existing registry scope")
}

// wildcard registry scope
wildcardStatement := dummyPolicyStatement()
wildcardStatement.Name = "test-statement-name-2"
wildcardStatement.RegistryScopes = []string{"*"}
wildcardStatement.TrustStore = ""
wildcardStatement.TrustedIdentities = []string{}
wildcardStatement.SignatureVerification = "skip"

policyDoc.TrustPolicies = []TrustPolicy{
policyStatement,
wildcardStatement,
}
policy, err = policyDoc.getApplicableTrustPolicy("some.registry.that/has.no.policy:hash")
if policy.Name != wildcardStatement.Name || err != nil {
t.Fatalf("getApplicableTrustPolicy should return wildcard policy for registry scope \"some.registry.that/has.no.policy\"")
}
}

0 comments on commit a4fdff4

Please sign in to comment.