Skip to content

Commit

Permalink
Verification helpers (#72)
Browse files Browse the repository at this point in the history
* Add verification helpers
* fix build

Signed-off-by: rgnote <[email protected]>
  • Loading branch information
rgnote authored Jul 7, 2022
1 parent 4a649a9 commit 3d22fbc
Show file tree
Hide file tree
Showing 21 changed files with 351 additions and 80 deletions.
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 {
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
}

// 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"`
// 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)
}
}

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

0 comments on commit 3d22fbc

Please sign in to comment.