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

Verification helpers #72

Merged
merged 9 commits into from
Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 8 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# VS Code
.vscode

# Custom
.cover/
# Code Editors
.vscode
.idea
*.sublime-project
*.sublime-workspace

# Custom
.cover/
.test/
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.17
require (
github.com/go-ldap/ldap/v3 v3.4.3
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/notaryproject/notation-core-go v0.0.0-20220602183001-a7b72555a44b
github.com/notaryproject/notation-core-go v0.0.0-20220630163157-985d8e8f12d1
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.2
github.com/oras-project/artifacts-spec v1.0.0-rc.1
Expand Down
5 changes: 3 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF
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/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/notaryproject/notation-core-go v0.0.0-20220602183001-a7b72555a44b h1:GbSRgRhau3GJEUfaO6o4sdxlRLc4egHCGvKMf1Q3trM=
github.com/notaryproject/notation-core-go v0.0.0-20220602183001-a7b72555a44b/go.mod h1:fsgybHh7eXD0Wg672UGiubSm9OYLSRlGCBUnLrCLaec=
github.com/notaryproject/notation-core-go v0.0.0-20220630163157-985d8e8f12d1 h1:dyquq1dANCeTvYVy3ccpkj2C1vsR24kjMNBcgbERXVc=
github.com/notaryproject/notation-core-go v0.0.0-20220630163157-985d8e8f12d1/go.mod h1:n+UjcUoYhvawO/JW5JfZerUUsGbHYTd4wH8ndGeeyas=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
Expand Down
7 changes: 6 additions & 1 deletion notation.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// Media type for Notary payload for OCI artifacts, which contains an artifact descriptor.
const MediaTypePayload = "application/vnd.cncf.notary.payload.v1+json"

// Descriptor describes the content signed or to be signed.
// Descriptor describes the artifact that needs to be signed.
type Descriptor struct {
// The media type of the targeted content.
MediaType string `json:"mediaType"`
Expand All @@ -33,6 +33,11 @@ func (d Descriptor) Equal(t Descriptor) bool {
return d.MediaType == t.MediaType && d.Digest == t.Digest && d.Size == t.Size
}

// Payload describes the content that gets signed.
type Payload struct {
TargetArtifact Descriptor `json:"targetArtifact"`
}

// SignOptions contains parameters for Signer.Sign.
type SignOptions struct {
// Expiry identifies the expiration time of the resulted signature.
Expand Down
49 changes: 49 additions & 0 deletions verification/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package verification

// ErrorVerificationInconclusive is used when signature verification fails due to a runtime error (e.g. a network error)
type ErrorVerificationInconclusive struct {
msg string
}

func (e ErrorVerificationInconclusive) Error() string {
if e.msg != "" {
return e.msg
}
return "signature verification was inclusive due to an unexpected error"
}

// ErrorNoApplicableTrustPolicy is used when there is no trust policy that applies to the given artifact
type ErrorNoApplicableTrustPolicy struct {
msg string
}

func (e ErrorNoApplicableTrustPolicy) Error() string {
if e.msg != "" {
return e.msg
}
return "there is no applicable trust policy for the given artifact"
}

// ErrorSignatureRetrievalFailed is used when notation is unable to retrieve the digital signature/s for the given artifact
type ErrorSignatureRetrievalFailed struct {
gokarnm marked this conversation as resolved.
Show resolved Hide resolved
msg string
}

func (e ErrorSignatureRetrievalFailed) Error() string {
if e.msg != "" {
return e.msg
}
return "unable to retrieve the digital signature from the registry"
}

// ErrorVerificationFailed is used when it is determined that the digital signature/s is not valid for the given artifact
type ErrorVerificationFailed struct {
msg string
}

func (e ErrorVerificationFailed) Error() string {
if e.msg != "" {
return e.msg
}
return "signature verification failed"
}
51 changes: 51 additions & 0 deletions verification/helpers.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,49 @@
package verification

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

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

func loadPolicyDocument(policyDocumentPath string) (*PolicyDocument, error) {
policyDocument := &PolicyDocument{}
jsonFile, err := os.Open(policyDocumentPath)
if err != nil {
return nil, err
}
defer jsonFile.Close()
err = json.NewDecoder(jsonFile).Decode(policyDocument)
if err != nil {
return nil, err
}
return policyDocument, nil
}

func loadX509TrustStores(policy *TrustPolicy, trustStoreBasePath string) (map[string]*X509TrustStore, error) {
var result = make(map[string]*X509TrustStore)
for _, trustStore := range policy.TrustStores {
if result[trustStore] != nil {
// we loaded this trust store already
continue
}
i := strings.Index(trustStore, ":")
prefix := trustStore[:i]
name := trustStore[i+1:]
x509TrustStore, err := LoadX509TrustStore(filepath.Join(trustStoreBasePath, prefix, name))
if err != nil {
return nil, err
}
result[trustStore] = x509TrustStore
}
return result, nil
}

// 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 {
Expand All @@ -32,6 +68,21 @@ func getArtifactPathFromUri(artifactUri string) (string, error) {
return artifactPath, nil
}

func getArtifactDigestFromUri(artifactUri string) (string, error) {
invalidUriErr := 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@sha256:digest", artifactUri)
i := strings.LastIndex(artifactUri, "@")
if i < 0 || i+1 == len(artifactUri) {
return "", invalidUriErr
}

j := strings.LastIndex(artifactUri[i+1:], ":")
if j < 0 || j+1 == len(artifactUri[i+1:]) {
return "", invalidUriErr
}

return artifactUri[i+1:], nil
}
Comment on lines +71 to +84
Copy link
Contributor

Choose a reason for hiding this comment

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

This block of code is workable but fragile. Try registry.ParseReference().

Copy link
Contributor

Choose a reason for hiding this comment

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

Note: A digest is usually with the algorithm, not just the hash value.


// 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
79 changes: 79 additions & 0 deletions verification/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package verification

import (
"encoding/json"
"io/ioutil"
"path/filepath"
"strconv"
"testing"
)

func TestGetArtifactDigestFromUri(t *testing.T) {

tests := []struct {
artifactUri string
digest string
wantErr bool
}{
{"domain.com/repository@sha256:digest", "sha256:digest", false},
{"domain.com:80/repository:digest", "", true},
{"domain.com/repository", "", true},
{"domain.com/repository@sha256", "", true},
{"domain.com/repository@sha256:", "", true},
{"", "", true},
{"domain.com", "", true},
}
for i, tt := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
digest, err := getArtifactDigestFromUri(tt.artifactUri)

if tt.wantErr != (err != nil) {
t.Fatalf("TestGetArtifactDigestFromUri Error: %q WantErr: %v Input: %q", err, tt.wantErr, tt.artifactUri)
} else if digest != tt.digest {
t.Fatalf("TestGetArtifactDigestFromUri Want: %q Got: %v", tt.digest, digest)
}
})
}
}

func TestLoadPolicyDocument(t *testing.T) {
// non-existing policy file
_, err := loadPolicyDocument(filepath.FromSlash("/non/existent"))
if err == nil {
t.Fatalf("TestLoadPolicyDocument should throw error for non existent policy")
}
// existing invalid json file
path := filepath.Join(t.TempDir(), "invalid.json")
err = ioutil.WriteFile(path, []byte(`{"invalid`), 0644)
_, err = loadPolicyDocument(path)
if err == nil {
t.Fatalf("TestLoadPolicyDocument should throw error for invalid policy file. Error: %v", err)
}

// existing policy file
path = filepath.Join(t.TempDir(), "trustpolicy.json")
policyDoc1 := dummyPolicyDocument()
policyJson, _ := json.Marshal(policyDoc1)
err = ioutil.WriteFile(path, policyJson, 0644)
_, err = loadPolicyDocument(path)
if err != nil {
t.Fatalf("TestLoadPolicyDocument should not throw error for an existing policy file. Error: %v", err)
}
}

func TestLoadX509TrustStore(t *testing.T) {
caStore := "ca:valid-trust-store"
anotherStore := "ca:valid-trust-store-2"
dummyPolicy := dummyPolicyStatement()
dummyPolicy.TrustStores = []string{caStore, anotherStore}
trustStores, err := loadX509TrustStores(&dummyPolicy, filepath.FromSlash("testdata/trust-store/"))
if err != nil {
t.Fatalf("TestLoadX509TrustStore should not throw error for a valid trust store. Error: %v", err)
}
if (len(trustStores)) != 2 {
t.Fatalf("TestLoadX509TrustStore must load two trust stores")
}
if trustStores[caStore] == nil || trustStores[anotherStore] == nil {
t.Fatalf("TestLoadX509TrustStore must load trust stores")
}
}
23 changes: 12 additions & 11 deletions verification/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ type TrustPolicy struct {
RegistryScopes []string `json:"registryScopes"`
// SignatureVerification setting for this policy statement
SignatureVerification string `json:"signatureVerification"`
// TrustStore this policy statement uses
TrustStore string `json:"trustStore,omitempty"`
// TrustStores this policy statement uses
TrustStores []string `json:"trustStores,omitempty"`
Comment on lines +35 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we update the spec first then the code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We discussed this change in yesterday's call. Made the code update as it is minor. If the spec deviates, I can come back and make those changes.

Copy link

Choose a reason for hiding this comment

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

@shizhMSFT I'll be submitting a spec PR for this, we discussed the change and use case in Thursday call . Rakesh has been working on verification workflow logic and it was simpler to update it in a single pass.

Copy link

Choose a reason for hiding this comment

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

Related spec update PR.

// TrustedIdentities this policy statement pins
TrustedIdentities []string `json:"trustedIdentities,omitempty"`
}
Expand Down Expand Up @@ -125,9 +125,11 @@ func validateTrustedIdentities(statement TrustPolicy) error {
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)
for _, trustStore := range statement.TrustStores {
i := strings.Index(trustStore, ":")
if i < 0 || !isPresent(trustStore[:i], supportedTrustStorePrefixes) {
return fmt.Errorf("trust policy statement %q uses an unsupported trust store type %q in trust store value %q", statement.Name, trustStore[:i], trustStore)
}
rgnote marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
Expand All @@ -138,7 +140,6 @@ func validateTrustStore(statement TrustPolicy) error {
func (policyDoc *PolicyDocument) ValidatePolicyDocument() error {
// Constants
supportedPolicyVersions := []string{"1.0"}
supportedVerificationLevels := []string{"strict", "permissive", "audit", "skip"}

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

// Verify signature verification level is valid
if !isPresent(statement.SignatureVerification, supportedVerificationLevels) {
if _, err := FindVerificationLevel(statement.SignatureVerification); err != nil {
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 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)
if len(statement.TrustStores) > 0 || len(statement.TrustedIdentities) > 0 {
return fmt.Errorf("trust policy statement %q is set to skip signature verification but configured with trust stores and/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)
if len(statement.TrustStores) == 0 || len(statement.TrustedIdentities) == 0 {
return fmt.Errorf("trust policy statement %q is either missing trust stores or trusted identities, both must be specified", statement.Name)
}

// Verify Trust Store is valid
Expand Down
Loading