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 more validations to trust-policy parsing #43

Merged
merged 4 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ module github.com/notaryproject/notation-go
go 1.17

require (
github.com/go-ldap/ldap/v3 v3.4.3
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go-ldap has a dependency on github.com/Azure/go-ntlmssp, but the functionality we need, DN validation does not. Is there a way to avoid this transitive dependency? Is the transitive dependency dropped during build as we do not use any types from github.com/Azure/go-ntlmssp?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, final build will only include the specific methods and their dependencies. Although I could not confirm this with official Go docs, I found multiple online forums confirming this. Seems like https://github.com/jondot/goweight could help us see what's included in the final build. I added it to my TODO list to revisit later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unpopular opinion: Pulling in github.com/go-ldap/ldap, which is a library to talk with LDAP servers, just for validating a DN, seems like using a sledgehammer to crack a nut.

I would argue it's better just to copy ldap.ParseDN, and related tests, into notation-go and remove that dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qmuntal If we consume the whole package in our builds (like in java), then I agree with what you said. Thanks to Go static linking, our build will only include dn.ParseDN and it's dependencies. I prefer to keep a dependency on go-ldap to ensure we get the updates and security fixes. Also, go-ldap is the most popular library for ldap in Go, I'm not too worried about having a dependency on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides, I want to give them the credit rather than simply copying their code :)

Copy link
Member

@qmuntal qmuntal May 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not that concerned about binary size, as Go is really good at prunning unnecessary code. I'm worried about having a sane dependency tree. Keeping it to the bare minimum is always a plus, even if it means duplicating code.

That said, this particular dependency is not a red line for me.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One concern I had was, we would see patch updates to this package more frequently from go-ldap/ldap package than we'd be actually impacted, as we are using a single type from the whole package. What is generally the process to handle patch updates to dependencies, will dependabot make this simpler?

@rgnote it's possible to copy a code files into our repo and still provide correct attribution to source repo.

I'd prefer copying the code in this case, it's not a one way decision, we can change it the other way if we wanted to later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking this in #45

github.com/golang-jwt/jwt/v4 v4.4.1
github.com/opencontainers/go-digest v1.0.0
)

require (
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
)
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ=
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.3 h1:JCKUtJPIcyOuG7ctGabLKMgIlKnGumD/iGjuWeEruDI=
github.com/go-ldap/ldap/v3 v3.4.3/go.mod h1:7LdHfVt6iIOESVEe3Bs4Jp2sHEKgDeduAhgM1/f9qmo=
github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ=
github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
74 changes: 74 additions & 0 deletions verification/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package verification

import (
"fmt"
"regexp"
"strings"

ldapv3 "github.com/go-ldap/ldap/v3"
)

// isPresent is a utility function to check if a string exists in an array
func isPresent(val string, values []string) bool {
for _, v := range values {
if v == val {
return true
}
}
return false
}

// 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
// https://github.com/distribution/distribution/blob/main/reference/regexp.go#L31
domainRegexp := regexp.MustCompile(`^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?$`)
repositoryRegexp := regexp.MustCompile(`^[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`)
errorMessage := "registry scope %q is not valid, make sure it is the fully qualified registry URL without the scheme/protocol. e.g domain.com/my/repository"
firstSlash := strings.Index(scope, "/")
if firstSlash < 0 {
return fmt.Errorf(errorMessage, scope)
}
domain := scope[:firstSlash]
repository := scope[firstSlash+1:]

if domain == "" || repository == "" || !domainRegexp.MatchString(domain) || !repositoryRegexp.MatchString(repository) {
return fmt.Errorf(errorMessage, scope)
}

// No errors
return nil
}

// validateDistinguishedName validates if a DN name parsable and follows Notary V2 rules
func validateDistinguishedName(name string) error {
Copy link

@gokarnm gokarnm May 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this method should move into notation-core-go. All X509 related parsing, validation, cert chain validation logic should be pushed there.

On second thought, this is Notary V2 specific DN validation based on what we allow in trust policy, so seems appropriate here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this method and validateRegistryScopeFormat be part of policy.go, they are only used in the context of validating a trust policy statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These helpers are for policy validation but I wanted to move them to a helper file so that policy.go is cleaner and deals with higher level functionality like policy verification. There is no side effect of having these methods in a helper class as Go package is a closure of everything under the package directory.

mandatoryFields := []string{"C", "ST", "O"}
rDnCount := make(map[string]int)
dn, err := ldapv3.ParseDN(name)

if err != nil {
return fmt.Errorf("distinguished name (DN) %q is not valid, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard", name)
}

for _, rdn := range dn.RDNs {
for _, attribute := range rdn.Attributes {
rDnCount[attribute.Type]++
}
}

// Verify there are no duplicate RDNs (multi-valdued RDNs are not supported)
for key := range rDnCount {
if rDnCount[key] > 1 {
return fmt.Errorf("distinguished name (DN) %q has duplicate RDN attribute for %q, DN can only have unique RDN attributes", name, key)
}
}

// Verify mandatory fields are present
for _, field := range mandatoryFields {
if rDnCount[field] != 1 {
return fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum", name, field)
}
}
// No errors
return nil
}
149 changes: 105 additions & 44 deletions verification/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
"strings"
)

const (
wildcard = "*"
x509Subject = "x509.subject"
)

// PolicyDocument represents a trustPolicy.json document
type PolicyDocument struct {
// Version of the policy document
Expand All @@ -33,23 +38,93 @@ type TrustPolicy struct {
TrustedIdentities []string `json:"trustedIdentities,omitempty"`
}

func isPresent(val string, values []string) bool {
for _, v := range values {
if v == val {
return true
// validateRegistryScopes validates if the policy document is following the Notary V2 spec rules for registry scopes
func validateRegistryScopes(policyDoc *PolicyDocument) error {
registryScopeCount := make(map[string]int)

for _, statement := range policyDoc.TrustPolicies {
// Verify registry scopes are valid
if len(statement.RegistryScopes) == 0 {
return fmt.Errorf("trust policy statement %q has zero registry scopes, it must specify registry scopes with at least one value", statement.Name)
}
if len(statement.RegistryScopes) > 1 && isPresent(wildcard, statement.RegistryScopes) {
return fmt.Errorf("trust policy statement %q uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values", statement.Name)
}
for _, scope := range statement.RegistryScopes {
if scope != wildcard {
if err := validateRegistryScopeFormat(scope); err != nil {
return err
}
}
registryScopeCount[scope]++
}
}

// Verify one policy statement per registry scope
for key := range registryScopeCount {
if registryScopeCount[key] > 1 {
return fmt.Errorf("registry scope %q is present in multiple trust policy statements, one registry scope value can only be associated with one statement", key)
}
}

// No error
return nil
}

// validateRegistryScopes validates if the policy statement is following the Notary V2 spec rules for trusted identities
func validateTrustedIdentities(statement TrustPolicy) error {

// If there is a wildcard in trusted identies, there shouldn't be any other identities
if len(statement.TrustedIdentities) > 1 && isPresent(wildcard, statement.TrustedIdentities) {
return fmt.Errorf("trust policy statement %q uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values", statement.Name)
}

// If there are trusted identities, verify they are valid
for _, identity := range statement.TrustedIdentities {
if identity == "" {
return fmt.Errorf("trust policy statement %q has an empty trusted identity", statement.Name)
}

if identity != wildcard {
i := strings.Index(identity, ":")
if i < 0 {
return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity prefix", statement.Name, identity)
}

identityPrefix := identity[:i]
identityValue := identity[i+1:]

// notation natively supports x509.subject identities only
if identityPrefix == x509Subject {
if err := validateDistinguishedName(identityValue); err != nil {
return err
}
}
}
}

// No error
return nil
}

// validateTrustStore validates if the policy statement is following the Notary V2 spec rules for truststores
func validateTrustStore(statement TrustPolicy) error {
supportedTrustStorePrefixes := []string{"ca"}

i := strings.Index(statement.TrustStore, ":")
if i < 0 || !isPresent(statement.TrustStore[:i], supportedTrustStorePrefixes) {
return fmt.Errorf("trust policy statement %q uses an unsupported trust store type %q in trust store value %q", statement.Name, statement.TrustStore[:i], statement.TrustStore)
}
return false

return nil
}

// 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 {
// Constants
wildcard := "*"
supportedPolicyVersions := []string{"1.0"}
supportedVerificationPresets := []string{"strict", "permissive", "audit", "skip"}
supportedTrustStorePrefixes := []string{"ca"}

// Validate Version
if !isPresent(policyDoc.Version, supportedPolicyVersions) {
Expand All @@ -60,8 +135,9 @@ func ValidatePolicyDocument(policyDoc *PolicyDocument) error {
if len(policyDoc.TrustPolicies) == 0 {
return errors.New("trust policy document can not have zero trust policy statements")
}

policyStatementNameCount := make(map[string]int)
registryScopeCount := make(map[string]int)

for _, statement := range policyDoc.TrustPolicies {

// Verify statement name is valid
Expand All @@ -70,45 +146,37 @@ func ValidatePolicyDocument(policyDoc *PolicyDocument) error {
}
policyStatementNameCount[statement.Name]++

// Verify registry scopes are valid
if len(statement.RegistryScopes) == 0 {
return fmt.Errorf("trust policy statement %q has zero registry scopes, it must specify registry scopes with at least one value", statement.Name)
}
if len(statement.RegistryScopes) > 1 && isPresent(wildcard, statement.RegistryScopes) {
return fmt.Errorf("trust policy statement %q uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values", statement.Name)
}
for _, scope := range statement.RegistryScopes {
registryScopeCount[scope]++
}

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

// Any signature verification other than "skip" needs a trust store
if statement.SignatureVerification != "skip" && (statement.TrustStore == "" || len(statement.TrustedIdentities) == 0) {
return fmt.Errorf("trust policy statement %q is either missing a trust store or trusted identities, both must be specified", statement.Name)
}
// Any signature verification other than "skip" needs a trust store and trusted identities
if statement.SignatureVerification == "skip" {
if statement.TrustStore != "" || len(statement.TrustedIdentities) > 0 {
return fmt.Errorf("trust policy statement %q is set to skip signature verification but configured with a trust store or trusted identities, remove them if signature verification needs to be skipped", statement.Name)
}
} else {
if statement.TrustStore == "" || len(statement.TrustedIdentities) == 0 {
return fmt.Errorf("trust policy statement %q is either missing a trust store or trusted identities, both must be specified", statement.Name)
}

// Verify trust store type is valid if it is present (trust store is optional for "skip" signature verification)
if statement.TrustStore != "" {
i := strings.Index(statement.TrustStore, ":")
if i < 0 || !isPresent(statement.TrustStore[:i], supportedTrustStorePrefixes) {
return fmt.Errorf("trust policy statement %q uses an unsupported trust store type %q in trust store value %q", statement.Name, statement.TrustStore[:i], statement.TrustStore)
// Verify Trust Store is valid
if err := validateTrustStore(statement); err != nil {
return err
}
}

// If there are trusted identities, verify they are not empty
for _, identity := range statement.TrustedIdentities {
if identity == "" {
return fmt.Errorf("trust policy statement %q has an empty trusted identity", statement.Name)
// Verify Trusted Identities are valid
if err := validateTrustedIdentities(statement); err != nil {
return err
}
}
// If there is a wildcard in trusted identies, there shouldn't be any other identities
if len(statement.TrustedIdentities) > 1 && isPresent(wildcard, statement.TrustedIdentities) {
return fmt.Errorf("trust policy statement %q uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values", statement.Name)
}

}

// Verify registry scopes are valid
if err := validateRegistryScopes(policyDoc); err != nil {
return err
}

// Verify unique policy statement names across the policy document
Expand All @@ -118,13 +186,6 @@ func ValidatePolicyDocument(policyDoc *PolicyDocument) error {
}
}

// Verify one policy statement per registry scope
for key := range registryScopeCount {
if registryScopeCount[key] > 1 {
return fmt.Errorf("registry scope %q is present in multiple trust policy statements, one registry scope value can only be associated with one statement", key)
}
}

// No errors
return nil
}
Loading