Vendor in notary v0.2.0

Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
This commit is contained in:
Riyaz Faizullabhoy 2016-02-25 13:40:00 -08:00
parent 6fa5576e30
commit 84dc2d9e70
45 changed files with 1755 additions and 1195 deletions

View file

@ -169,7 +169,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install notary server
ENV NOTARY_VERSION docker-v1.10-5
ENV NOTARY_VERSION v0.2.0
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \

View file

@ -119,7 +119,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install notary server
ENV NOTARY_VERSION docker-v1.10-5
ENV NOTARY_VERSION v0.2.0
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \

View file

@ -135,7 +135,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install notary server
ENV NOTARY_VERSION docker-v1.10-5
ENV NOTARY_VERSION v0.2.0
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \

View file

@ -127,16 +127,16 @@ RUN set -x \
go build -o /usr/local/bin/registry-v2-schema1 github.com/docker/distribution/cmd/registry \
&& rm -rf "$GOPATH"
# TODO update this when we upgrade to Go 1.5.1+
# Install notary server
#ENV NOTARY_VERSION docker-v1.10-5
#RUN set -x \
# && export GOPATH="$(mktemp -d)" \
# && git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \
# && (cd "$GOPATH/src/github.com/docker/notary" && git checkout -q "$NOTARY_VERSION") \
# && GOPATH="$GOPATH/src/github.com/docker/notary/Godeps/_workspace:$GOPATH" \
# go build -o /usr/local/bin/notary-server github.com/docker/notary/cmd/notary-server \
# && rm -rf "$GOPATH"
ENV NOTARY_VERSION v0.2.0
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \
&& (cd "$GOPATH/src/github.com/docker/notary" && git checkout -q "$NOTARY_VERSION") \
&& GOPATH="$GOPATH/src/github.com/docker/notary/Godeps/_workspace:$GOPATH" \
go build -o /usr/local/bin/notary-server github.com/docker/notary/cmd/notary-server \
&& rm -rf "$GOPATH"
# Get the "docker-py" source so we can run their integration tests
ENV DOCKER_PY_COMMIT e2878cbcc3a7eef99917adc1be252800b0e41ece

View file

@ -108,7 +108,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install notary server
ENV NOTARY_VERSION docker-v1.10-5
ENV NOTARY_VERSION v0.2.0
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \

View file

@ -52,7 +52,7 @@ clone git github.com/docker/distribution 7b66c50bb7e0e4b3b83f8fd134a9f6ea4be08b5
clone git github.com/vbatts/tar-split v0.9.11
# get desired notary commit, might also need to be updated in Dockerfile
clone git github.com/docker/notary docker-v1.10-5
clone git github.com/docker/notary v0.2.0
clone git google.golang.org/grpc 174192fc93efcb188fc8f46ca447f0da606b6885 https://github.com/grpc/grpc-go.git
clone git github.com/miekg/pkcs11 80f102b5cac759de406949c47f0928b99bd64cdf

View file

@ -365,7 +365,7 @@ func (s *DockerTrustSuite) TestCreateWhenCertExpired(c *check.C) {
func (s *DockerTrustSuite) TestTrustedCreateFromBadTrustServer(c *check.C) {
repoName := fmt.Sprintf("%v/dockerclievilcreate/trusted:latest", privateRegistryURL)
evilLocalConfigDir, err := ioutil.TempDir("", "evil-local-config-dir")
evilLocalConfigDir, err := ioutil.TempDir("", "evilcreate-local-config-dir")
c.Assert(err, check.IsNil)
// tag the image and upload it to the private registry
@ -404,12 +404,16 @@ func (s *DockerTrustSuite) TestTrustedCreateFromBadTrustServer(c *check.C) {
c.Assert(err, check.IsNil)
c.Assert(string(out), checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push:\n%s", out))
// Now, try creating with the original client from this new trust server. This should fail.
// Now, try creating with the original client from this new trust server. This should fallback to our cached timestamp and metadata.
createCmd = exec.Command(dockerBinary, "create", repoName)
s.trustedCmd(createCmd)
out, _, err = runCommandWithOutput(createCmd)
c.Assert(err, check.Not(check.IsNil))
c.Assert(string(out), checker.Contains, "valid signatures did not meet threshold", check.Commentf("Missing expected output on trusted push:\n%s", out))
if err != nil {
c.Fatalf("Error falling back to cached trust data: %s\n%s", err, out)
}
if !strings.Contains(string(out), "Error while downloading remote metadata, using cached timestamp") {
c.Fatalf("Missing expected output on trusted create:\n%s", out)
}
}

View file

@ -135,13 +135,16 @@ func (s *DockerTrustSuite) TestTrustedPullFromBadTrustServer(c *check.C) {
c.Assert(err, check.IsNil, check.Commentf(out))
c.Assert(string(out), checker.Contains, "Signing and pushing trust metadata", check.Commentf(out))
// Now, try pulling with the original client from this new trust server. This should fail.
// Now, try pulling with the original client from this new trust server. This should fall back to cached metadata.
pullCmd = exec.Command(dockerBinary, "pull", repoName)
s.trustedCmd(pullCmd)
out, _, err = runCommandWithOutput(pullCmd)
c.Assert(err, check.NotNil, check.Commentf(out))
c.Assert(string(out), checker.Contains, "valid signatures did not meet threshold", check.Commentf(out))
if err != nil {
c.Fatalf("Error falling back to cached trust data: %s\n%s", err, out)
}
if !strings.Contains(string(out), "Error while downloading remote metadata, using cached timestamp") {
c.Fatalf("Missing expected output on trusted pull:\n%s", out)
}
}
func (s *DockerTrustSuite) TestTrustedPullWithExpiredSnapshot(c *check.C) {

View file

@ -3260,7 +3260,7 @@ func (s *DockerTrustSuite) TestTrustedRunFromBadTrustServer(c *check.C) {
// Windows does not support this functionality
testRequires(c, DaemonIsLinux)
repoName := fmt.Sprintf("%v/dockerclievilrun/trusted:latest", privateRegistryURL)
evilLocalConfigDir, err := ioutil.TempDir("", "evil-local-config-dir")
evilLocalConfigDir, err := ioutil.TempDir("", "evilrun-local-config-dir")
if err != nil {
c.Fatalf("Failed to create local temp dir")
}
@ -3316,15 +3316,15 @@ func (s *DockerTrustSuite) TestTrustedRunFromBadTrustServer(c *check.C) {
c.Fatalf("Missing expected output on trusted push:\n%s", out)
}
// Now, try running with the original client from this new trust server. This should fail.
// Now, try running with the original client from this new trust server. This should fallback to our cached timestamp and metadata.
runCmd = exec.Command(dockerBinary, "run", repoName)
s.trustedCmd(runCmd)
out, _, err = runCommandWithOutput(runCmd)
if err == nil {
c.Fatalf("Expected to fail on this run due to different remote data: %s\n%s", err, out)
}
if !strings.Contains(string(out), "valid signatures did not meet threshold") {
if err != nil {
c.Fatalf("Error falling back to cached trust data: %s\n%s", err, out)
}
if !strings.Contains(string(out), "Error while downloading remote metadata, using cached timestamp") {
c.Fatalf("Missing expected output on trusted push:\n%s", out)
}
}

View file

@ -237,7 +237,7 @@ func (s *DockerTrustSuite) setupDelegations(c *check.C, repoName, pwd string) {
if err != nil {
c.Fatalf("Error creating delegation key: %s\n", err)
}
err = nRepo.AddDelegation("targets/releases", 1, []data.PublicKey{delgKey}, []string{""})
err = nRepo.AddDelegation("targets/releases", []data.PublicKey{delgKey}, []string{""})
if err != nil {
c.Fatalf("Error creating delegation: %s\n", err)
}

View file

@ -42,14 +42,6 @@ GO_VERSION = $(shell go version | awk '{print $$3}')
.DELETE_ON_ERROR: cover
.DEFAULT: default
go_version:
ifeq (,$(findstring go1.5.,$(GO_VERSION)))
$(error Requires go version 1.5.x - found $(GO_VERSION))
else
@echo
endif
all: AUTHORS clean fmt vet fmt lint build test binaries
AUTHORS: .git/HEAD
@ -71,7 +63,23 @@ ${PREFIX}/bin/notary-signer: NOTARY_VERSION $(shell find . -type f -name '*.go')
@echo "+ $@"
@godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-signer
vet: go_version
ifeq ($(shell uname -s),Darwin)
${PREFIX}/bin/static/notary-server:
@echo "notary-server: static builds not supported on OS X"
${PREFIX}/bin/static/notary-signer:
@echo "notary-signer: static builds not supported on OS X"
else
${PREFIX}/bin/static/notary-server: NOTARY_VERSION $(shell find . -type f -name '*.go')
@echo "+ $@"
@godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary-server
${PREFIX}/bin/static/notary-signer: NOTARY_VERSION $(shell find . -type f -name '*.go')
@echo "+ $@"
@godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary-signer
endif
vet:
@echo "+ $@"
ifeq ($(shell uname -s), Darwin)
@test -z "$(shell find . -iname *test*.go | grep -v _test.go | grep -v Godeps | xargs echo "This file should end with '_test':" | tee /dev/stderr)"
@ -88,14 +96,24 @@ lint:
@echo "+ $@"
@test -z "$$(golint ./... | grep -v .pb. | grep -v Godeps/_workspace/src/ | tee /dev/stderr)"
build: go_version
# Requires that the following:
# go get -u github.com/client9/misspell/cmd/misspell
#
# be run first
# misspell target, don't include Godeps, binaries, python tests, or git files
misspell:
@echo "+ $@"
@test -z "$$(find . -name '*' | grep -v Godeps/_workspace/src/ | grep -v bin/ | grep -v misc/ | grep -v .git/ | xargs misspell | tee /dev/stderr)"
build:
@echo "+ $@"
@go build -tags "${NOTARY_BUILDTAGS}" -v ${GO_LDFLAGS} ./...
# When running `go test ./...`, it runs all the suites in parallel, which causes
# problems when running with a yubikey
test: TESTOPTS =
test: go_version
test:
@echo Note: when testing with a yubikey plugged in, make sure to include 'TESTOPTS="-p 1"'
@echo "+ $@ $(TESTOPTS)"
@echo
@ -121,7 +139,7 @@ define gocover
$(GO_EXC) test $(OPTS) $(TESTOPTS) -covermode="$(COVERMODE)" -coverprofile="$(COVERDIR)/$(subst /,-,$(1)).$(subst $(_space),.,$(NOTARY_BUILDTAGS)).coverage.txt" "$(1)" || exit 1;
endef
gen-cover: go_version
gen-cover:
@mkdir -p "$(COVERDIR)"
$(foreach PKG,$(PKGS),$(call gocover,$(PKG)))
rm -f "$(COVERDIR)"/*testutils*.coverage.txt
@ -150,7 +168,10 @@ covmerge:
clean-protos:
@rm proto/*.pb.go
binaries: go_version ${PREFIX}/bin/notary-server ${PREFIX}/bin/notary ${PREFIX}/bin/notary-signer
binaries: ${PREFIX}/bin/notary-server ${PREFIX}/bin/notary ${PREFIX}/bin/notary-signer
@echo "+ $@"
static: ${PREFIX}/bin/static/notary-server ${PREFIX}/bin/static/notary-signer
@echo "+ $@"
define template
@ -158,7 +179,7 @@ mkdir -p ${PREFIX}/cross/$(1)/$(2);
GOOS=$(1) GOARCH=$(2) CGO_ENABLED=0 go build -o ${PREFIX}/cross/$(1)/$(2)/notary -a -tags "static_build netgo" -installsuffix netgo ${GO_LDFLAGS_STATIC} ./cmd/notary;
endef
cross: go_version
cross:
$(foreach GOARCH,$(GOARCHS),$(foreach GOOS,$(GOOSES),$(call template,$(GOOS),$(GOARCH))))

View file

@ -1,4 +1,5 @@
# Notary [![Circle CI](https://circleci.com/gh/docker/notary/tree/master.svg?style=shield)](https://circleci.com/gh/docker/notary/tree/master)
# Notary
[![Circle CI](https://circleci.com/gh/docker/notary/tree/master.svg?style=shield)](https://circleci.com/gh/docker/notary/tree/master) [![CodeCov](https://codecov.io/github/docker/notary/coverage.svg?branch=master)](https://codecov.io/github/docker/notary)
The Notary project comprises a [server](cmd/notary-server) and a [client](cmd/notary) for running and interacting
with trusted collections.

View file

@ -6,7 +6,7 @@ machine:
post:
# Install many go versions
- gvm install go1.5.1 -B --name=stable
- gvm install go1.6 -B --name=stable
environment:
# Convenient shortcuts to "common" locations
@ -37,10 +37,11 @@ dependencies:
pwd: $BASE_STABLE
post:
# For the stable go version, additionally install linting tools
# For the stable go version, additionally install linting and misspell tools
- >
gvm use stable &&
go get github.com/golang/lint/golint
go get github.com/golang/lint/golint &&
go get -u github.com/client9/misspell/cmd/misspell
test:
pre:
# Output the go versions we are going to test
@ -62,6 +63,10 @@ test:
- gvm use stable && make lint:
pwd: $BASE_STABLE
# MISSPELL
- gvm use stable && make misspell:
pwd: $BASE_STABLE
override:
# Test stable, and report
# hacking this to be parallel

View file

@ -17,7 +17,7 @@ const (
// Types for TufChanges are namespaced by the Role they
// are relevant for. The Root and Targets roles are the
// only ones for which user action can cause a change, as
// all changes in Snapshot and Timestamp are programatically
// all changes in Snapshot and Timestamp are programmatically
// generated base on Root and Targets changes.
const (
TypeRootRole = "role"
@ -88,8 +88,7 @@ type TufDelegation struct {
RemoveKeys []string `json:"remove_keys,omitempty"`
AddPaths []string `json:"add_paths,omitempty"`
RemovePaths []string `json:"remove_paths,omitempty"`
AddPathHashPrefixes []string `json:"add_prefixes,omitempty"`
RemovePathHashPrefixes []string `json:"remove_prefixes,omitempty"`
ClearAllPaths bool `json:"clear_paths,omitempty"`
}
// ToNewRole creates a fresh role object from the TufDelegation data
@ -98,5 +97,5 @@ func (td TufDelegation) ToNewRole(scope string) (*data.Role, error) {
if td.NewName != "" {
name = td.NewName
}
return data.NewRole(name, td.NewThreshold, td.AddKeys.IDs(), td.AddPaths, td.AddPathHashPrefixes)
return data.NewRole(name, td.NewThreshold, td.AddKeys.IDs(), td.AddPaths)
}

View file

@ -9,6 +9,7 @@ import (
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/Sirupsen/logrus"
@ -20,23 +21,13 @@ import (
"github.com/docker/notary/tuf"
tufclient "github.com/docker/notary/tuf/client"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/keys"
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/store"
)
const (
maxSize = 5 << 20
"github.com/docker/notary/tuf/utils"
)
func init() {
data.SetDefaultExpiryTimes(
map[string]int{
"root": 3650,
"targets": 1095,
"snapshot": 1095,
},
)
data.SetDefaultExpiryTimes(notary.NotaryDefaultExpiries)
}
// ErrRepoNotInitialized is returned when trying to publish an uninitialized
@ -118,7 +109,6 @@ func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper,
nRepo.tufRepoPath,
"metadata",
"json",
"",
)
if err != nil {
return nil, err
@ -218,11 +208,16 @@ func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...st
return fmt.Errorf("invalid format for root key: %s", privKey.Algorithm())
}
kdb := keys.NewDB()
err = addKeyForRole(kdb, data.CanonicalRootRole, rootKey)
if err != nil {
return err
}
var (
rootRole = data.NewBaseRole(
data.CanonicalRootRole,
notary.MinThreshold,
rootKey,
)
timestampRole data.BaseRole
snapshotRole data.BaseRole
targetsRole data.BaseRole
)
// we want to create all the local keys first so we don't have to
// make unnecessary network calls
@ -232,8 +227,19 @@ func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...st
if err != nil {
return err
}
if err := addKeyForRole(kdb, role, key); err != nil {
return err
switch role {
case data.CanonicalSnapshotRole:
snapshotRole = data.NewBaseRole(
role,
notary.MinThreshold,
key,
)
case data.CanonicalTargetsRole:
targetsRole = data.NewBaseRole(
role,
notary.MinThreshold,
key,
)
}
}
for _, role := range remotelyManagedKeys {
@ -244,14 +250,31 @@ func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...st
}
logrus.Debugf("got remote %s %s key with keyID: %s",
role, key.Algorithm(), key.ID())
if err := addKeyForRole(kdb, role, key); err != nil {
return err
switch role {
case data.CanonicalSnapshotRole:
snapshotRole = data.NewBaseRole(
role,
notary.MinThreshold,
key,
)
case data.CanonicalTimestampRole:
timestampRole = data.NewBaseRole(
role,
notary.MinThreshold,
key,
)
}
}
r.tufRepo = tuf.NewRepo(kdb, r.CryptoService)
r.tufRepo = tuf.NewRepo(r.CryptoService)
err = r.tufRepo.InitRoot(false)
err = r.tufRepo.InitRoot(
rootRole,
timestampRole,
snapshotRole,
targetsRole,
false,
)
if err != nil {
logrus.Debug("Error on InitRoot: ", err.Error())
return err
@ -305,96 +328,6 @@ func addChange(cl *changelist.FileChangelist, c changelist.Change, roles ...stri
return nil
}
// AddDelegation creates a new changelist entry to add a delegation to the repository
// when the changelist gets applied at publish time. This does not do any validation
// other than checking the name of the delegation to add - all that will happen
// at publish time.
func (r *NotaryRepository) AddDelegation(name string, threshold int,
delegationKeys []data.PublicKey, paths []string) error {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Adding delegation "%s" with threshold %d, and %d keys\n`,
name, threshold, len(delegationKeys))
tdJSON, err := json.Marshal(&changelist.TufDelegation{
NewThreshold: threshold,
AddKeys: data.KeyList(delegationKeys),
AddPaths: paths,
})
if err != nil {
return err
}
template := changelist.NewTufChange(
changelist.ActionCreate,
name,
changelist.TypeTargetsDelegation,
"", // no path
tdJSON,
)
return addChange(cl, template, name)
}
// RemoveDelegation creates a new changelist entry to remove a delegation from
// the repository when the changelist gets applied at publish time.
// This does not validate that the delegation exists, since one might exist
// after applying all changes.
func (r *NotaryRepository) RemoveDelegation(name string, keyIDs, paths []string, removeAll bool) error {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Removing delegation "%s"\n`, name)
var template *changelist.TufChange
// We use the Delete action only for force removal, Update is used for removing individual keys and paths
if removeAll {
template = changelist.NewTufChange(
changelist.ActionDelete,
name,
changelist.TypeTargetsDelegation,
"", // no path
nil, // deleting role, no data needed
)
} else {
tdJSON, err := json.Marshal(&changelist.TufDelegation{
RemoveKeys: keyIDs,
RemovePaths: paths,
})
if err != nil {
return err
}
template = changelist.NewTufChange(
changelist.ActionUpdate,
name,
changelist.TypeTargetsDelegation,
"", // no path
tdJSON,
)
}
return addChange(cl, template, name)
}
// AddTarget creates new changelist entries to add a target to the given roles
// in the repository when the changelist gets applied at publish time.
// If roles are unspecified, the default role is "targets".
@ -452,10 +385,24 @@ func (r *NotaryRepository) ListTargets(roles ...string) ([]*TargetWithRole, erro
}
targets := make(map[string]*TargetWithRole)
for _, role := range roles {
// we don't need to do anything special with removing role from
// roles because listSubtree always processes role and only excludes
// descendant delegations that appear in roles.
r.listSubtree(targets, role, roles...)
// Define an array of roles to skip for this walk (see IMPORTANT comment above)
skipRoles := utils.StrSliceRemove(roles, role)
// Define a visitor function to populate the targets map in priority order
listVisitorFunc := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
// We found targets so we should try to add them to our targets map
for targetName, targetMeta := range tgt.Signed.Targets {
// Follow the priority by not overriding previously set targets
// and check that this path is valid with this role
if _, ok := targets[targetName]; ok || !validRole.CheckPaths(targetName) {
continue
}
targets[targetName] =
&TargetWithRole{Target: Target{Name: targetName, Hashes: targetMeta.Hashes, Length: targetMeta.Length}, Role: validRole.Name}
}
return nil
}
r.tufRepo.WalkTargets("", role, listVisitorFunc, skipRoles...)
}
var targetList []*TargetWithRole
@ -466,34 +413,6 @@ func (r *NotaryRepository) ListTargets(roles ...string) ([]*TargetWithRole, erro
return targetList, nil
}
func (r *NotaryRepository) listSubtree(targets map[string]*TargetWithRole, role string, exclude ...string) {
excl := make(map[string]bool)
for _, r := range exclude {
excl[r] = true
}
roles := []string{role}
for len(roles) > 0 {
role = roles[0]
roles = roles[1:]
tgts, ok := r.tufRepo.Targets[role]
if !ok {
// not every role has to exist
continue
}
for name, meta := range tgts.Signed.Targets {
if _, ok := targets[name]; !ok {
targets[name] = &TargetWithRole{
Target: Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, Role: role}
}
}
for _, d := range tgts.Signed.Delegations.Roles {
if !excl[d.Name] {
roles = append(roles, d.Name)
}
}
}
}
// GetTargetByName returns a target given a name. If no roles are passed
// it uses the targets role and does a search of the entire delegation
// graph, finding the first entry in a breadth first search of the delegations.
@ -502,7 +421,7 @@ func (r *NotaryRepository) listSubtree(targets map[string]*TargetWithRole, role
// will be returned
// See the IMPORTANT section on ListTargets above. Those roles also apply here.
func (r *NotaryRepository) GetTargetByName(name string, roles ...string) (*TargetWithRole, error) {
c, err := r.Update(false)
_, err := r.Update(false)
if err != nil {
return nil, err
}
@ -510,11 +429,30 @@ func (r *NotaryRepository) GetTargetByName(name string, roles ...string) (*Targe
if len(roles) == 0 {
roles = append(roles, data.CanonicalTargetsRole)
}
var resultMeta data.FileMeta
var resultRoleName string
var foundTarget bool
for _, role := range roles {
meta, foundRole := c.TargetMeta(role, name, roles...)
if meta != nil {
return &TargetWithRole{
Target: Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, Role: foundRole}, nil
// Define an array of roles to skip for this walk (see IMPORTANT comment above)
skipRoles := utils.StrSliceRemove(roles, role)
// Define a visitor function to find the specified target
getTargetVisitorFunc := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
if tgt == nil {
return nil
}
// We found the target and validated path compatibility in our walk,
// so we should stop our walk and set the resultMeta and resultRoleName variables
if resultMeta, foundTarget = tgt.Signed.Targets[name]; foundTarget {
resultRoleName = validRole.Name
return tuf.StopWalk{}
}
return nil
}
err = r.tufRepo.WalkTargets(name, role, getTargetVisitorFunc, skipRoles...)
// Check that we didn't error, and that we assigned to our target
if err == nil && foundTarget {
return &TargetWithRole{Target: Target{Name: name, Hashes: resultMeta.Hashes, Length: resultMeta.Length}, Role: resultRoleName}, nil
}
}
return nil, fmt.Errorf("No trust data for %s", name)
@ -532,45 +470,6 @@ func (r *NotaryRepository) GetChangelist() (changelist.Changelist, error) {
return cl, nil
}
// GetDelegationRoles returns the keys and roles of the repository's delegations
func (r *NotaryRepository) GetDelegationRoles() ([]*data.Role, error) {
// Update state of the repo to latest
if _, err := r.Update(false); err != nil {
return nil, err
}
// All top level delegations (ex: targets/level1) are stored exclusively in targets.json
targets, ok := r.tufRepo.Targets[data.CanonicalTargetsRole]
if !ok {
return nil, store.ErrMetaNotFound{Resource: data.CanonicalTargetsRole}
}
allDelegations := targets.Signed.Delegations.Roles
// make a copy for traversing nested delegations
delegationsList := make([]*data.Role, len(allDelegations))
copy(delegationsList, allDelegations)
// Now traverse to lower level delegations (ex: targets/level1/level2)
for len(delegationsList) > 0 {
// Pop off first delegation to traverse
delegation := delegationsList[0]
delegationsList = delegationsList[1:]
// Get metadata
delegationMeta, ok := r.tufRepo.Targets[delegation.Name]
// If we get an error, don't try to traverse further into this subtree because it doesn't exist or is malformed
if !ok {
continue
}
// Add nested delegations to return list and exploration list
allDelegations = append(allDelegations, delegationMeta.Signed.Delegations.Roles...)
delegationsList = append(delegationsList, delegationMeta.Signed.Delegations.Roles...)
}
return allDelegations, nil
}
// RoleWithSignatures is a Role with its associated signatures
type RoleWithSignatures struct {
Signatures []data.Signature
@ -604,7 +503,7 @@ func (r *NotaryRepository) ListRoles() ([]RoleWithSignatures, error) {
case data.CanonicalTimestampRole:
roleWithSig.Signatures = r.tufRepo.Timestamp.Signatures
default:
// If the role isn't a delegation, we should error -- this is only possible if we have invalid keyDB state
// If the role isn't a delegation, we should error -- this is only possible if we have invalid state
if !data.IsDelegation(role.Name) {
return nil, data.ErrInvalidRole{Role: role.Name, Reason: "invalid role name"}
}
@ -705,7 +604,7 @@ func (r *NotaryRepository) Publish() error {
r.tufRepo, data.CanonicalSnapshotRole)
if err == nil {
// Only update the snapshot if we've sucessfully signed it.
// Only update the snapshot if we've successfully signed it.
updatedFiles[data.CanonicalSnapshotRole] = snapshotJSON
} else if _, ok := err.(signed.ErrNoKeys); ok {
// If signing fails due to us not having the snapshot key, then
@ -743,11 +642,10 @@ func (r *NotaryRepository) Publish() error {
// This can also be unified with some cache reading tools from tuf/client.
// This assumes that bootstrapRepo is only used by Publish()
func (r *NotaryRepository) bootstrapRepo() error {
kdb := keys.NewDB()
tufRepo := tuf.NewRepo(kdb, r.CryptoService)
tufRepo := tuf.NewRepo(r.CryptoService)
logrus.Debugf("Loading trusted collection.")
rootJSON, err := r.fileStore.GetMeta("root", 0)
rootJSON, err := r.fileStore.GetMeta("root", -1)
if err != nil {
return err
}
@ -760,7 +658,7 @@ func (r *NotaryRepository) bootstrapRepo() error {
if err != nil {
return err
}
targetsJSON, err := r.fileStore.GetMeta("targets", 0)
targetsJSON, err := r.fileStore.GetMeta("targets", -1)
if err != nil {
return err
}
@ -771,7 +669,7 @@ func (r *NotaryRepository) bootstrapRepo() error {
}
tufRepo.SetTargets("targets", targets)
snapshotJSON, err := r.fileStore.GetMeta("snapshot", 0)
snapshotJSON, err := r.fileStore.GetMeta("snapshot", -1)
if err == nil {
snapshot := &data.SignedSnapshot{}
err = json.Unmarshal(snapshotJSON, snapshot)
@ -854,7 +752,10 @@ func (r *NotaryRepository) Update(forWrite bool) (*tufclient.Client, error) {
}
err = c.Update()
if err != nil {
if notFound, ok := err.(store.ErrMetaNotFound); ok && notFound.Resource == data.CanonicalRootRole {
// notFound.Resource may include a checksum so when the role is root,
// it will be root.json or root.<checksum>.json. Therefore best we can
// do it match a "root." prefix
if notFound, ok := err.(store.ErrMetaNotFound); ok && strings.HasPrefix(notFound.Resource, data.CanonicalRootRole+".") {
return nil, r.errRepositoryNotExist()
}
return nil, err
@ -876,7 +777,7 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl
// try to read root from cache first. We will trust this root
// until we detect a problem during update which will cause
// us to download a new root and perform a rotation.
rootJSON, cachedRootErr := r.fileStore.GetMeta("root", maxSize)
rootJSON, cachedRootErr := r.fileStore.GetMeta("root", -1)
if cachedRootErr == nil {
signedRoot, cachedRootErr = r.validateRoot(rootJSON)
@ -890,7 +791,8 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl
// checking for initialization of the repo).
// if remote store successfully set up, try and get root from remote
tmpJSON, err := remote.GetMeta("root", maxSize)
// We don't have any local data to determine the size of root, so try the maximum (though it is restricted at 100MB)
tmpJSON, err := remote.GetMeta("root", -1)
if err != nil {
// we didn't have a root in cache and were unable to load one from
// the server. Nothing we can do but error.
@ -912,8 +814,7 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl
}
}
kdb := keys.NewDB()
r.tufRepo = tuf.NewRepo(kdb, r.CryptoService)
r.tufRepo = tuf.NewRepo(r.CryptoService)
if signedRoot == nil {
return nil, ErrRepoNotInitialized{}
@ -927,7 +828,6 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl
return tufclient.NewClient(
r.tufRepo,
remote,
kdb,
r.fileStore,
), nil
}
@ -1020,7 +920,7 @@ func (r *NotaryRepository) DeleteTrustData() error {
if err := r.fileStore.RemoveAll(); err != nil {
return fmt.Errorf("error clearing TUF repo data: %v", err)
}
r.tufRepo = tuf.NewRepo(nil, nil)
r.tufRepo = tuf.NewRepo(nil)
// Clear certificates
certificates, err := r.CertStore.GetCertificatesByCN(r.gun)
if err != nil {

View file

@ -0,0 +1,294 @@
package client
import (
"encoding/json"
"fmt"
"path/filepath"
"github.com/Sirupsen/logrus"
"github.com/docker/notary"
"github.com/docker/notary/client/changelist"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/store"
"github.com/docker/notary/tuf/utils"
)
// AddDelegation creates changelist entries to add provided delegation public keys and paths.
// This method composes AddDelegationRoleAndKeys and AddDelegationPaths (each creates one changelist if called).
func (r *NotaryRepository) AddDelegation(name string, delegationKeys []data.PublicKey, paths []string) error {
if len(delegationKeys) > 0 {
err := r.AddDelegationRoleAndKeys(name, delegationKeys)
if err != nil {
return err
}
}
if len(paths) > 0 {
err := r.AddDelegationPaths(name, paths)
if err != nil {
return err
}
}
return nil
}
// AddDelegationRoleAndKeys creates a changelist entry to add provided delegation public keys.
// This method is the simplest way to create a new delegation, because the delegation must have at least
// one key upon creation to be valid since we will reject the changelist while validating the threshold.
func (r *NotaryRepository) AddDelegationRoleAndKeys(name string, delegationKeys []data.PublicKey) error {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Adding delegation "%s" with threshold %d, and %d keys\n`,
name, notary.MinThreshold, len(delegationKeys))
// Defaulting to threshold of 1, since we don't allow for larger thresholds at the moment.
tdJSON, err := json.Marshal(&changelist.TufDelegation{
NewThreshold: notary.MinThreshold,
AddKeys: data.KeyList(delegationKeys),
})
if err != nil {
return err
}
template := newCreateDelegationChange(name, tdJSON)
return addChange(cl, template, name)
}
// AddDelegationPaths creates a changelist entry to add provided paths to an existing delegation.
// This method cannot create a new delegation itself because the role must meet the key threshold upon creation.
func (r *NotaryRepository) AddDelegationPaths(name string, paths []string) error {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Adding %s paths to delegation %s\n`, paths, name)
tdJSON, err := json.Marshal(&changelist.TufDelegation{
AddPaths: paths,
})
if err != nil {
return err
}
template := newCreateDelegationChange(name, tdJSON)
return addChange(cl, template, name)
}
// RemoveDelegationKeysAndPaths creates changelist entries to remove provided delegation key IDs and paths.
// This method composes RemoveDelegationPaths and RemoveDelegationKeys (each creates one changelist if called).
func (r *NotaryRepository) RemoveDelegationKeysAndPaths(name string, keyIDs, paths []string) error {
if len(paths) > 0 {
err := r.RemoveDelegationPaths(name, paths)
if err != nil {
return err
}
}
if len(keyIDs) > 0 {
err := r.RemoveDelegationKeys(name, keyIDs)
if err != nil {
return err
}
}
return nil
}
// RemoveDelegationRole creates a changelist to remove all paths and keys from a role, and delete the role in its entirety.
func (r *NotaryRepository) RemoveDelegationRole(name string) error {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Removing delegation "%s"\n`, name)
template := newDeleteDelegationChange(name, nil)
return addChange(cl, template, name)
}
// RemoveDelegationPaths creates a changelist entry to remove provided paths from an existing delegation.
func (r *NotaryRepository) RemoveDelegationPaths(name string, paths []string) error {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Removing %s paths from delegation "%s"\n`, paths, name)
tdJSON, err := json.Marshal(&changelist.TufDelegation{
RemovePaths: paths,
})
if err != nil {
return err
}
template := newUpdateDelegationChange(name, tdJSON)
return addChange(cl, template, name)
}
// RemoveDelegationKeys creates a changelist entry to remove provided keys from an existing delegation.
// When this changelist is applied, if the specified keys are the only keys left in the role,
// the role itself will be deleted in its entirety.
func (r *NotaryRepository) RemoveDelegationKeys(name string, keyIDs []string) error {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Removing %s keys from delegation "%s"\n`, keyIDs, name)
tdJSON, err := json.Marshal(&changelist.TufDelegation{
RemoveKeys: keyIDs,
})
if err != nil {
return err
}
template := newUpdateDelegationChange(name, tdJSON)
return addChange(cl, template, name)
}
// ClearDelegationPaths creates a changelist entry to remove all paths from an existing delegation.
func (r *NotaryRepository) ClearDelegationPaths(name string) error {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Removing all paths from delegation "%s"\n`, name)
tdJSON, err := json.Marshal(&changelist.TufDelegation{
ClearAllPaths: true,
})
if err != nil {
return err
}
template := newUpdateDelegationChange(name, tdJSON)
return addChange(cl, template, name)
}
func newUpdateDelegationChange(name string, content []byte) *changelist.TufChange {
return changelist.NewTufChange(
changelist.ActionUpdate,
name,
changelist.TypeTargetsDelegation,
"", // no path for delegations
content,
)
}
func newCreateDelegationChange(name string, content []byte) *changelist.TufChange {
return changelist.NewTufChange(
changelist.ActionCreate,
name,
changelist.TypeTargetsDelegation,
"", // no path for delegations
content,
)
}
func newDeleteDelegationChange(name string, content []byte) *changelist.TufChange {
return changelist.NewTufChange(
changelist.ActionDelete,
name,
changelist.TypeTargetsDelegation,
"", // no path for delegations
content,
)
}
// GetDelegationRoles returns the keys and roles of the repository's delegations
// Also converts key IDs to canonical key IDs to keep consistent with signing prompts
func (r *NotaryRepository) GetDelegationRoles() ([]*data.Role, error) {
// Update state of the repo to latest
if _, err := r.Update(false); err != nil {
return nil, err
}
// All top level delegations (ex: targets/level1) are stored exclusively in targets.json
_, ok := r.tufRepo.Targets[data.CanonicalTargetsRole]
if !ok {
return nil, store.ErrMetaNotFound{Resource: data.CanonicalTargetsRole}
}
// make a copy for traversing nested delegations
allDelegations := []*data.Role{}
// Define a visitor function to populate the delegations list and translate their key IDs to canonical IDs
delegationCanonicalListVisitor := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
// For the return list, update with a copy that includes canonicalKeyIDs
// These aren't validated by the validRole
canonicalDelegations, err := translateDelegationsToCanonicalIDs(tgt.Signed.Delegations)
if err != nil {
return err
}
allDelegations = append(allDelegations, canonicalDelegations...)
return nil
}
err := r.tufRepo.WalkTargets("", "", delegationCanonicalListVisitor)
if err != nil {
return nil, err
}
return allDelegations, nil
}
func translateDelegationsToCanonicalIDs(delegationInfo data.Delegations) ([]*data.Role, error) {
canonicalDelegations := make([]*data.Role, len(delegationInfo.Roles))
copy(canonicalDelegations, delegationInfo.Roles)
delegationKeys := delegationInfo.Keys
for i, delegation := range canonicalDelegations {
canonicalKeyIDs := []string{}
for _, keyID := range delegation.KeyIDs {
pubKey, ok := delegationKeys[keyID]
if !ok {
return nil, fmt.Errorf("Could not translate canonical key IDs for %s", delegation.Name)
}
canonicalKeyID, err := utils.CanonicalKeyID(pubKey)
if err != nil {
return nil, fmt.Errorf("Could not translate canonical key IDs for %s: %v", delegation.Name, err)
}
canonicalKeyIDs = append(canonicalKeyIDs, canonicalKeyID)
}
canonicalDelegations[i].KeyIDs = canonicalKeyIDs
}
return canonicalDelegations, nil
}

View file

@ -12,8 +12,8 @@ import (
"github.com/docker/notary/client/changelist"
tuf "github.com/docker/notary/tuf"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/keys"
"github.com/docker/notary/tuf/store"
"github.com/docker/notary/tuf/utils"
)
// Use this to initialize remote HTTPStores from the config settings
@ -22,7 +22,6 @@ func getRemoteStore(baseURL, gun string, rt http.RoundTripper) (store.RemoteStor
baseURL+"/v2/"+gun+"/_trust/tuf/",
"",
"json",
"",
"key",
rt,
)
@ -80,53 +79,51 @@ func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error {
if err != nil {
return err
}
r, err := repo.GetDelegation(c.Scope())
if _, ok := err.(data.ErrNoSuchRole); err != nil && !ok {
// error that wasn't ErrNoSuchRole
return err
}
if err == nil {
// role existed, attempt to merge paths and keys
if err := r.AddPaths(td.AddPaths); err != nil {
return err
}
return repo.UpdateDelegations(r, td.AddKeys)
}
// create brand new role
r, err = td.ToNewRole(c.Scope())
// Try to create brand new role or update one
// First add the keys, then the paths. We can only add keys and paths in this scenario
err = repo.UpdateDelegationKeys(c.Scope(), td.AddKeys, []string{}, td.NewThreshold)
if err != nil {
return err
}
return repo.UpdateDelegations(r, td.AddKeys)
return repo.UpdateDelegationPaths(c.Scope(), td.AddPaths, []string{}, false)
case changelist.ActionUpdate:
td := changelist.TufDelegation{}
err := json.Unmarshal(c.Content(), &td)
if err != nil {
return err
}
r, err := repo.GetDelegation(c.Scope())
delgRole, err := repo.GetDelegationRole(c.Scope())
if err != nil {
return err
}
// We need to translate the keys from canonical ID to TUF ID for compatibility
canonicalToTUFID := make(map[string]string)
for tufID, pubKey := range delgRole.Keys {
canonicalID, err := utils.CanonicalKeyID(pubKey)
if err != nil {
return err
}
canonicalToTUFID[canonicalID] = tufID
}
removeTUFKeyIDs := []string{}
for _, canonID := range td.RemoveKeys {
removeTUFKeyIDs = append(removeTUFKeyIDs, canonicalToTUFID[canonID])
}
// If we specify the only keys left delete the role, else just delete specified keys
if strings.Join(r.KeyIDs, ";") == strings.Join(td.RemoveKeys, ";") && len(td.AddKeys) == 0 {
r := data.Role{Name: c.Scope()}
return repo.DeleteDelegation(r)
if strings.Join(delgRole.ListKeyIDs(), ";") == strings.Join(removeTUFKeyIDs, ";") && len(td.AddKeys) == 0 {
return repo.DeleteDelegation(c.Scope())
}
// if we aren't deleting and the role exists, merge
if err := r.AddPaths(td.AddPaths); err != nil {
err = repo.UpdateDelegationKeys(c.Scope(), td.AddKeys, removeTUFKeyIDs, td.NewThreshold)
if err != nil {
return err
}
if err := r.AddPathHashPrefixes(td.AddPathHashPrefixes); err != nil {
return err
}
r.RemoveKeys(td.RemoveKeys)
r.RemovePaths(td.RemovePaths)
r.RemovePathHashPrefixes(td.RemovePathHashPrefixes)
return repo.UpdateDelegations(r, td.AddKeys)
return repo.UpdateDelegationPaths(c.Scope(), td.AddPaths, td.RemovePaths, td.ClearAllPaths)
case changelist.ActionDelete:
r := data.Role{Name: c.Scope()}
return repo.DeleteDelegation(r)
return repo.DeleteDelegation(c.Scope())
default:
return fmt.Errorf("unsupported action against delegations: %s", c.Action())
}
@ -239,19 +236,6 @@ func getRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKey,
return pubKey, nil
}
// add a key to a KeyDB, and create a role for the key and add it.
func addKeyForRole(kdb *keys.KeyDB, role string, key data.PublicKey) error {
theRole, err := data.NewRole(role, 1, []string{key.ID()}, nil, nil)
if err != nil {
return err
}
kdb.AddKey(key)
if err := kdb.AddRole(theRole); err != nil {
return err
}
return nil
}
// signs and serializes the metadata for a canonical role in a tuf repo to JSON
func serializeCanonicalRole(tufRepo *tuf.Repo, role string) (out []byte, err error) {
var s *data.Signed

View file

@ -1,7 +1,15 @@
package notary
import (
"time"
)
// application wide constants
const (
// MaxDownloadSize is the maximum size we'll download for metadata if no limit is given
MaxDownloadSize int64 = 100 << 20
// MaxTimestampSize is the maximum size of timestamp metadata - 1MiB.
MaxTimestampSize int64 = 1 << 20
// MinRSABitSize is the minimum bit size for RSA keys allowed in notary
MinRSABitSize = 2048
// MinThreshold requires a minimum of one threshold for roles; currently we do not support a higher threshold
@ -14,4 +22,29 @@ const (
Sha256HexSize = 64
// TrustedCertsDir is the directory, under the notary repo base directory, where trusted certs are stored
TrustedCertsDir = "trusted_certificates"
// PrivDir is the directory, under the notary repo base directory, where private keys are stored
PrivDir = "private"
// RootKeysSubdir is the subdirectory under PrivDir where root private keys are stored
RootKeysSubdir = "root_keys"
// NonRootKeysSubdir is the subdirectory under PrivDir where non-root private keys are stored
NonRootKeysSubdir = "tuf_keys"
// Day is a duration of one day
Day = 24 * time.Hour
Year = 365 * Day
// NotaryRootExpiry is the duration representing the expiry time of the Root role
NotaryRootExpiry = 10 * Year
NotaryTargetsExpiry = 3 * Year
NotarySnapshotExpiry = 3 * Year
NotaryTimestampExpiry = 14 * Day
)
// NotaryDefaultExpiries is the construct used to configure the default expiry times of
// the various role files.
var NotaryDefaultExpiries = map[string]time.Duration{
"root": NotaryRootExpiry,
"targets": NotaryTargetsExpiry,
"snapshot": NotarySnapshotExpiry,
"timestamp": NotaryTimestampExpiry,
}

View file

@ -11,8 +11,10 @@ import (
"path/filepath"
"strings"
"github.com/docker/notary"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/tuf/data"
)
const zipMadeByUNIX = 3 << 8
@ -31,14 +33,17 @@ var (
ErrNoKeysFoundForGUN = errors.New("no keys found for specified GUN")
)
// ExportRootKey exports the specified root key to an io.Writer in PEM format.
// ExportKey exports the specified private key to an io.Writer in PEM format.
// The key's existing encryption is preserved.
func (cs *CryptoService) ExportRootKey(dest io.Writer, keyID string) error {
func (cs *CryptoService) ExportKey(dest io.Writer, keyID, role string) error {
var (
pemBytes []byte
err error
)
if role != data.CanonicalRootRole {
keyID = filepath.Join(cs.gun, keyID)
}
for _, ks := range cs.keyStores {
pemBytes, err = ks.ExportKey(keyID)
if err != nil {
@ -59,9 +64,9 @@ func (cs *CryptoService) ExportRootKey(dest io.Writer, keyID string) error {
return nil
}
// ExportRootKeyReencrypt exports the specified root key to an io.Writer in
// ExportKeyReencrypt exports the specified private key to an io.Writer in
// PEM format. The key is reencrypted with a new passphrase.
func (cs *CryptoService) ExportRootKeyReencrypt(dest io.Writer, keyID string, newPassphraseRetriever passphrase.Retriever) error {
func (cs *CryptoService) ExportKeyReencrypt(dest io.Writer, keyID string, newPassphraseRetriever passphrase.Retriever) error {
privateKey, role, err := cs.GetPrivateKey(keyID)
if err != nil {
return err
@ -103,14 +108,41 @@ func (cs *CryptoService) ImportRootKey(source io.Reader) error {
if err != nil {
return err
}
return cs.ImportRoleKey(pemBytes, data.CanonicalRootRole, nil)
}
// ImportRoleKey imports a private key in PEM format key from a byte array
// It prompts for the key's passphrase to verify the data and to determine
// the key ID.
func (cs *CryptoService) ImportRoleKey(pemBytes []byte, role string, newPassphraseRetriever passphrase.Retriever) error {
var alias string
var err error
if role == data.CanonicalRootRole {
alias = role
if err = checkRootKeyIsEncrypted(pemBytes); err != nil {
return err
}
} else {
// Parse the private key to get the key ID so that we can import it to the correct location
privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, "")
if err != nil {
privKey, _, err = trustmanager.GetPasswdDecryptBytes(newPassphraseRetriever, pemBytes, role, string(role))
if err != nil {
return err
}
}
// Since we're importing a non-root role, we need to pass the path as an alias
alias = filepath.Join(notary.NonRootKeysSubdir, cs.gun, privKey.ID())
// We also need to ensure that the role is properly set in the PEM headers
pemBytes, err = trustmanager.KeyToPEM(privKey, role)
if err != nil {
return err
}
}
for _, ks := range cs.keyStores {
// don't redeclare err, we want the value carried out of the loop
if err = ks.ImportKey(pemBytes, "root"); err == nil {
if err = ks.ImportKey(pemBytes, alias); err == nil {
return nil //bail on the first keystore we import to
}
}

View file

@ -1,27 +1,34 @@
notaryserver:
server:
build: .
dockerfile: server.Dockerfile
links:
- notarymysql
- notarysigner
- mysql
- signer
- signer:notarysigner
environment:
- SERVICE_NAME=notary_server
ports:
- "8080"
- "4443:4443"
environment:
- SERVICE_NAME=notary
command: -config=fixtures/server-config.json
notarysigner:
volumes:
- /dev/bus/usb/003/010:/dev/bus/usb/002/010
- /var/run/pcscd/pcscd.comm:/var/run/pcscd/pcscd.comm
entrypoint: /bin/bash
command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.json"
signer:
build: .
dockerfile: signer.Dockerfile
links:
- notarymysql
command: -config=fixtures/signer-config.json
notarymysql:
- mysql
environment:
- SERVICE_NAME=notary_signer
entrypoint: /bin/bash
command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.json"
mysql:
volumes:
- notarymysql:/var/lib/mysql
build: ./notarymysql/
- ./notarymysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
- notary_data:/var/lib/mysql
image: mariadb:10.1.10
ports:
- "3306:3306"
environment:
- TERM=dumb
- MYSQL_ALLOW_EMPTY_PASSWORD="true"
command: mysqld --innodb_file_per_table

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 Sameer Naik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,4 +1,5 @@
FROM golang:1.5.1
FROM golang:1.5.3
MAINTAINER David Lawrence "david.lawrence@docker.com"
RUN apt-get update && apt-get install -y \
libltdl-dev \
@ -7,13 +8,20 @@ RUN apt-get update && apt-get install -y \
EXPOSE 4443
# Install DB migration tool
RUN go get github.com/mattes/migrate
ENV NOTARYPKG github.com/docker/notary
ENV GOPATH /go/src/${NOTARYPKG}/Godeps/_workspace:$GOPATH
# Copy the local repo to the expected go path
COPY . /go/src/github.com/docker/notary
WORKDIR /go/src/${NOTARYPKG}
# Install notary-server
RUN go install \
-tags pkcs11 \
-ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \

View file

@ -1,29 +1,20 @@
FROM dockersecurity/golang-softhsm2
MAINTAINER Diogo Monica "diogo@docker.com"
FROM golang:1.5.3
MAINTAINER David Lawrence "david.lawrence@docker.com"
# CHANGE-ME: Default values for SoftHSM2 PIN and SOPIN, used to initialize the first token
ENV NOTARY_SIGNER_PIN="1234"
ENV SOPIN="1234"
ENV LIBDIR="/usr/local/lib/softhsm/"
ENV NOTARY_SIGNER_DEFAULT_ALIAS="timestamp_1"
ENV NOTARY_SIGNER_TIMESTAMP_1="testpassword"
# Install openSC and dependencies
RUN apt-get update && apt-get install -y \
libltdl-dev \
libpcsclite-dev \
opensc \
usbutils \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Initialize the SoftHSM2 token on slod 0, using PIN and SOPIN varaibles
RUN softhsm2-util --init-token --slot 0 --label "test_token" --pin $NOTARY_SIGNER_PIN --so-pin $SOPIN
EXPOSE 4444
# Install DB migration tool
RUN go get github.com/mattes/migrate
ENV NOTARYPKG github.com/docker/notary
ENV GOPATH /go/src/${NOTARYPKG}/Godeps/_workspace:$GOPATH
EXPOSE 4444
ENV NOTARY_SIGNER_DEFAULT_ALIAS="timestamp_1"
ENV NOTARY_SIGNER_TIMESTAMP_1="testpassword"
# Copy the local repo to the expected go path
COPY . /go/src/github.com/docker/notary
@ -36,6 +27,5 @@ RUN go install \
-ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \
${NOTARYPKG}/cmd/notary-signer
ENTRYPOINT [ "notary-signer" ]
CMD [ "-config=fixtures/signer-config-local.json" ]

View file

@ -8,16 +8,11 @@ import (
"sync"
"github.com/Sirupsen/logrus"
"github.com/docker/notary"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/tuf/data"
)
const (
rootKeysSubdir = "root_keys"
nonRootKeysSubdir = "tuf_keys"
privDir = "private"
)
// KeyFileStore persists and manages private keys on disk
type KeyFileStore struct {
sync.Mutex
@ -37,7 +32,7 @@ type KeyMemoryStore struct {
// NewKeyFileStore returns a new KeyFileStore creating a private directory to
// hold the keys.
func NewKeyFileStore(baseDir string, passphraseRetriever passphrase.Retriever) (*KeyFileStore, error) {
baseDir = filepath.Join(baseDir, privDir)
baseDir = filepath.Join(baseDir, notary.PrivDir)
fileStore, err := NewPrivateSimpleFileStore(baseDir, keyExtension)
if err != nil {
return nil, err
@ -242,10 +237,10 @@ func listKeys(s LimitedFileStore) map[string]string {
for _, f := range s.ListFiles() {
// Remove the prefix of the directory from the filename
var keyIDFull string
if strings.HasPrefix(f, rootKeysSubdir+"/") {
keyIDFull = strings.TrimPrefix(f, rootKeysSubdir+"/")
if strings.HasPrefix(f, notary.RootKeysSubdir+"/") {
keyIDFull = strings.TrimPrefix(f, notary.RootKeysSubdir+"/")
} else {
keyIDFull = strings.TrimPrefix(f, nonRootKeysSubdir+"/")
keyIDFull = strings.TrimPrefix(f, notary.NonRootKeysSubdir+"/")
}
keyIDFull = strings.TrimSpace(keyIDFull)
@ -302,9 +297,9 @@ func removeKey(s LimitedFileStore, cachedKeys map[string]*cachedKey, name string
// Assumes 2 subdirectories, 1 containing root keys and 1 containing tuf keys
func getSubdir(alias string) string {
if alias == "root" {
return rootKeysSubdir
return notary.RootKeysSubdir
}
return nonRootKeysSubdir
return notary.NonRootKeysSubdir
}
// Given a key ID, gets the bytes and alias belonging to that key if the key
@ -327,7 +322,7 @@ func getRawKey(s LimitedFileStore, name string) ([]byte, string, error) {
return keyBytes, role, nil
}
// GetPasswdDecryptBytes gets the password to decript the given pem bytes.
// GetPasswdDecryptBytes gets the password to decrypt the given pem bytes.
// Returns the password and private key
func GetPasswdDecryptBytes(passphraseRetriever passphrase.Retriever, pemBytes []byte, name, alias string) (data.PrivateKey, string, error) {
var (

View file

@ -470,11 +470,16 @@ func KeyToPEM(privKey data.PrivateKey, role string) ([]byte, error) {
return nil, err
}
headers := map[string]string{}
if role != "" {
headers = map[string]string{
"role": role,
}
}
block := &pem.Block{
Type: bt,
Headers: map[string]string{
"role": role,
},
Headers: headers,
Bytes: privKey.Private(),
}
@ -509,6 +514,19 @@ func EncryptPrivateKey(key data.PrivateKey, role, passphrase string) ([]byte, er
return pem.EncodeToMemory(encryptedPEMBlock), nil
}
// ReadRoleFromPEM returns the value from the role PEM header, if it exists
func ReadRoleFromPEM(pemBytes []byte) string {
pemBlock, _ := pem.Decode(pemBytes)
if pemBlock.Headers == nil {
return ""
}
role, ok := pemBlock.Headers["role"]
if !ok {
return ""
}
return role
}
// CertToKey transforms a single input certificate into its corresponding
// PublicKey
func CertToKey(cert *x509.Certificate) data.PublicKey {

View file

@ -765,15 +765,15 @@ func (s *YubiKeyStore) ExportKey(keyID string) ([]byte, error) {
// ImportKey imports a root key into a Yubikey
func (s *YubiKeyStore) ImportKey(pemBytes []byte, keyPath string) error {
logrus.Debugf("Attempting to import: %s key inside of YubiKeyStore", keyPath)
if keyPath != data.CanonicalRootRole {
return fmt.Errorf("yubikey only supports storing root keys")
}
privKey, _, err := trustmanager.GetPasswdDecryptBytes(
s.passRetriever, pemBytes, "", "imported root")
if err != nil {
logrus.Debugf("Failed to get and retrieve a key from: %s", keyPath)
return err
}
if keyPath != data.CanonicalRootRole {
return fmt.Errorf("yubikey only supports storing root keys")
}
_, err = s.addKey(privKey.ID(), "root", privKey)
return err
}

View file

@ -29,7 +29,7 @@ however in attempting to add delegations I found I was making such
significant changes that I could not maintain backwards compatibility
without the code becoming overly convoluted.
Some features such as pluggable verifiers have alreayd been merged upstream to flynn/go-tuf
Some features such as pluggable verifiers have already been merged upstream to flynn/go-tuf
and we are in discussion with [titanous](https://github.com/titanous) about working to merge the 2 implementations.
This implementation retains the same 3 Clause BSD license present on

View file

@ -3,38 +3,31 @@ package client
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"path"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/notary"
tuf "github.com/docker/notary/tuf"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/keys"
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/store"
"github.com/docker/notary/tuf/utils"
)
const maxSize int64 = 5 << 20
// Client is a usability wrapper around a raw TUF repo
type Client struct {
local *tuf.Repo
remote store.RemoteStore
keysDB *keys.KeyDB
cache store.MetadataStore
}
// NewClient initialized a Client with the given repo, remote source of content, key database, and cache
func NewClient(local *tuf.Repo, remote store.RemoteStore, keysDB *keys.KeyDB, cache store.MetadataStore) *Client {
// NewClient initialized a Client with the given repo, remote source of content, and cache
func NewClient(local *tuf.Repo, remote store.RemoteStore, cache store.MetadataStore) *Client {
return &Client{
local: local,
remote: remote,
keysDB: keysDB,
cache: cache,
}
}
@ -131,11 +124,15 @@ func (c Client) checkRoot() error {
func (c *Client) downloadRoot() error {
logrus.Debug("Downloading Root...")
role := data.CanonicalRootRole
size := maxSize
// We can't read an exact size for the root metadata without risking getting stuck in the TUF update cycle
// since it's possible that downloading timestamp/snapshot metadata may fail due to a signature mismatch
var size int64 = -1
var expectedSha256 []byte
if c.local.Snapshot != nil {
size = c.local.Snapshot.Signed.Meta[role].Length
expectedSha256 = c.local.Snapshot.Signed.Meta[role].Hashes["sha256"]
if prevRootMeta, ok := c.local.Snapshot.Signed.Meta[role]; ok {
size = prevRootMeta.Length
expectedSha256 = prevRootMeta.Hashes["sha256"]
}
}
// if we're bootstrapping we may not have a cached root, an
@ -178,6 +175,7 @@ func (c *Client) downloadRoot() error {
var s *data.Signed
var raw []byte
if download {
// use consistent download if we have the checksum.
raw, s, err = c.downloadSigned(role, size, expectedSha256)
if err != nil {
return err
@ -201,34 +199,45 @@ func (c *Client) downloadRoot() error {
func (c Client) verifyRoot(role string, s *data.Signed, minVersion int) error {
// this will confirm that the root has been signed by the old root role
// as c.keysDB contains the root keys we bootstrapped with.
// with the root keys we bootstrapped with.
// Still need to determine if there has been a root key update and
// confirm signature with new root key
logrus.Debug("verifying root with existing keys")
err := signed.Verify(s, role, minVersion, c.keysDB)
rootRole, err := c.local.GetBaseRole(role)
if err != nil {
logrus.Debug("no previous root role loaded")
return err
}
// Verify using the rootRole loaded from the known root.json
if err = signed.Verify(s, rootRole, minVersion); err != nil {
logrus.Debug("root did not verify with existing keys")
return err
}
// This will cause keyDB to get updated, overwriting any keyIDs associated
// with the roles in root.json
logrus.Debug("updating known root roles and keys")
root, err := data.RootFromSigned(s)
if err != nil {
logrus.Error(err.Error())
return err
}
// replace the existing root.json with the new one (just in memory, we
// have another validation step before we fully accept the new root)
err = c.local.SetRoot(root)
if err != nil {
logrus.Error(err.Error())
return err
}
// verify again now that the old keys have been replaced with the new keys.
// Verify the new root again having loaded the rootRole out of this new
// file (verifies self-referential integrity)
// TODO(endophage): be more intelligent and only re-verify if we detect
// there has been a change in root keys
logrus.Debug("verifying root with updated keys")
err = signed.Verify(s, role, minVersion, c.keysDB)
rootRole, err = c.local.GetBaseRole(role)
if err != nil {
logrus.Debug("root role with new keys not loaded")
return err
}
err = signed.Verify(s, rootRole, minVersion)
if err != nil {
logrus.Debug("root did not verify with new keys")
return err
@ -248,11 +257,11 @@ func (c *Client) downloadTimestamp() error {
// we're interacting with the repo. This will result in the
// version being 0
var (
saveToCache bool
old *data.Signed
ts *data.SignedTimestamp
version = 0
)
cachedTS, err := c.cache.GetMeta(role, maxSize)
cachedTS, err := c.cache.GetMeta(role, notary.MaxTimestampSize)
if err == nil {
cached := &data.Signed{}
err := json.Unmarshal(cachedTS, cached)
@ -266,49 +275,56 @@ func (c *Client) downloadTimestamp() error {
}
// unlike root, targets and snapshot, always try and download timestamps
// from remote, only using the cache one if we couldn't reach remote.
raw, s, err := c.downloadSigned(role, maxSize, nil)
if err != nil || len(raw) == 0 {
if old == nil {
raw, s, err := c.downloadSigned(role, notary.MaxTimestampSize, nil)
if err == nil {
// couldn't retrieve data from server and don't have valid
// data in cache.
return store.ErrMetaNotFound{Resource: data.CanonicalTimestampRole}
ts, err = c.verifyTimestamp(s, version)
if err == nil {
logrus.Debug("successfully verified downloaded timestamp")
c.cache.SetMeta(role, raw)
c.local.SetTimestamp(ts)
return nil
}
}
if old == nil {
// couldn't retrieve valid data from server and don't have unmarshallable data in cache.
logrus.Debug("no cached timestamp available")
return err
}
logrus.Debug(err.Error())
logrus.Warn("Error while downloading remote metadata, using cached timestamp - this might not be the latest version available remotely")
s = old
} else {
saveToCache = true
}
err = signed.Verify(s, role, version, c.keysDB)
if err != nil {
return err
}
logrus.Debug("successfully verified timestamp")
if saveToCache {
c.cache.SetMeta(role, raw)
}
ts, err := data.TimestampFromSigned(s)
ts, err = c.verifyTimestamp(old, version)
if err != nil {
return err
}
logrus.Debug("successfully verified cached timestamp")
c.local.SetTimestamp(ts)
return nil
}
// verifies that a timestamp is valid, and returned the SignedTimestamp object to add to the tuf repo
func (c *Client) verifyTimestamp(s *data.Signed, minVersion int) (*data.SignedTimestamp, error) {
timestampRole, err := c.local.GetBaseRole(data.CanonicalTimestampRole)
if err != nil {
logrus.Debug("no timestamp role loaded")
return nil, err
}
if err := signed.Verify(s, timestampRole, minVersion); err != nil {
return nil, err
}
return data.TimestampFromSigned(s)
}
// downloadSnapshot is responsible for downloading the snapshot.json
func (c *Client) downloadSnapshot() error {
logrus.Debug("Downloading Snapshot...")
role := data.CanonicalSnapshotRole
if c.local.Timestamp == nil {
return ErrMissingMeta{role: "snapshot"}
return tuf.ErrNotLoaded{Role: data.CanonicalTimestampRole}
}
size := c.local.Timestamp.Signed.Meta[role].Length
expectedSha256, ok := c.local.Timestamp.Signed.Meta[role].Hashes["sha256"]
if !ok {
return ErrMissingMeta{role: "snapshot"}
return data.ErrMissingMeta{Role: "snapshot"}
}
var download bool
@ -350,7 +366,12 @@ func (c *Client) downloadSnapshot() error {
s = old
}
err = signed.Verify(s, role, version, c.keysDB)
snapshotRole, err := c.local.GetBaseRole(role)
if err != nil {
logrus.Debug("no snapshot role loaded")
return err
}
err = signed.Verify(s, snapshotRole, version)
if err != nil {
return err
}
@ -382,18 +403,14 @@ func (c *Client) downloadTargets(role string) error {
return err
}
if c.local.Snapshot == nil {
return ErrMissingMeta{role: role}
return tuf.ErrNotLoaded{Role: data.CanonicalSnapshotRole}
}
snap := c.local.Snapshot.Signed
root := c.local.Root.Signed
r := c.keysDB.GetRole(role)
if r == nil {
return fmt.Errorf("Invalid role: %s", role)
}
keyIDs := r.KeyIDs
s, err := c.getTargetsFile(role, keyIDs, snap.Meta, root.ConsistentSnapshot, r.Threshold)
s, err := c.getTargetsFile(role, snap.Meta, root.ConsistentSnapshot)
if err != nil {
if _, ok := err.(ErrMissingMeta); ok && role != data.CanonicalTargetsRole {
if _, ok := err.(data.ErrMissingMeta); ok && role != data.CanonicalTargetsRole {
// if the role meta hasn't been published,
// that's ok, continue
continue
@ -401,7 +418,7 @@ func (c *Client) downloadTargets(role string) error {
logrus.Error("Error getting targets file:", err)
return err
}
t, err := data.TargetsFromSigned(s)
t, err := data.TargetsFromSigned(s, role)
if err != nil {
return err
}
@ -412,14 +429,19 @@ func (c *Client) downloadTargets(role string) error {
// push delegated roles contained in the targets file onto the stack
for _, r := range t.Signed.Delegations.Roles {
if path.Dir(r.Name) == role {
// only load children that are direct 1st generation descendants
// of the role we've just downloaded
stack.Push(r.Name)
}
}
}
return nil
}
func (c *Client) downloadSigned(role string, size int64, expectedSha256 []byte) ([]byte, *data.Signed, error) {
raw, err := c.remote.GetMeta(role, size)
rolePath := utils.ConsistentName(role, expectedSha256)
raw, err := c.remote.GetMeta(rolePath, size)
if err != nil {
return nil, nil, err
}
@ -437,15 +459,15 @@ func (c *Client) downloadSigned(role string, size int64, expectedSha256 []byte)
return raw, s, nil
}
func (c Client) getTargetsFile(role string, keyIDs []string, snapshotMeta data.Files, consistent bool, threshold int) (*data.Signed, error) {
func (c Client) getTargetsFile(role string, snapshotMeta data.Files, consistent bool) (*data.Signed, error) {
// require role exists in snapshots
roleMeta, ok := snapshotMeta[role]
if !ok {
return nil, ErrMissingMeta{role: role}
return nil, data.ErrMissingMeta{Role: role}
}
expectedSha256, ok := snapshotMeta[role].Hashes["sha256"]
if !ok {
return nil, ErrMissingMeta{role: role}
return nil, data.ErrMissingMeta{Role: role}
}
// try to get meta file from content addressed cache
@ -464,7 +486,7 @@ func (c Client) getTargetsFile(role string, keyIDs []string, snapshotMeta data.F
}
err := json.Unmarshal(raw, old)
if err == nil {
targ, err := data.TargetsFromSigned(old)
targ, err := data.TargetsFromSigned(old, role)
if err == nil {
version = targ.Signed.Version
} else {
@ -478,11 +500,7 @@ func (c Client) getTargetsFile(role string, keyIDs []string, snapshotMeta data.F
size := snapshotMeta[role].Length
var s *data.Signed
if download {
rolePath, err := c.RoleTargetsPath(role, hex.EncodeToString(expectedSha256), consistent)
if err != nil {
return nil, err
}
raw, s, err = c.downloadSigned(rolePath, size, expectedSha256)
raw, s, err = c.downloadSigned(role, size, expectedSha256)
if err != nil {
return nil, err
}
@ -490,9 +508,22 @@ func (c Client) getTargetsFile(role string, keyIDs []string, snapshotMeta data.F
logrus.Debug("using cached ", role)
s = old
}
err = signed.Verify(s, role, version, c.keysDB)
var targetOrDelgRole data.BaseRole
if data.IsDelegation(role) {
delgRole, err := c.local.GetDelegationRole(role)
if err != nil {
logrus.Debugf("no %s delegation role loaded", role)
return nil, err
}
targetOrDelgRole = delgRole.BaseRole
} else {
targetOrDelgRole, err = c.local.GetBaseRole(role)
if err != nil {
logrus.Debugf("no %s role loaded", role)
return nil, err
}
}
if err = signed.Verify(s, targetOrDelgRole, version); err != nil {
return nil, err
}
logrus.Debugf("successfully verified %s", role)
@ -505,73 +536,3 @@ func (c Client) getTargetsFile(role string, keyIDs []string, snapshotMeta data.F
}
return s, nil
}
// RoleTargetsPath generates the appropriate HTTP URL for the targets file,
// based on whether the repo is marked as consistent.
func (c Client) RoleTargetsPath(role string, hashSha256 string, consistent bool) (string, error) {
if consistent {
// Use path instead of filepath since we refer to the TUF role directly instead of its target files
dir := path.Dir(role)
if strings.Contains(role, "/") {
lastSlashIdx := strings.LastIndex(role, "/")
role = role[lastSlashIdx+1:]
}
role = path.Join(
dir,
fmt.Sprintf("%s.%s.json", hashSha256, role),
)
}
return role, nil
}
// TargetMeta ensures the repo is up to date. It assumes downloadTargets
// has already downloaded all delegated roles
func (c Client) TargetMeta(role, path string, excludeRoles ...string) (*data.FileMeta, string) {
excl := make(map[string]bool)
for _, r := range excludeRoles {
excl[r] = true
}
pathDigest := sha256.Sum256([]byte(path))
pathHex := hex.EncodeToString(pathDigest[:])
// FIFO list of targets delegations to inspect for target
roles := []string{role}
var (
meta *data.FileMeta
curr string
)
for len(roles) > 0 {
// have to do these lines here because of order of execution in for statement
curr = roles[0]
roles = roles[1:]
meta = c.local.TargetMeta(curr, path)
if meta != nil {
// we found the target!
return meta, curr
}
delegations := c.local.TargetDelegations(curr, path, pathHex)
for _, d := range delegations {
if !excl[d.Name] {
roles = append(roles, d.Name)
}
}
}
return meta, ""
}
// DownloadTarget downloads the target to dst from the remote
func (c Client) DownloadTarget(dst io.Writer, path string, meta *data.FileMeta) error {
reader, err := c.remote.GetTarget(path)
if err != nil {
return err
}
defer reader.Close()
r := io.TeeReader(
io.LimitReader(reader, meta.Length),
dst,
)
err = utils.ValidateTarget(r, meta)
return err
}

View file

@ -13,15 +13,6 @@ func (e ErrChecksumMismatch) Error() string {
return fmt.Sprintf("tuf: checksum for %s did not match", e.role)
}
// ErrMissingMeta - couldn't find the FileMeta object for a role or target
type ErrMissingMeta struct {
role string
}
func (e ErrMissingMeta) Error() string {
return fmt.Sprintf("tuf: sha256 checksum required for %s", e.role)
}
// ErrCorruptedCache - local data is incorrect
type ErrCorruptedCache struct {
file string

View file

@ -0,0 +1,22 @@
package data
import "fmt"
// ErrInvalidMetadata is the error to be returned when metadata is invalid
type ErrInvalidMetadata struct {
role string
msg string
}
func (e ErrInvalidMetadata) Error() string {
return fmt.Sprintf("%s type metadata invalid: %s", e.role, e.msg)
}
// ErrMissingMeta - couldn't find the FileMeta object for a role or target
type ErrMissingMeta struct {
Role string
}
func (e ErrMissingMeta) Error() string {
return fmt.Sprintf("tuf: sha256 checksum required for %s", e.Role)
}

View file

@ -46,7 +46,7 @@ type Keys map[string]PublicKey
// UnmarshalJSON implements the json.Unmarshaller interface
func (ks *Keys) UnmarshalJSON(data []byte) error {
parsed := make(map[string]tufKey)
parsed := make(map[string]TUFKey)
err := json.Unmarshal(data, &parsed)
if err != nil {
return err
@ -64,7 +64,7 @@ type KeyList []PublicKey
// UnmarshalJSON implements the json.Unmarshaller interface
func (ks *KeyList) UnmarshalJSON(data []byte) error {
parsed := make([]tufKey, 0, 1)
parsed := make([]TUFKey, 0, 1)
err := json.Unmarshal(data, &parsed)
if err != nil {
return err
@ -86,64 +86,64 @@ func (ks KeyList) IDs() []string {
return keyIDs
}
func typedPublicKey(tk tufKey) PublicKey {
func typedPublicKey(tk TUFKey) PublicKey {
switch tk.Algorithm() {
case ECDSAKey:
return &ECDSAPublicKey{tufKey: tk}
return &ECDSAPublicKey{TUFKey: tk}
case ECDSAx509Key:
return &ECDSAx509PublicKey{tufKey: tk}
return &ECDSAx509PublicKey{TUFKey: tk}
case RSAKey:
return &RSAPublicKey{tufKey: tk}
return &RSAPublicKey{TUFKey: tk}
case RSAx509Key:
return &RSAx509PublicKey{tufKey: tk}
return &RSAx509PublicKey{TUFKey: tk}
case ED25519Key:
return &ED25519PublicKey{tufKey: tk}
return &ED25519PublicKey{TUFKey: tk}
}
return &UnknownPublicKey{tufKey: tk}
return &UnknownPublicKey{TUFKey: tk}
}
func typedPrivateKey(tk tufKey) (PrivateKey, error) {
func typedPrivateKey(tk TUFKey) (PrivateKey, error) {
private := tk.Value.Private
tk.Value.Private = nil
switch tk.Algorithm() {
case ECDSAKey:
return NewECDSAPrivateKey(
&ECDSAPublicKey{
tufKey: tk,
TUFKey: tk,
},
private,
)
case ECDSAx509Key:
return NewECDSAPrivateKey(
&ECDSAx509PublicKey{
tufKey: tk,
TUFKey: tk,
},
private,
)
case RSAKey:
return NewRSAPrivateKey(
&RSAPublicKey{
tufKey: tk,
TUFKey: tk,
},
private,
)
case RSAx509Key:
return NewRSAPrivateKey(
&RSAx509PublicKey{
tufKey: tk,
TUFKey: tk,
},
private,
)
case ED25519Key:
return NewED25519PrivateKey(
ED25519PublicKey{
tufKey: tk,
TUFKey: tk,
},
private,
)
}
return &UnknownPrivateKey{
tufKey: tk,
TUFKey: tk,
privateKey: privateKey{private: private},
}, nil
}
@ -151,7 +151,7 @@ func typedPrivateKey(tk tufKey) (PrivateKey, error) {
// NewPublicKey creates a new, correctly typed PublicKey, using the
// UnknownPublicKey catchall for unsupported ciphers
func NewPublicKey(alg string, public []byte) PublicKey {
tk := tufKey{
tk := TUFKey{
Type: alg,
Value: KeyPair{
Public: public,
@ -163,7 +163,7 @@ func NewPublicKey(alg string, public []byte) PublicKey {
// NewPrivateKey creates a new, correctly typed PrivateKey, using the
// UnknownPrivateKey catchall for unsupported ciphers
func NewPrivateKey(pubKey PublicKey, private []byte) (PrivateKey, error) {
tk := tufKey{
tk := TUFKey{
Type: pubKey.Algorithm(),
Value: KeyPair{
Public: pubKey.Public(),
@ -175,7 +175,7 @@ func NewPrivateKey(pubKey PublicKey, private []byte) (PrivateKey, error) {
// UnmarshalPublicKey is used to parse individual public keys in JSON
func UnmarshalPublicKey(data []byte) (PublicKey, error) {
var parsed tufKey
var parsed TUFKey
err := json.Unmarshal(data, &parsed)
if err != nil {
return nil, err
@ -185,7 +185,7 @@ func UnmarshalPublicKey(data []byte) (PublicKey, error) {
// UnmarshalPrivateKey is used to parse individual private keys in JSON
func UnmarshalPrivateKey(data []byte) (PrivateKey, error) {
var parsed tufKey
var parsed TUFKey
err := json.Unmarshal(data, &parsed)
if err != nil {
return nil, err
@ -193,26 +193,26 @@ func UnmarshalPrivateKey(data []byte) (PrivateKey, error) {
return typedPrivateKey(parsed)
}
// tufKey is the structure used for both public and private keys in TUF.
// TUFKey is the structure used for both public and private keys in TUF.
// Normally it would make sense to use a different structures for public and
// private keys, but that would change the key ID algorithm (since the canonical
// JSON would be different). This structure should normally be accessed through
// the PublicKey or PrivateKey interfaces.
type tufKey struct {
type TUFKey struct {
id string
Type string `json:"keytype"`
Value KeyPair `json:"keyval"`
}
// Algorithm returns the algorithm of the key
func (k tufKey) Algorithm() string {
func (k TUFKey) Algorithm() string {
return k.Type
}
// ID efficiently generates if necessary, and caches the ID of the key
func (k *tufKey) ID() string {
func (k *TUFKey) ID() string {
if k.id == "" {
pubK := tufKey{
pubK := TUFKey{
Type: k.Algorithm(),
Value: KeyPair{
Public: k.Public(),
@ -230,7 +230,7 @@ func (k *tufKey) ID() string {
}
// Public returns the public bytes
func (k tufKey) Public() []byte {
func (k TUFKey) Public() []byte {
return k.Value.Public
}
@ -239,42 +239,42 @@ func (k tufKey) Public() []byte {
// ECDSAPublicKey represents an ECDSA key using a raw serialization
// of the public key
type ECDSAPublicKey struct {
tufKey
TUFKey
}
// ECDSAx509PublicKey represents an ECDSA key using an x509 cert
// as the serialized format of the public key
type ECDSAx509PublicKey struct {
tufKey
TUFKey
}
// RSAPublicKey represents an RSA key using a raw serialization
// of the public key
type RSAPublicKey struct {
tufKey
TUFKey
}
// RSAx509PublicKey represents an RSA key using an x509 cert
// as the serialized format of the public key
type RSAx509PublicKey struct {
tufKey
TUFKey
}
// ED25519PublicKey represents an ED25519 key using a raw serialization
// of the public key
type ED25519PublicKey struct {
tufKey
TUFKey
}
// UnknownPublicKey is a catchall for key types that are not supported
type UnknownPublicKey struct {
tufKey
TUFKey
}
// NewECDSAPublicKey initializes a new public key with the ECDSAKey type
func NewECDSAPublicKey(public []byte) *ECDSAPublicKey {
return &ECDSAPublicKey{
tufKey: tufKey{
TUFKey: TUFKey{
Type: ECDSAKey,
Value: KeyPair{
Public: public,
@ -287,7 +287,7 @@ func NewECDSAPublicKey(public []byte) *ECDSAPublicKey {
// NewECDSAx509PublicKey initializes a new public key with the ECDSAx509Key type
func NewECDSAx509PublicKey(public []byte) *ECDSAx509PublicKey {
return &ECDSAx509PublicKey{
tufKey: tufKey{
TUFKey: TUFKey{
Type: ECDSAx509Key,
Value: KeyPair{
Public: public,
@ -300,7 +300,7 @@ func NewECDSAx509PublicKey(public []byte) *ECDSAx509PublicKey {
// NewRSAPublicKey initializes a new public key with the RSA type
func NewRSAPublicKey(public []byte) *RSAPublicKey {
return &RSAPublicKey{
tufKey: tufKey{
TUFKey: TUFKey{
Type: RSAKey,
Value: KeyPair{
Public: public,
@ -313,7 +313,7 @@ func NewRSAPublicKey(public []byte) *RSAPublicKey {
// NewRSAx509PublicKey initializes a new public key with the RSAx509Key type
func NewRSAx509PublicKey(public []byte) *RSAx509PublicKey {
return &RSAx509PublicKey{
tufKey: tufKey{
TUFKey: TUFKey{
Type: RSAx509Key,
Value: KeyPair{
Public: public,
@ -326,7 +326,7 @@ func NewRSAx509PublicKey(public []byte) *RSAx509PublicKey {
// NewED25519PublicKey initializes a new public key with the ED25519Key type
func NewED25519PublicKey(public []byte) *ED25519PublicKey {
return &ED25519PublicKey{
tufKey: tufKey{
TUFKey: TUFKey{
Type: ED25519Key,
Value: KeyPair{
Public: public,
@ -367,7 +367,7 @@ type ED25519PrivateKey struct {
// UnknownPrivateKey is a catchall for unsupported key types
type UnknownPrivateKey struct {
tufKey
TUFKey
privateKey
}
@ -515,10 +515,10 @@ func (k UnknownPrivateKey) SignatureAlgorithm() SigAlgorithm {
return ""
}
// PublicKeyFromPrivate returns a new tufKey based on a private key, with
// PublicKeyFromPrivate returns a new TUFKey based on a private key, with
// the private key bytes guaranteed to be nil.
func PublicKeyFromPrivate(pk PrivateKey) PublicKey {
return typedPublicKey(tufKey{
return typedPublicKey(TUFKey{
Type: pk.Algorithm(),
Value: KeyPair{
Public: pk.Public(),

View file

@ -2,10 +2,11 @@ package data
import (
"fmt"
"github.com/Sirupsen/logrus"
"path"
"regexp"
"strings"
"github.com/Sirupsen/logrus"
)
// Canonical base role names
@ -85,32 +86,139 @@ func IsDelegation(role string) bool {
isClean
}
// BaseRole is an internal representation of a root/targets/snapshot/timestamp role, with its public keys included
type BaseRole struct {
Keys map[string]PublicKey
Name string
Threshold int
}
// NewBaseRole creates a new BaseRole object with the provided parameters
func NewBaseRole(name string, threshold int, keys ...PublicKey) BaseRole {
r := BaseRole{
Name: name,
Threshold: threshold,
Keys: make(map[string]PublicKey),
}
for _, k := range keys {
r.Keys[k.ID()] = k
}
return r
}
// ListKeys retrieves the public keys valid for this role
func (b BaseRole) ListKeys() KeyList {
return listKeys(b.Keys)
}
// ListKeyIDs retrieves the list of key IDs valid for this role
func (b BaseRole) ListKeyIDs() []string {
return listKeyIDs(b.Keys)
}
// DelegationRole is an internal representation of a delegation role, with its public keys included
type DelegationRole struct {
BaseRole
Paths []string
}
func listKeys(keyMap map[string]PublicKey) KeyList {
keys := KeyList{}
for _, key := range keyMap {
keys = append(keys, key)
}
return keys
}
func listKeyIDs(keyMap map[string]PublicKey) []string {
keyIDs := []string{}
for id := range keyMap {
keyIDs = append(keyIDs, id)
}
return keyIDs
}
// Restrict restricts the paths and path hash prefixes for the passed in delegation role,
// returning a copy of the role with validated paths as if it was a direct child
func (d DelegationRole) Restrict(child DelegationRole) (DelegationRole, error) {
if !d.IsParentOf(child) {
return DelegationRole{}, fmt.Errorf("%s is not a parent of %s", d.Name, child.Name)
}
return DelegationRole{
BaseRole: BaseRole{
Keys: child.Keys,
Name: child.Name,
Threshold: child.Threshold,
},
Paths: RestrictDelegationPathPrefixes(d.Paths, child.Paths),
}, nil
}
// IsParentOf returns whether the passed in delegation role is the direct child of this role,
// determined by delegation name.
// Ex: targets/a is a direct parent of targets/a/b, but targets/a is not a direct parent of targets/a/b/c
func (d DelegationRole) IsParentOf(child DelegationRole) bool {
return path.Dir(child.Name) == d.Name
}
// CheckPaths checks if a given path is valid for the role
func (d DelegationRole) CheckPaths(path string) bool {
return checkPaths(path, d.Paths)
}
func checkPaths(path string, permitted []string) bool {
for _, p := range permitted {
if strings.HasPrefix(path, p) {
return true
}
}
return false
}
// RestrictDelegationPathPrefixes returns the list of valid delegationPaths that are prefixed by parentPaths
func RestrictDelegationPathPrefixes(parentPaths, delegationPaths []string) []string {
validPaths := []string{}
if len(delegationPaths) == 0 {
return validPaths
}
// Validate each individual delegation path
for _, delgPath := range delegationPaths {
isPrefixed := false
for _, parentPath := range parentPaths {
if strings.HasPrefix(delgPath, parentPath) {
isPrefixed = true
break
}
}
// If the delegation path did not match prefix against any parent path, it is not valid
if isPrefixed {
validPaths = append(validPaths, delgPath)
}
}
return validPaths
}
// RootRole is a cut down role as it appears in the root.json
// Eventually should only be used for immediately before and after serialization/deserialization
type RootRole struct {
KeyIDs []string `json:"keyids"`
Threshold int `json:"threshold"`
}
// Role is a more verbose role as they appear in targets delegations
// Eventually should only be used for immediately before and after serialization/deserialization
type Role struct {
RootRole
Name string `json:"name"`
Paths []string `json:"paths,omitempty"`
PathHashPrefixes []string `json:"path_hash_prefixes,omitempty"`
Email string `json:"email,omitempty"`
}
// NewRole creates a new Role object from the given parameters
func NewRole(name string, threshold int, keyIDs, paths, pathHashPrefixes []string) (*Role, error) {
if len(paths) > 0 && len(pathHashPrefixes) > 0 {
return nil, ErrInvalidRole{
Role: name,
Reason: "roles may not have both Paths and PathHashPrefixes",
}
}
func NewRole(name string, threshold int, keyIDs, paths []string) (*Role, error) {
if IsDelegation(name) {
if len(paths) == 0 && len(pathHashPrefixes) == 0 {
logrus.Debugf("role %s with no Paths and no PathHashPrefixes will never be able to publish content until one or more are added", name)
if len(paths) == 0 {
logrus.Debugf("role %s with no Paths will never be able to publish content until one or more are added", name)
}
}
if threshold < 1 {
@ -126,50 +234,13 @@ func NewRole(name string, threshold int, keyIDs, paths, pathHashPrefixes []strin
},
Name: name,
Paths: paths,
PathHashPrefixes: pathHashPrefixes,
}, nil
}
// IsValid checks if the role has defined both paths and path hash prefixes,
// having both is invalid
func (r Role) IsValid() bool {
return !(len(r.Paths) > 0 && len(r.PathHashPrefixes) > 0)
}
// ValidKey checks if the given id is a recognized signing key for the role
func (r Role) ValidKey(id string) bool {
for _, key := range r.KeyIDs {
if key == id {
return true
}
}
return false
}
// CheckPaths checks if a given path is valid for the role
func (r Role) CheckPaths(path string) bool {
for _, p := range r.Paths {
if strings.HasPrefix(path, p) {
return true
}
}
return false
}
// CheckPrefixes checks if a given hash matches the prefixes for the role
func (r Role) CheckPrefixes(hash string) bool {
for _, p := range r.PathHashPrefixes {
if strings.HasPrefix(hash, p) {
return true
}
}
return false
}
// IsDelegation checks if the role is a delegation or a root role
func (r Role) IsDelegation() bool {
return IsDelegation(r.Name)
return checkPaths(path, r.Paths)
}
// AddKeys merges the ids into the current list of role key ids
@ -182,25 +253,10 @@ func (r *Role) AddPaths(paths []string) error {
if len(paths) == 0 {
return nil
}
if len(r.PathHashPrefixes) > 0 {
return ErrInvalidRole{Role: r.Name, Reason: "attempted to add paths to role that already has hash prefixes"}
}
r.Paths = mergeStrSlices(r.Paths, paths)
return nil
}
// AddPathHashPrefixes merges the prefixes into the list of role path hash prefixes
func (r *Role) AddPathHashPrefixes(prefixes []string) error {
if len(prefixes) == 0 {
return nil
}
if len(r.Paths) > 0 {
return ErrInvalidRole{Role: r.Name, Reason: "attempted to add hash prefixes to role that already has paths"}
}
r.PathHashPrefixes = mergeStrSlices(r.PathHashPrefixes, prefixes)
return nil
}
// RemoveKeys removes the ids from the current list of key ids
func (r *Role) RemoveKeys(ids []string) {
r.KeyIDs = subtractStrSlices(r.KeyIDs, ids)
@ -211,11 +267,6 @@ func (r *Role) RemovePaths(paths []string) {
r.Paths = subtractStrSlices(r.Paths, paths)
}
// RemovePathHashPrefixes removes the prefixes from the current list of path hash prefixes
func (r *Role) RemovePathHashPrefixes(prefixes []string) {
r.PathHashPrefixes = subtractStrSlices(r.PathHashPrefixes, prefixes)
}
func mergeStrSlices(orig, new []string) []string {
have := make(map[string]bool)
for _, e := range orig {

View file

@ -1,6 +1,7 @@
package data
import (
"fmt"
"time"
"github.com/docker/go/canonical/json"
@ -23,14 +24,57 @@ type Root struct {
ConsistentSnapshot bool `json:"consistent_snapshot"`
}
// isValidRootStructure returns an error, or nil, depending on whether the content of the struct
// is valid for root metadata. This does not check signatures or expiry, just that
// the metadata content is valid.
func isValidRootStructure(r Root) error {
expectedType := TUFTypes[CanonicalRootRole]
if r.Type != expectedType {
return ErrInvalidMetadata{
role: CanonicalRootRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, r.Type)}
}
// all the base roles MUST appear in the root.json - other roles are allowed,
// but other than the mirror role (not currently supported) are out of spec
for _, roleName := range BaseRoles {
roleObj, ok := r.Roles[roleName]
if !ok || roleObj == nil {
return ErrInvalidMetadata{
role: CanonicalRootRole, msg: fmt.Sprintf("missing %s role specification", roleName)}
}
if err := isValidRootRoleStructure(CanonicalRootRole, roleName, *roleObj, r.Keys); err != nil {
return err
}
}
return nil
}
func isValidRootRoleStructure(metaContainingRole, rootRoleName string, r RootRole, validKeys Keys) error {
if r.Threshold < 1 {
return ErrInvalidMetadata{
role: metaContainingRole,
msg: fmt.Sprintf("invalid threshold specified for %s: %v ", rootRoleName, r.Threshold),
}
}
for _, keyID := range r.KeyIDs {
if _, ok := validKeys[keyID]; !ok {
return ErrInvalidMetadata{
role: metaContainingRole,
msg: fmt.Sprintf("key ID %s specified in %s without corresponding key", keyID, rootRoleName),
}
}
}
return nil
}
// NewRoot initializes a new SignedRoot with a set of keys, roles, and the consistent flag
func NewRoot(keys map[string]PublicKey, roles map[string]*RootRole, consistent bool) (*SignedRoot, error) {
signedRoot := &SignedRoot{
Signatures: make([]Signature, 0),
Signed: Root{
Type: TUFTypes["root"],
Type: TUFTypes[CanonicalRootRole],
Version: 0,
Expires: DefaultExpires("root"),
Expires: DefaultExpires(CanonicalRootRole),
Keys: keys,
Roles: roles,
ConsistentSnapshot: consistent,
@ -41,6 +85,34 @@ func NewRoot(keys map[string]PublicKey, roles map[string]*RootRole, consistent b
return signedRoot, nil
}
// BuildBaseRole returns a copy of a BaseRole using the information in this SignedRoot for the specified role name.
// Will error for invalid role name or key metadata within this SignedRoot
func (r SignedRoot) BuildBaseRole(roleName string) (BaseRole, error) {
roleData, ok := r.Signed.Roles[roleName]
if !ok {
return BaseRole{}, ErrInvalidRole{Role: roleName, Reason: "role not found in root file"}
}
// Get all public keys for the base role from TUF metadata
keyIDs := roleData.KeyIDs
pubKeys := make(map[string]PublicKey)
for _, keyID := range keyIDs {
pubKey, ok := r.Signed.Keys[keyID]
if !ok {
return BaseRole{}, ErrInvalidRole{
Role: roleName,
Reason: fmt.Sprintf("key with ID %s was not found in root metadata", keyID),
}
}
pubKeys[keyID] = pubKey
}
return BaseRole{
Name: roleName,
Keys: pubKeys,
Threshold: roleData.Threshold,
}, nil
}
// ToSigned partially serializes a SignedRoot for further signing
func (r SignedRoot) ToSigned() (*Signed, error) {
s, err := defaultSerializer.MarshalCanonical(r.Signed)
@ -70,11 +142,14 @@ func (r SignedRoot) MarshalJSON() ([]byte, error) {
return defaultSerializer.Marshal(signed)
}
// RootFromSigned fully unpacks a Signed object into a SignedRoot
// RootFromSigned fully unpacks a Signed object into a SignedRoot and ensures
// that it is a valid SignedRoot
func RootFromSigned(s *Signed) (*SignedRoot, error) {
r := Root{}
err := json.Unmarshal(s.Signed, &r)
if err != nil {
if err := defaultSerializer.Unmarshal(s.Signed, &r); err != nil {
return nil, err
}
if err := isValidRootStructure(r); err != nil {
return nil, err
}
sigs := make([]Signature, len(s.Signatures))

View file

@ -2,6 +2,8 @@ package data
import (
"bytes"
"crypto/sha256"
"fmt"
"time"
"github.com/Sirupsen/logrus"
@ -23,6 +25,30 @@ type Snapshot struct {
Meta Files `json:"meta"`
}
// isValidSnapshotStructure returns an error, or nil, depending on whether the content of the
// struct is valid for snapshot metadata. This does not check signatures or expiry, just that
// the metadata content is valid.
func isValidSnapshotStructure(s Snapshot) error {
expectedType := TUFTypes[CanonicalSnapshotRole]
if s.Type != expectedType {
return ErrInvalidMetadata{
role: CanonicalSnapshotRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, s.Type)}
}
for _, role := range []string{CanonicalRootRole, CanonicalTargetsRole} {
// Meta is a map of FileMeta, so if the role isn't in the map it returns
// an empty FileMeta, which has an empty map, and you can check on keys
// from an empty map.
if checksum, ok := s.Meta[role].Hashes["sha256"]; !ok || len(checksum) != sha256.Size {
return ErrInvalidMetadata{
role: CanonicalSnapshotRole,
msg: fmt.Sprintf("missing or invalid %s sha256 checksum information", role),
}
}
}
return nil
}
// NewSnapshot initilizes a SignedSnapshot with a given top level root
// and targets objects
func NewSnapshot(root *Signed, targets *Signed) (*SignedSnapshot, error) {
@ -64,8 +90,8 @@ func (sp *SignedSnapshot) hashForRole(role string) []byte {
}
// ToSigned partially serializes a SignedSnapshot for further signing
func (sp SignedSnapshot) ToSigned() (*Signed, error) {
s, err := json.MarshalCanonical(sp.Signed)
func (sp *SignedSnapshot) ToSigned() (*Signed, error) {
s, err := defaultSerializer.MarshalCanonical(sp.Signed)
if err != nil {
return nil, err
}
@ -88,6 +114,15 @@ func (sp *SignedSnapshot) AddMeta(role string, meta FileMeta) {
sp.Dirty = true
}
// GetMeta gets the metadata for a particular role, returning an error if it's
// not found
func (sp *SignedSnapshot) GetMeta(role string) (*FileMeta, error) {
if meta, ok := sp.Signed.Meta[role]; ok {
return &meta, nil
}
return nil, ErrMissingMeta{Role: role}
}
// DeleteMeta removes a role from the snapshot. If the role doesn't
// exist in the snapshot, it's a noop.
func (sp *SignedSnapshot) DeleteMeta(role string) {
@ -97,11 +132,22 @@ func (sp *SignedSnapshot) DeleteMeta(role string) {
}
}
// MarshalJSON returns the serialized form of SignedSnapshot as bytes
func (sp *SignedSnapshot) MarshalJSON() ([]byte, error) {
signed, err := sp.ToSigned()
if err != nil {
return nil, err
}
return defaultSerializer.Marshal(signed)
}
// SnapshotFromSigned fully unpacks a Signed object into a SignedSnapshot
func SnapshotFromSigned(s *Signed) (*SignedSnapshot, error) {
sp := Snapshot{}
err := json.Unmarshal(s.Signed, &sp)
if err != nil {
if err := defaultSerializer.Unmarshal(s.Signed, &sp); err != nil {
return nil, err
}
if err := isValidSnapshotStructure(sp); err != nil {
return nil, err
}
sigs := make([]Signature, len(s.Signatures))

View file

@ -1,9 +1,9 @@
package data
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"path"
"github.com/docker/go/canonical/json"
)
@ -23,6 +23,33 @@ type Targets struct {
Delegations Delegations `json:"delegations,omitempty"`
}
// isValidTargetsStructure returns an error, or nil, depending on whether the content of the struct
// is valid for targets metadata. This does not check signatures or expiry, just that
// the metadata content is valid.
func isValidTargetsStructure(t Targets, roleName string) error {
if roleName != CanonicalTargetsRole && !IsDelegation(roleName) {
return ErrInvalidRole{Role: roleName}
}
// even if it's a delegated role, the metadata type is "Targets"
expectedType := TUFTypes[CanonicalTargetsRole]
if t.Type != expectedType {
return ErrInvalidMetadata{
role: roleName, msg: fmt.Sprintf("expected type %s, not %s", expectedType, t.Type)}
}
for _, roleObj := range t.Delegations.Roles {
if !IsDelegation(roleObj.Name) || path.Dir(roleObj.Name) != roleName {
return ErrInvalidMetadata{
role: roleName, msg: fmt.Sprintf("delegation role %s invalid", roleObj.Name)}
}
if err := isValidRootRoleStructure(roleName, roleObj.Name, roleObj.RootRole, t.Delegations.Keys); err != nil {
return err
}
}
return nil
}
// NewTargets intiializes a new empty SignedTargets object
func NewTargets() *SignedTargets {
return &SignedTargets{
@ -51,30 +78,58 @@ func (t SignedTargets) GetMeta(path string) *FileMeta {
return nil
}
// GetDelegations filters the roles and associated keys that may be
// the signers for the given target path. If no appropriate roles
// can be found, it will simply return nil for the return values.
// The returned slice of Role will have order maintained relative
// to the role slice on Delegations per TUF spec proposal on using
// order to determine priority.
func (t SignedTargets) GetDelegations(path string) []*Role {
var roles []*Role
pathHashBytes := sha256.Sum256([]byte(path))
pathHash := hex.EncodeToString(pathHashBytes[:])
for _, r := range t.Signed.Delegations.Roles {
if !r.IsValid() {
// Role has both Paths and PathHashPrefixes.
// GetValidDelegations filters the delegation roles specified in the signed targets, and
// only returns roles that are direct children and restricts their paths
func (t SignedTargets) GetValidDelegations(parent DelegationRole) []DelegationRole {
roles := t.buildDelegationRoles()
result := []DelegationRole{}
for _, r := range roles {
validRole, err := parent.Restrict(r)
if err != nil {
continue
}
if r.CheckPaths(path) {
roles = append(roles, r)
result = append(result, validRole)
}
return result
}
// BuildDelegationRole returns a copy of a DelegationRole using the information in this SignedTargets for the specified role name.
// Will error for invalid role name or key metadata within this SignedTargets. Path data is not validated.
func (t *SignedTargets) BuildDelegationRole(roleName string) (DelegationRole, error) {
for _, role := range t.Signed.Delegations.Roles {
if role.Name == roleName {
pubKeys := make(map[string]PublicKey)
for _, keyID := range role.KeyIDs {
pubKey, ok := t.Signed.Delegations.Keys[keyID]
if !ok {
// Couldn't retrieve all keys, so stop walking and return invalid role
return DelegationRole{}, ErrInvalidRole{Role: roleName, Reason: "delegation does not exist with all specified keys"}
}
pubKeys[keyID] = pubKey
}
return DelegationRole{
BaseRole: BaseRole{
Name: role.Name,
Keys: pubKeys,
Threshold: role.Threshold,
},
Paths: role.Paths,
}, nil
}
}
return DelegationRole{}, ErrNoSuchRole{Role: roleName}
}
// helper function to create DelegationRole structures from all delegations in a SignedTargets,
// these delegations are read directly from the SignedTargets and not modified or validated
func (t SignedTargets) buildDelegationRoles() []DelegationRole {
var roles []DelegationRole
for _, roleData := range t.Signed.Delegations.Roles {
delgRole, err := t.BuildDelegationRole(roleData.Name)
if err != nil {
continue
}
if r.CheckPrefixes(pathHash) {
roles = append(roles, r)
continue
}
//keysDB.AddRole(r)
roles = append(roles, delgRole)
}
return roles
}
@ -93,8 +148,8 @@ func (t *SignedTargets) AddDelegation(role *Role, keys []*PublicKey) error {
}
// ToSigned partially serializes a SignedTargets for further signing
func (t SignedTargets) ToSigned() (*Signed, error) {
s, err := json.MarshalCanonical(t.Signed)
func (t *SignedTargets) ToSigned() (*Signed, error) {
s, err := defaultSerializer.MarshalCanonical(t.Signed)
if err != nil {
return nil, err
}
@ -111,13 +166,25 @@ func (t SignedTargets) ToSigned() (*Signed, error) {
}, nil
}
// TargetsFromSigned fully unpacks a Signed object into a SignedTargets
func TargetsFromSigned(s *Signed) (*SignedTargets, error) {
t := Targets{}
err := json.Unmarshal(s.Signed, &t)
// MarshalJSON returns the serialized form of SignedTargets as bytes
func (t *SignedTargets) MarshalJSON() ([]byte, error) {
signed, err := t.ToSigned()
if err != nil {
return nil, err
}
return defaultSerializer.Marshal(signed)
}
// TargetsFromSigned fully unpacks a Signed object into a SignedTargets, given
// a role name (so it can validate the SignedTargets object)
func TargetsFromSigned(s *Signed, roleName string) (*SignedTargets, error) {
t := Targets{}
if err := defaultSerializer.Unmarshal(s.Signed, &t); err != nil {
return nil, err
}
if err := isValidTargetsStructure(t, roleName); err != nil {
return nil, err
}
sigs := make([]Signature, len(s.Signatures))
copy(sigs, s.Signatures)
return &SignedTargets{

View file

@ -2,6 +2,8 @@ package data
import (
"bytes"
"crypto/sha256"
"fmt"
"time"
"github.com/docker/go/canonical/json"
@ -22,6 +24,26 @@ type Timestamp struct {
Meta Files `json:"meta"`
}
// isValidTimestampStructure returns an error, or nil, depending on whether the content of the struct
// is valid for timestamp metadata. This does not check signatures or expiry, just that
// the metadata content is valid.
func isValidTimestampStructure(t Timestamp) error {
expectedType := TUFTypes[CanonicalTimestampRole]
if t.Type != expectedType {
return ErrInvalidMetadata{
role: CanonicalTimestampRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, t.Type)}
}
// Meta is a map of FileMeta, so if the role isn't in the map it returns
// an empty FileMeta, which has an empty map, and you can check on keys
// from an empty map.
if cs, ok := t.Meta[CanonicalSnapshotRole].Hashes["sha256"]; !ok || len(cs) != sha256.Size {
return ErrInvalidMetadata{
role: CanonicalTimestampRole, msg: "missing or invalid snapshot sha256 checksum information"}
}
return nil
}
// NewTimestamp initializes a timestamp with an existing snapshot
func NewTimestamp(snapshot *Signed) (*SignedTimestamp, error) {
snapshotJSON, err := json.Marshal(snapshot)
@ -47,8 +69,8 @@ func NewTimestamp(snapshot *Signed) (*SignedTimestamp, error) {
// ToSigned partially serializes a SignedTimestamp such that it can
// be signed
func (ts SignedTimestamp) ToSigned() (*Signed, error) {
s, err := json.MarshalCanonical(ts.Signed)
func (ts *SignedTimestamp) ToSigned() (*Signed, error) {
s, err := defaultSerializer.MarshalCanonical(ts.Signed)
if err != nil {
return nil, err
}
@ -65,12 +87,33 @@ func (ts SignedTimestamp) ToSigned() (*Signed, error) {
}, nil
}
// GetSnapshot gets the expected snapshot metadata hashes in the timestamp metadata,
// or nil if it doesn't exist
func (ts *SignedTimestamp) GetSnapshot() (*FileMeta, error) {
snapshotExpected, ok := ts.Signed.Meta[CanonicalSnapshotRole]
if !ok {
return nil, ErrMissingMeta{Role: CanonicalSnapshotRole}
}
return &snapshotExpected, nil
}
// MarshalJSON returns the serialized form of SignedTimestamp as bytes
func (ts *SignedTimestamp) MarshalJSON() ([]byte, error) {
signed, err := ts.ToSigned()
if err != nil {
return nil, err
}
return defaultSerializer.Marshal(signed)
}
// TimestampFromSigned parsed a Signed object into a fully unpacked
// SignedTimestamp
func TimestampFromSigned(s *Signed) (*SignedTimestamp, error) {
ts := Timestamp{}
err := json.Unmarshal(s.Signed, &ts)
if err != nil {
if err := defaultSerializer.Unmarshal(s.Signed, &ts); err != nil {
return nil, err
}
if err := isValidTimestampStructure(ts); err != nil {
return nil, err
}
sigs := make([]Signature, len(s.Signatures))

View file

@ -12,6 +12,7 @@ import (
"github.com/Sirupsen/logrus"
"github.com/docker/go/canonical/json"
"github.com/docker/notary"
)
// SigAlgorithm for types of signatures
@ -171,16 +172,16 @@ func NewDelegations() *Delegations {
}
}
// defines number of days in which something should expire
var defaultExpiryTimes = map[string]int{
CanonicalRootRole: 365,
CanonicalTargetsRole: 90,
CanonicalSnapshotRole: 7,
CanonicalTimestampRole: 1,
// These values are recommended TUF expiry times.
var defaultExpiryTimes = map[string]time.Duration{
CanonicalRootRole: notary.Year,
CanonicalTargetsRole: 90 * notary.Day,
CanonicalSnapshotRole: 7 * notary.Day,
CanonicalTimestampRole: notary.Day,
}
// SetDefaultExpiryTimes allows one to change the default expiries.
func SetDefaultExpiryTimes(times map[string]int) {
func SetDefaultExpiryTimes(times map[string]time.Duration) {
for key, value := range times {
if _, ok := defaultExpiryTimes[key]; !ok {
logrus.Errorf("Attempted to set default expiry for an unknown role: %s", key)
@ -192,10 +193,10 @@ func SetDefaultExpiryTimes(times map[string]int) {
// DefaultExpires gets the default expiry time for the given role
func DefaultExpires(role string) time.Time {
var t time.Time
if t, ok := defaultExpiryTimes[role]; ok {
return time.Now().AddDate(0, 0, t)
if d, ok := defaultExpiryTimes[role]; ok {
return time.Now().Add(d)
}
var t time.Time
return t.UTC().Round(time.Second)
}

View file

@ -1,78 +0,0 @@
package keys
import (
"errors"
"github.com/docker/notary/tuf/data"
)
// Various basic key database errors
var (
ErrWrongType = errors.New("tuf: invalid key type")
ErrExists = errors.New("tuf: key already in db")
ErrWrongID = errors.New("tuf: key id mismatch")
ErrInvalidKey = errors.New("tuf: invalid key")
ErrInvalidKeyID = errors.New("tuf: invalid key id")
ErrInvalidThreshold = errors.New("tuf: invalid role threshold")
)
// KeyDB is an in memory database of public keys and role associations.
// It is populated when parsing TUF files and used during signature
// verification to look up the keys for a given role
type KeyDB struct {
roles map[string]*data.Role
keys map[string]data.PublicKey
}
// NewDB initializes an empty KeyDB
func NewDB() *KeyDB {
return &KeyDB{
roles: make(map[string]*data.Role),
keys: make(map[string]data.PublicKey),
}
}
// AddKey adds a public key to the database
func (db *KeyDB) AddKey(k data.PublicKey) {
db.keys[k.ID()] = k
}
// AddRole adds a role to the database. Any keys associated with the
// role must have already been added.
func (db *KeyDB) AddRole(r *data.Role) error {
if !data.ValidRole(r.Name) {
return data.ErrInvalidRole{Role: r.Name}
}
if r.Threshold < 1 {
return ErrInvalidThreshold
}
// validate all key ids are in the keys maps
for _, id := range r.KeyIDs {
if _, ok := db.keys[id]; !ok {
return ErrInvalidKeyID
}
}
db.roles[r.Name] = r
return nil
}
// GetAllRoles gets all roles from the database
func (db *KeyDB) GetAllRoles() []*data.Role {
roles := []*data.Role{}
for _, role := range db.roles {
roles = append(roles, role)
}
return roles
}
// GetKey pulls a key out of the database by its ID
func (db *KeyDB) GetKey(id string) data.PublicKey {
return db.keys[id]
}
// GetRole retrieves a role based on its name
func (db *KeyDB) GetRole(name string) *data.Role {
return db.roles[name]
}

View file

@ -8,7 +8,6 @@ import (
"github.com/Sirupsen/logrus"
"github.com/docker/go/canonical/json"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/keys"
)
// Various basic signing errors
@ -57,18 +56,18 @@ func VerifyRoot(s *data.Signed, minVersion int, keys map[string]data.PublicKey)
continue
}
// threshold of 1 so return on first success
return verifyMeta(s, "root", minVersion)
return verifyMeta(s, data.CanonicalRootRole, minVersion)
}
return ErrRoleThreshold{}
}
// Verify checks the signatures and metadata (expiry, version) for the signed role
// data
func Verify(s *data.Signed, role string, minVersion int, db *keys.KeyDB) error {
if err := verifyMeta(s, role, minVersion); err != nil {
func Verify(s *data.Signed, role data.BaseRole, minVersion int) error {
if err := verifyMeta(s, role.Name, minVersion); err != nil {
return err
}
return VerifySignatures(s, role, db)
return VerifySignatures(s, role)
}
func verifyMeta(s *data.Signed, role string, minVersion int) error {
@ -96,21 +95,18 @@ func IsExpired(t time.Time) bool {
}
// VerifySignatures checks the we have sufficient valid signatures for the given role
func VerifySignatures(s *data.Signed, role string, db *keys.KeyDB) error {
func VerifySignatures(s *data.Signed, roleData data.BaseRole) error {
if len(s.Signatures) == 0 {
return ErrNoSignatures
}
roleData := db.GetRole(role)
if roleData == nil {
return ErrUnknownRole
}
if roleData.Threshold < 1 {
return ErrRoleThreshold{}
}
logrus.Debugf("%s role has key IDs: %s", role, strings.Join(roleData.KeyIDs, ","))
logrus.Debugf("%s role has key IDs: %s", roleData.Name, strings.Join(roleData.ListKeyIDs(), ","))
// remarshal the signed part so we can verify the signature, since the signature has
// to be of a canonically marshalled signed object
var decoded map[string]interface{}
if err := json.Unmarshal(s.Signed, &decoded); err != nil {
return err
@ -123,12 +119,8 @@ func VerifySignatures(s *data.Signed, role string, db *keys.KeyDB) error {
valid := make(map[string]struct{})
for _, sig := range s.Signatures {
logrus.Debug("verifying signature for key ID: ", sig.KeyID)
if !roleData.ValidKey(sig.KeyID) {
logrus.Debugf("continuing b/c keyid was invalid: %s for roledata %s\n", sig.KeyID, roleData)
continue
}
key := db.GetKey(sig.KeyID)
if key == nil {
key, ok := roleData.Keys[sig.KeyID]
if !ok {
logrus.Debugf("continuing b/c keyid lookup was nil: %s\n", sig.KeyID)
continue
}
@ -153,28 +145,3 @@ func VerifySignatures(s *data.Signed, role string, db *keys.KeyDB) error {
return nil
}
// Unmarshal unmarshals and verifys the raw bytes for a given role's metadata
func Unmarshal(b []byte, v interface{}, role string, minVersion int, db *keys.KeyDB) error {
s := &data.Signed{}
if err := json.Unmarshal(b, s); err != nil {
return err
}
if err := Verify(s, role, minVersion, db); err != nil {
return err
}
return json.Unmarshal(s.Signed, v)
}
// UnmarshalTrusted unmarshals and verifies signatures only, not metadata, for a
// given role's metadata
func UnmarshalTrusted(b []byte, v interface{}, role string, db *keys.KeyDB) error {
s := &data.Signed{}
if err := json.Unmarshal(b, s); err != nil {
return err
}
if err := VerifySignatures(s, role, db); err != nil {
return err
}
return json.Unmarshal(s.Signed, v)
}

View file

@ -2,6 +2,7 @@ package store
import (
"fmt"
"github.com/docker/notary"
"io/ioutil"
"os"
"path"
@ -9,25 +10,19 @@ import (
)
// NewFilesystemStore creates a new store in a directory tree
func NewFilesystemStore(baseDir, metaSubDir, metaExtension, targetsSubDir string) (*FilesystemStore, error) {
func NewFilesystemStore(baseDir, metaSubDir, metaExtension string) (*FilesystemStore, error) {
metaDir := path.Join(baseDir, metaSubDir)
targetsDir := path.Join(baseDir, targetsSubDir)
// Make sure we can create the necessary dirs and they are writable
err := os.MkdirAll(metaDir, 0700)
if err != nil {
return nil, err
}
err = os.MkdirAll(targetsDir, 0700)
if err != nil {
return nil, err
}
return &FilesystemStore{
baseDir: baseDir,
metaDir: metaDir,
metaExtension: metaExtension,
targetsDir: targetsDir,
}, nil
}
@ -36,7 +31,6 @@ type FilesystemStore struct {
baseDir string
metaDir string
metaExtension string
targetsDir string
}
func (f *FilesystemStore) getPath(name string) string {
@ -44,7 +38,8 @@ func (f *FilesystemStore) getPath(name string) string {
return filepath.Join(f.metaDir, fileName)
}
// GetMeta returns the meta for the given name (a role)
// GetMeta returns the meta for the given name (a role) up to size bytes
// If size is -1, this corresponds to "infinite," but we cut off at 100MB
func (f *FilesystemStore) GetMeta(name string, size int64) ([]byte, error) {
meta, err := ioutil.ReadFile(f.getPath(name))
if err != nil {
@ -53,8 +48,15 @@ func (f *FilesystemStore) GetMeta(name string, size int64) ([]byte, error) {
}
return nil, err
}
if size == -1 {
size = notary.MaxDownloadSize
}
// Only return up to size bytes
if int64(len(meta)) < size {
return meta, nil
}
return meta[:size], nil
}
// SetMultiMeta sets the metadata for multiple roles in one operation
func (f *FilesystemStore) SetMultiMeta(metas map[string][]byte) error {

View file

@ -23,6 +23,7 @@ import (
"path"
"github.com/Sirupsen/logrus"
"github.com/docker/notary"
"github.com/docker/notary/tuf/validation"
)
@ -33,6 +34,9 @@ type ErrServerUnavailable struct {
}
func (err ErrServerUnavailable) Error() string {
if err.code == 401 {
return fmt.Sprintf("you are not authorized to perform this operation: server returned 401.")
}
return fmt.Sprintf("unable to reach trust server at this time: %d.", err.code)
}
@ -71,13 +75,12 @@ type HTTPStore struct {
baseURL url.URL
metaPrefix string
metaExtension string
targetsPrefix string
keyExtension string
roundTrip http.RoundTripper
}
// NewHTTPStore initializes a new store against a URL and a number of configuration options
func NewHTTPStore(baseURL, metaPrefix, metaExtension, targetsPrefix, keyExtension string, roundTrip http.RoundTripper) (RemoteStore, error) {
func NewHTTPStore(baseURL, metaPrefix, metaExtension, keyExtension string, roundTrip http.RoundTripper) (RemoteStore, error) {
base, err := url.Parse(baseURL)
if err != nil {
return nil, err
@ -92,7 +95,6 @@ func NewHTTPStore(baseURL, metaPrefix, metaExtension, targetsPrefix, keyExtensio
baseURL: *base,
metaPrefix: metaPrefix,
metaExtension: metaExtension,
targetsPrefix: targetsPrefix,
keyExtension: keyExtension,
roundTrip: roundTrip,
}, nil
@ -137,6 +139,7 @@ func translateStatusToError(resp *http.Response, resource string) error {
// GetMeta downloads the named meta file with the given size. A short body
// is acceptable because in the case of timestamp.json, the size is a cap,
// not an exact length.
// If size is -1, this corresponds to "infinite," but we cut off at 100MB
func (s HTTPStore) GetMeta(name string, size int64) ([]byte, error) {
url, err := s.buildMetaURL(name)
if err != nil {
@ -155,6 +158,9 @@ func (s HTTPStore) GetMeta(name string, size int64) ([]byte, error) {
logrus.Debugf("received HTTP status %d when requesting %s.", resp.StatusCode, name)
return nil, err
}
if size == -1 {
size = notary.MaxDownloadSize
}
if resp.ContentLength > size {
return nil, ErrMaliciousServer{}
}
@ -250,11 +256,6 @@ func (s HTTPStore) buildMetaURL(name string) (*url.URL, error) {
return s.buildURL(uri)
}
func (s HTTPStore) buildTargetsURL(name string) (*url.URL, error) {
uri := path.Join(s.targetsPrefix, name)
return s.buildURL(uri)
}
func (s HTTPStore) buildKeyURL(name string) (*url.URL, error) {
filename := fmt.Sprintf("%s.%s", name, s.keyExtension)
uri := path.Join(s.metaPrefix, filename)
@ -269,29 +270,6 @@ func (s HTTPStore) buildURL(uri string) (*url.URL, error) {
return s.baseURL.ResolveReference(sub), nil
}
// GetTarget returns a reader for the desired target or an error.
// N.B. The caller is responsible for closing the reader.
func (s HTTPStore) GetTarget(path string) (io.ReadCloser, error) {
url, err := s.buildTargetsURL(path)
if err != nil {
return nil, err
}
logrus.Debug("Attempting to download target: ", url.String())
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, err
}
resp, err := s.roundTrip.RoundTrip(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := translateStatusToError(resp, path); err != nil {
return nil, err
}
return resp.Body, nil
}
// GetKey retrieves a public key from the remote server
func (s HTTPStore) GetKey(role string) ([]byte, error) {
url, err := s.buildKeyURL(role)

View file

@ -1,13 +1,5 @@
package store
import (
"io"
"github.com/docker/notary/tuf/data"
)
type targetsWalkFunc func(path string, meta data.FileMeta) error
// MetadataStore must be implemented by anything that intends to interact
// with a store of TUF files
type MetadataStore interface {
@ -23,17 +15,9 @@ type PublicKeyStore interface {
GetKey(role string) ([]byte, error)
}
// TargetStore represents a collection of targets that can be walked similarly
// to walking a directory, passing a callback that receives the path and meta
// for each target
type TargetStore interface {
WalkStagedTargets(paths []string, targetsFn targetsWalkFunc) error
}
// LocalStore represents a local TUF sture
type LocalStore interface {
MetadataStore
TargetStore
}
// RemoteStore is similar to LocalStore with the added expectation that it should
@ -41,5 +25,4 @@ type LocalStore interface {
type RemoteStore interface {
MetadataStore
PublicKeyStore
GetTarget(path string) (io.ReadCloser, error)
}

View file

@ -1,38 +1,59 @@
package store
import (
"bytes"
"crypto/sha256"
"fmt"
"io"
"github.com/docker/notary"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/utils"
)
// NewMemoryStore returns a MetadataStore that operates entirely in memory.
// Very useful for testing
func NewMemoryStore(meta map[string][]byte, files map[string][]byte) RemoteStore {
func NewMemoryStore(meta map[string][]byte) *MemoryStore {
var consistent = make(map[string][]byte)
if meta == nil {
meta = make(map[string][]byte)
} else {
// add all seed meta to consistent
for name, data := range meta {
checksum := sha256.Sum256(data)
path := utils.ConsistentName(name, checksum[:])
consistent[path] = data
}
if files == nil {
files = make(map[string][]byte)
}
return &memoryStore{
return &MemoryStore{
meta: meta,
files: files,
consistent: consistent,
keys: make(map[string][]data.PrivateKey),
}
}
type memoryStore struct {
// MemoryStore implements a mock RemoteStore entirely in memory.
// For testing purposes only.
type MemoryStore struct {
meta map[string][]byte
files map[string][]byte
consistent map[string][]byte
keys map[string][]data.PrivateKey
}
func (m *memoryStore) GetMeta(name string, size int64) ([]byte, error) {
// GetMeta returns up to size bytes of data references by name.
// If size is -1, this corresponds to "infinite," but we cut off at 100MB
// as we will always know the size for everything but a timestamp and
// sometimes a root, neither of which should be exceptionally large
func (m *MemoryStore) GetMeta(name string, size int64) ([]byte, error) {
d, ok := m.meta[name]
if ok {
if size == -1 {
size = notary.MaxDownloadSize
}
if int64(len(d)) < size {
return d, nil
}
return d[:size], nil
}
d, ok = m.consistent[name]
if ok {
if int64(len(d)) < size {
return d, nil
@ -42,12 +63,19 @@ func (m *memoryStore) GetMeta(name string, size int64) ([]byte, error) {
return nil, ErrMetaNotFound{Resource: name}
}
func (m *memoryStore) SetMeta(name string, meta []byte) error {
// SetMeta sets the metadata value for the given name
func (m *MemoryStore) SetMeta(name string, meta []byte) error {
m.meta[name] = meta
checksum := sha256.Sum256(meta)
path := utils.ConsistentName(name, checksum[:])
m.consistent[path] = meta
return nil
}
func (m *memoryStore) SetMultiMeta(metas map[string][]byte) error {
// SetMultiMeta sets multiple pieces of metadata for multiple names
// in a single operation.
func (m *MemoryStore) SetMultiMeta(metas map[string][]byte) error {
for role, blob := range metas {
m.SetMeta(role, blob)
}
@ -56,57 +84,23 @@ func (m *memoryStore) SetMultiMeta(metas map[string][]byte) error {
// RemoveMeta removes the metadata for a single role - if the metadata doesn't
// exist, no error is returned
func (m *memoryStore) RemoveMeta(name string) error {
func (m *MemoryStore) RemoveMeta(name string) error {
if meta, ok := m.meta[name]; ok {
checksum := sha256.Sum256(meta)
path := utils.ConsistentName(name, checksum[:])
delete(m.meta, name)
return nil
}
func (m *memoryStore) GetTarget(path string) (io.ReadCloser, error) {
return &utils.NoopCloser{Reader: bytes.NewReader(m.files[path])}, nil
}
func (m *memoryStore) WalkStagedTargets(paths []string, targetsFn targetsWalkFunc) error {
if len(paths) == 0 {
for path, dat := range m.files {
meta, err := data.NewFileMeta(bytes.NewReader(dat), "sha256")
if err != nil {
return err
}
if err = targetsFn(path, meta); err != nil {
return err
}
delete(m.consistent, path)
}
return nil
}
for _, path := range paths {
dat, ok := m.files[path]
if !ok {
return ErrMetaNotFound{Resource: path}
}
meta, err := data.NewFileMeta(bytes.NewReader(dat), "sha256")
if err != nil {
return err
}
if err = targetsFn(path, meta); err != nil {
return err
}
}
return nil
// GetKey returns the public key for the given role
func (m *MemoryStore) GetKey(role string) ([]byte, error) {
return nil, fmt.Errorf("GetKey is not implemented for the MemoryStore")
}
func (m *memoryStore) Commit(map[string][]byte, bool, map[string]data.Hashes) error {
return nil
}
func (m *memoryStore) GetKey(role string) ([]byte, error) {
return nil, fmt.Errorf("GetKey is not implemented for the memoryStore")
}
// Clear this existing memory store by setting this store as new empty one
func (m *memoryStore) RemoveAll() error {
m.meta = make(map[string][]byte)
m.files = make(map[string][]byte)
m.keys = make(map[string][]data.PrivateKey)
// RemoveAll clears the existing memory store by setting this store as new empty one
func (m *MemoryStore) RemoveAll() error {
*m = *NewMemoryStore(nil)
return nil
}

View file

@ -3,8 +3,6 @@ package tuf
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"path"
@ -12,8 +10,8 @@ import (
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/notary"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/keys"
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/utils"
)
@ -40,15 +38,19 @@ func (e ErrLocalRootExpired) Error() string {
}
// ErrNotLoaded - attempted to access data that has not been loaded into
// the repo
// the repo. This means specifically that the relevant JSON file has not
// been loaded.
type ErrNotLoaded struct {
role string
Role string
}
func (err ErrNotLoaded) Error() string {
return fmt.Sprintf("%s role has not been loaded", err.role)
return fmt.Sprintf("%s role has not been loaded", err.Role)
}
// StopWalk - used by visitor functions to signal WalkTargets to stop walking
type StopWalk struct{}
// Repo is an in memory representation of the TUF Repo.
// It operates at the data.Signed level, accepting and producing
// data.Signed objects. Users of a Repo are responsible for
@ -59,16 +61,15 @@ type Repo struct {
Targets map[string]*data.SignedTargets
Snapshot *data.SignedSnapshot
Timestamp *data.SignedTimestamp
keysDB *keys.KeyDB
cryptoService signed.CryptoService
}
// NewRepo initializes a Repo instance with a keysDB and a signer.
// If the Repo will only be used for reading, the signer should be nil.
func NewRepo(keysDB *keys.KeyDB, cryptoService signed.CryptoService) *Repo {
// NewRepo initializes a Repo instance with a CryptoService.
// If the Repo will only be used for reading, the CryptoService
// can be nil.
func NewRepo(cryptoService signed.CryptoService) *Repo {
repo := &Repo{
Targets: make(map[string]*data.SignedTargets),
keysDB: keysDB,
cryptoService: cryptoService,
}
return repo
@ -77,27 +78,15 @@ func NewRepo(keysDB *keys.KeyDB, cryptoService signed.CryptoService) *Repo {
// AddBaseKeys is used to add keys to the role in root.json
func (tr *Repo) AddBaseKeys(role string, keys ...data.PublicKey) error {
if tr.Root == nil {
return ErrNotLoaded{role: "root"}
return ErrNotLoaded{Role: data.CanonicalRootRole}
}
ids := []string{}
for _, k := range keys {
// Store only the public portion
tr.Root.Signed.Keys[k.ID()] = k
tr.keysDB.AddKey(k)
tr.Root.Signed.Roles[role].KeyIDs = append(tr.Root.Signed.Roles[role].KeyIDs, k.ID())
ids = append(ids, k.ID())
}
r, err := data.NewRole(
role,
tr.Root.Signed.Roles[role].Threshold,
ids,
nil,
nil,
)
if err != nil {
return err
}
tr.keysDB.AddRole(r)
tr.Root.Dirty = true
// also, whichever role was switched out needs to be re-signed
@ -121,8 +110,11 @@ func (tr *Repo) AddBaseKeys(role string, keys ...data.PublicKey) error {
// ReplaceBaseKeys is used to replace all keys for the given role with the new keys
func (tr *Repo) ReplaceBaseKeys(role string, keys ...data.PublicKey) error {
r := tr.keysDB.GetRole(role)
err := tr.RemoveBaseKeys(role, r.KeyIDs...)
r, err := tr.GetBaseRole(role)
if err != nil {
return err
}
err = tr.RemoveBaseKeys(role, r.ListKeyIDs()...)
if err != nil {
return err
}
@ -132,7 +124,7 @@ func (tr *Repo) ReplaceBaseKeys(role string, keys ...data.PublicKey) error {
// RemoveBaseKeys is used to remove keys from the roles in root.json
func (tr *Repo) RemoveBaseKeys(role string, keyIDs ...string) error {
if tr.Root == nil {
return ErrNotLoaded{role: "root"}
return ErrNotLoaded{Role: data.CanonicalRootRole}
}
var keep []string
toDelete := make(map[string]struct{})
@ -173,117 +165,253 @@ func (tr *Repo) RemoveBaseKeys(role string, keyIDs ...string) error {
return nil
}
// GetBaseRole gets a base role from this repo's metadata
func (tr *Repo) GetBaseRole(name string) (data.BaseRole, error) {
if !data.ValidRole(name) {
return data.BaseRole{}, data.ErrInvalidRole{Role: name, Reason: "invalid base role name"}
}
if tr.Root == nil {
return data.BaseRole{}, ErrNotLoaded{data.CanonicalRootRole}
}
// Find the role data public keys for the base role from TUF metadata
baseRole, err := tr.Root.BuildBaseRole(name)
if err != nil {
return data.BaseRole{}, err
}
return baseRole, nil
}
// GetDelegationRole gets a delegation role from this repo's metadata, walking from the targets role down to the delegation itself
func (tr *Repo) GetDelegationRole(name string) (data.DelegationRole, error) {
if !data.IsDelegation(name) {
return data.DelegationRole{}, data.ErrInvalidRole{Role: name, Reason: "invalid delegation name"}
}
if tr.Root == nil {
return data.DelegationRole{}, ErrNotLoaded{data.CanonicalRootRole}
}
_, ok := tr.Root.Signed.Roles[data.CanonicalTargetsRole]
if !ok {
return data.DelegationRole{}, ErrNotLoaded{data.CanonicalTargetsRole}
}
// Traverse target metadata, down to delegation itself
// Get all public keys for the base role from TUF metadata
_, ok = tr.Targets[data.CanonicalTargetsRole]
if !ok {
return data.DelegationRole{}, ErrNotLoaded{data.CanonicalTargetsRole}
}
// Start with top level roles in targets. Walk the chain of ancestors
// until finding the desired role, or we run out of targets files to search.
var foundRole *data.DelegationRole
buildDelegationRoleVisitor := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
// Try to find the delegation and build a DelegationRole structure
for _, role := range tgt.Signed.Delegations.Roles {
if role.Name == name {
delgRole, err := tgt.BuildDelegationRole(name)
if err != nil {
return err
}
foundRole = &delgRole
return StopWalk{}
}
}
return nil
}
// Walk to the parent of this delegation, since that is where its role metadata exists
err := tr.WalkTargets("", path.Dir(name), buildDelegationRoleVisitor)
if err != nil {
return data.DelegationRole{}, err
}
// We never found the delegation. In the context of this repo it is considered
// invalid. N.B. it may be that it existed at one point but an ancestor has since
// been modified/removed.
if foundRole == nil {
return data.DelegationRole{}, data.ErrInvalidRole{Role: name, Reason: "delegation does not exist"}
}
return *foundRole, nil
}
// GetAllLoadedRoles returns a list of all role entries loaded in this TUF repo, could be empty
func (tr *Repo) GetAllLoadedRoles() []*data.Role {
return tr.keysDB.GetAllRoles()
var res []*data.Role
if tr.Root == nil {
// if root isn't loaded, we should consider we have no loaded roles because we can't
// trust any other state that might be present
return res
}
for name, rr := range tr.Root.Signed.Roles {
res = append(res, &data.Role{
RootRole: *rr,
Name: name,
})
}
for _, delegate := range tr.Targets {
for _, r := range delegate.Signed.Delegations.Roles {
res = append(res, r)
}
}
return res
}
// GetDelegation finds the role entry representing the provided
// role name or ErrInvalidRole
func (tr *Repo) GetDelegation(role string) (*data.Role, error) {
r := data.Role{Name: role}
if !r.IsDelegation() {
return nil, data.ErrInvalidRole{Role: role, Reason: "not a valid delegated role"}
// Walk to parent, and either create or update this delegation. We can only create a new delegation if we're given keys
// Ensure all updates are valid, by checking against parent ancestor paths and ensuring the keys meet the role threshold.
func delegationUpdateVisitor(roleName string, addKeys data.KeyList, removeKeys, addPaths, removePaths []string, clearAllPaths bool, newThreshold int) walkVisitorFunc {
return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
var err error
// Validate the changes underneath this restricted validRole for adding paths, reject invalid path additions
if len(addPaths) != len(data.RestrictDelegationPathPrefixes(validRole.Paths, addPaths)) {
return data.ErrInvalidRole{Role: roleName, Reason: "invalid paths to add to role"}
}
parent := path.Dir(role)
// check the parent role
if parentRole := tr.keysDB.GetRole(parent); parentRole == nil {
return nil, data.ErrInvalidRole{Role: role, Reason: "parent role not found"}
// Try to find the delegation and amend it using our changelist
var delgRole *data.Role
for _, role := range tgt.Signed.Delegations.Roles {
if role.Name == roleName {
// Make a copy and operate on this role until we validate the changes
keyIDCopy := make([]string, len(role.KeyIDs))
copy(keyIDCopy, role.KeyIDs)
pathsCopy := make([]string, len(role.Paths))
copy(pathsCopy, role.Paths)
delgRole = &data.Role{
RootRole: data.RootRole{
KeyIDs: keyIDCopy,
Threshold: role.Threshold,
},
Name: role.Name,
Paths: pathsCopy,
}
// check the parent role's metadata
p, ok := tr.Targets[parent]
if !ok { // the parent targetfile may not exist yet, so it can't be in the list
return nil, data.ErrNoSuchRole{Role: role}
delgRole.RemovePaths(removePaths)
if clearAllPaths {
delgRole.Paths = []string{}
}
foundAt := utils.FindRoleIndex(p.Signed.Delegations.Roles, role)
delgRole.AddPaths(addPaths)
delgRole.RemoveKeys(removeKeys)
break
}
}
// We didn't find the role earlier, so create it only if we have keys to add
if delgRole == nil {
if len(addKeys) > 0 {
delgRole, err = data.NewRole(roleName, newThreshold, addKeys.IDs(), addPaths)
if err != nil {
return err
}
} else {
// If we can't find the role and didn't specify keys to add, this is an error
return data.ErrInvalidRole{Role: roleName, Reason: "cannot create new delegation without keys"}
}
}
// Add the key IDs to the role and the keys themselves to the parent
for _, k := range addKeys {
if !utils.StrSliceContains(delgRole.KeyIDs, k.ID()) {
delgRole.KeyIDs = append(delgRole.KeyIDs, k.ID())
}
}
// Make sure we have a valid role still
if len(delgRole.KeyIDs) < delgRole.Threshold {
return data.ErrInvalidRole{Role: roleName, Reason: "insufficient keys to meet threshold"}
}
// NOTE: this closure CANNOT error after this point, as we've committed to editing the SignedTargets metadata in the repo object.
// Any errors related to updating this delegation must occur before this point.
// If all of our changes were valid, we should edit the actual SignedTargets to match our copy
for _, k := range addKeys {
tgt.Signed.Delegations.Keys[k.ID()] = k
}
foundAt := utils.FindRoleIndex(tgt.Signed.Delegations.Roles, delgRole.Name)
if foundAt < 0 {
return nil, data.ErrNoSuchRole{Role: role}
tgt.Signed.Delegations.Roles = append(tgt.Signed.Delegations.Roles, delgRole)
} else {
tgt.Signed.Delegations.Roles[foundAt] = delgRole
}
tgt.Dirty = true
utils.RemoveUnusedKeys(tgt)
return StopWalk{}
}
return p.Signed.Delegations.Roles[foundAt], nil
}
// UpdateDelegations updates the appropriate delegations, either adding
// UpdateDelegationKeys updates the appropriate delegations, either adding
// a new delegation or updating an existing one. If keys are
// provided, the IDs will be added to the role (if they do not exist
// there already), and the keys will be added to the targets file.
func (tr *Repo) UpdateDelegations(role *data.Role, keys []data.PublicKey) error {
if !role.IsDelegation() || !role.IsValid() {
return data.ErrInvalidRole{Role: role.Name, Reason: "not a valid delegated role"}
func (tr *Repo) UpdateDelegationKeys(roleName string, addKeys data.KeyList, removeKeys []string, newThreshold int) error {
if !data.IsDelegation(roleName) {
return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"}
}
parent := path.Dir(role.Name)
parent := path.Dir(roleName)
if err := tr.VerifyCanSign(parent); err != nil {
return err
}
// check the parent role's metadata
p, ok := tr.Targets[parent]
_, ok := tr.Targets[parent]
if !ok { // the parent targetfile may not exist yet - if not, then create it
var err error
p, err = tr.InitTargets(parent)
_, err = tr.InitTargets(parent)
if err != nil {
return err
}
}
for _, k := range keys {
if !utils.StrSliceContains(role.KeyIDs, k.ID()) {
role.KeyIDs = append(role.KeyIDs, k.ID())
// Walk to the parent of this delegation, since that is where its role metadata exists
// We do not have to verify that the walker reached its desired role in this scenario
// since we've already done another walk to the parent role in VerifyCanSign, and potentially made a targets file
err := tr.WalkTargets("", parent, delegationUpdateVisitor(roleName, addKeys, removeKeys, []string{}, []string{}, false, newThreshold))
if err != nil {
return err
}
p.Signed.Delegations.Keys[k.ID()] = k
tr.keysDB.AddKey(k)
return nil
}
// if the role has fewer keys than the threshold, it
// will never be able to create a valid targets file
// and should be considered invalid.
if len(role.KeyIDs) < role.Threshold {
return data.ErrInvalidRole{Role: role.Name, Reason: "insufficient keys to meet threshold"}
// UpdateDelegationPaths updates the appropriate delegation's paths.
// It is not allowed to create a new delegation.
func (tr *Repo) UpdateDelegationPaths(roleName string, addPaths, removePaths []string, clearPaths bool) error {
if !data.IsDelegation(roleName) {
return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"}
}
parent := path.Dir(roleName)
if err := tr.VerifyCanSign(parent); err != nil {
return err
}
foundAt := utils.FindRoleIndex(p.Signed.Delegations.Roles, role.Name)
if foundAt >= 0 {
p.Signed.Delegations.Roles[foundAt] = role
} else {
p.Signed.Delegations.Roles = append(p.Signed.Delegations.Roles, role)
// check the parent role's metadata
_, ok := tr.Targets[parent]
if !ok { // the parent targetfile may not exist yet
// if not, this is an error because a delegation must exist to edit only paths
return data.ErrInvalidRole{Role: roleName, Reason: "no valid delegated role exists"}
}
// We've made a change to parent. Set it to dirty
p.Dirty = true
// We don't actually want to create the new delegation metadata yet.
// When we add a delegation, it may only be signable by a key we don't have
// (hence we are delegating signing).
tr.keysDB.AddRole(role)
utils.RemoveUnusedKeys(p)
// Walk to the parent of this delegation, since that is where its role metadata exists
// We do not have to verify that the walker reached its desired role in this scenario
// since we've already done another walk to the parent role in VerifyCanSign
err := tr.WalkTargets("", parent, delegationUpdateVisitor(roleName, data.KeyList{}, []string{}, addPaths, removePaths, clearPaths, notary.MinThreshold))
if err != nil {
return err
}
return nil
}
// DeleteDelegation removes a delegated targets role from its parent
// targets object. It also deletes the delegation from the snapshot.
// DeleteDelegation will only make use of the role Name field.
func (tr *Repo) DeleteDelegation(role data.Role) error {
if !role.IsDelegation() {
return data.ErrInvalidRole{Role: role.Name, Reason: "not a valid delegated role"}
func (tr *Repo) DeleteDelegation(roleName string) error {
if !data.IsDelegation(roleName) {
return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"}
}
// the role variable must not be used past this assignment for safety
name := role.Name
parent := path.Dir(name)
parent := path.Dir(roleName)
if err := tr.VerifyCanSign(parent); err != nil {
return err
}
// delete delegated data from Targets map and Snapshot - if they don't
// exist, these are no-op
delete(tr.Targets, name)
tr.Snapshot.DeleteMeta(name)
delete(tr.Targets, roleName)
tr.Snapshot.DeleteMeta(roleName)
p, ok := tr.Targets[parent]
if !ok {
@ -292,7 +420,7 @@ func (tr *Repo) DeleteDelegation(role data.Role) error {
return nil
}
foundAt := utils.FindRoleIndex(p.Signed.Delegations.Roles, name)
foundAt := utils.FindRoleIndex(p.Signed.Delegations.Roles, roleName)
if foundAt >= 0 {
var roles []*data.Role
@ -311,53 +439,32 @@ func (tr *Repo) DeleteDelegation(role data.Role) error {
return nil
}
// InitRepo creates the base files for a repo. It inspects data.BaseRoles and
// data.ValidTypes to determine what the role names and filename should be. It
// also relies on the keysDB having already been populated with the keys and
// roles.
func (tr *Repo) InitRepo(consistent bool) error {
if err := tr.InitRoot(consistent); err != nil {
return err
}
if _, err := tr.InitTargets(data.CanonicalTargetsRole); err != nil {
return err
}
if err := tr.InitSnapshot(); err != nil {
return err
}
return tr.InitTimestamp()
}
// InitRoot initializes an empty root file with the 4 core roles based
// on the current content of th ekey db
func (tr *Repo) InitRoot(consistent bool) error {
// InitRoot initializes an empty root file with the 4 core roles passed to the
// method, and the consistent flag.
func (tr *Repo) InitRoot(root, timestamp, snapshot, targets data.BaseRole, consistent bool) error {
rootRoles := make(map[string]*data.RootRole)
rootKeys := make(map[string]data.PublicKey)
for _, r := range data.BaseRoles {
role := tr.keysDB.GetRole(r)
if role == nil {
return data.ErrInvalidRole{Role: data.CanonicalRootRole, Reason: "root role not initialized in key database"}
for _, r := range []data.BaseRole{root, timestamp, snapshot, targets} {
rootRoles[r.Name] = &data.RootRole{
Threshold: r.Threshold,
KeyIDs: r.ListKeyIDs(),
}
rootRoles[r] = &role.RootRole
for _, kid := range role.KeyIDs {
// don't need to check if GetKey returns nil, Key presence was
// checked by KeyDB when role was added.
key := tr.keysDB.GetKey(kid)
rootKeys[kid] = key
for kid, k := range r.Keys {
rootKeys[kid] = k
}
}
root, err := data.NewRoot(rootKeys, rootRoles, consistent)
r, err := data.NewRoot(rootKeys, rootRoles, consistent)
if err != nil {
return err
}
tr.Root = root
tr.Root = r
return nil
}
// InitTargets initializes an empty targets, and returns the new empty target
func (tr *Repo) InitTargets(role string) (*data.SignedTargets, error) {
r := data.Role{Name: role}
if !r.IsDelegation() && role != data.CanonicalTargetsRole {
if !data.IsDelegation(role) && role != data.CanonicalTargetsRole {
return nil, data.ErrInvalidRole{
Role: role,
Reason: fmt.Sprintf("role is not a valid targets role name: %s", role),
@ -371,7 +478,7 @@ func (tr *Repo) InitTargets(role string) (*data.SignedTargets, error) {
// InitSnapshot initializes a snapshot based on the current root and targets
func (tr *Repo) InitSnapshot() error {
if tr.Root == nil {
return ErrNotLoaded{role: "root"}
return ErrNotLoaded{Role: data.CanonicalRootRole}
}
root, err := tr.Root.ToSigned()
if err != nil {
@ -379,7 +486,7 @@ func (tr *Repo) InitSnapshot() error {
}
if _, ok := tr.Targets[data.CanonicalTargetsRole]; !ok {
return ErrNotLoaded{role: "targets"}
return ErrNotLoaded{Role: data.CanonicalTargetsRole}
}
targets, err := tr.Targets[data.CanonicalTargetsRole].ToSigned()
if err != nil {
@ -408,31 +515,8 @@ func (tr *Repo) InitTimestamp() error {
return nil
}
// SetRoot parses the Signed object into a SignedRoot object, sets
// the keys and roles in the KeyDB, and sets the Repo.Root field
// to the SignedRoot object.
// SetRoot sets the Repo.Root field to the SignedRoot object.
func (tr *Repo) SetRoot(s *data.SignedRoot) error {
for _, key := range s.Signed.Keys {
logrus.Debug("Adding key ", key.ID())
tr.keysDB.AddKey(key)
}
for roleName, role := range s.Signed.Roles {
logrus.Debugf("Adding role %s with keys %s", roleName, strings.Join(role.KeyIDs, ","))
baseRole, err := data.NewRole(
roleName,
role.Threshold,
role.KeyIDs,
nil,
nil,
)
if err != nil {
return err
}
err = tr.keysDB.AddRole(baseRole)
if err != nil {
return err
}
}
tr.Root = s
return nil
}
@ -451,16 +535,9 @@ func (tr *Repo) SetSnapshot(s *data.SignedSnapshot) error {
return nil
}
// SetTargets parses the Signed object into a SignedTargets object,
// reads the delegated roles and keys into the KeyDB, and sets the
// SignedTargets object agaist the role in the Repo.Targets map.
// SetTargets sets the SignedTargets object agaist the role in the
// Repo.Targets map.
func (tr *Repo) SetTargets(role string, s *data.SignedTargets) error {
for _, k := range s.Signed.Delegations.Keys {
tr.keysDB.AddKey(k)
}
for _, r := range s.Signed.Delegations.Roles {
tr.keysDB.AddRole(r)
}
tr.Targets[role] = s
return nil
}
@ -479,15 +556,11 @@ func (tr Repo) TargetMeta(role, path string) *data.FileMeta {
// TargetDelegations returns a slice of Roles that are valid publishers
// for the target path provided.
func (tr Repo) TargetDelegations(role, path, pathHex string) []*data.Role {
if pathHex == "" {
pathDigest := sha256.Sum256([]byte(path))
pathHex = hex.EncodeToString(pathDigest[:])
}
func (tr Repo) TargetDelegations(role, path string) []*data.Role {
var roles []*data.Role
if t, ok := tr.Targets[role]; ok {
for _, r := range t.Signed.Delegations.Roles {
if r.CheckPrefixes(pathHex) || r.CheckPaths(path) {
if r.CheckPaths(path) {
roles = append(roles, r)
}
}
@ -495,50 +568,34 @@ func (tr Repo) TargetDelegations(role, path, pathHex string) []*data.Role {
return roles
}
// FindTarget attempts to find the target represented by the given
// path by starting at the top targets file and traversing
// appropriate delegations until the first entry is found or it
// runs out of locations to search.
// N.B. Multiple entries may exist in different delegated roles
// for the same target. Only the first one encountered is returned.
func (tr Repo) FindTarget(path string) *data.FileMeta {
pathDigest := sha256.Sum256([]byte(path))
pathHex := hex.EncodeToString(pathDigest[:])
var walkTargets func(role string) *data.FileMeta
walkTargets = func(role string) *data.FileMeta {
if m := tr.TargetMeta(role, path); m != nil {
return m
}
// Depth first search of delegations based on order
// as presented in current targets file for role:
for _, r := range tr.TargetDelegations(role, path, pathHex) {
if m := walkTargets(r.Name); m != nil {
return m
}
}
return nil
}
return walkTargets("targets")
}
// VerifyCanSign returns nil if the role exists and we have at least one
// signing key for the role, false otherwise. This does not check that we have
// enough signing keys to meet the threshold, since we want to support the use
// case of multiple signers for a role. It returns an error if the role doesn't
// exist or if there are no signing keys.
func (tr *Repo) VerifyCanSign(roleName string) error {
role := tr.keysDB.GetRole(roleName)
if role == nil {
var (
role data.BaseRole
err error
)
// we only need the BaseRole part of a delegation because we're just
// checking KeyIDs
if data.IsDelegation(roleName) {
r, err := tr.GetDelegationRole(roleName)
if err != nil {
return err
}
role = r.BaseRole
} else {
role, err = tr.GetBaseRole(roleName)
}
if err != nil {
return data.ErrInvalidRole{Role: roleName, Reason: "does not exist"}
}
for _, keyID := range role.KeyIDs {
k := tr.keysDB.GetKey(keyID)
canonicalID, err := utils.CanonicalKeyID(k)
for keyID, k := range role.Keys {
check := []string{keyID}
if err == nil {
if canonicalID, err := utils.CanonicalKeyID(k); err == nil {
check = append(check, canonicalID)
}
for _, id := range check {
@ -548,45 +605,123 @@ func (tr *Repo) VerifyCanSign(roleName string) error {
}
}
}
return signed.ErrNoKeys{KeyIDs: role.KeyIDs}
return signed.ErrNoKeys{KeyIDs: role.ListKeyIDs()}
}
// used for walking the targets/delegations tree, potentially modifying the underlying SignedTargets for the repo
type walkVisitorFunc func(*data.SignedTargets, data.DelegationRole) interface{}
// WalkTargets will apply the specified visitor function to iteratively walk the targets/delegation metadata tree,
// until receiving a StopWalk. The walk starts from the base "targets" role, and searches for the correct targetPath and/or rolePath
// to call the visitor function on. Any roles passed into skipRoles will be excluded from the walk, as well as roles in those subtrees
func (tr *Repo) WalkTargets(targetPath, rolePath string, visitTargets walkVisitorFunc, skipRoles ...string) error {
// Start with the base targets role, which implicitly has the "" targets path
targetsRole, err := tr.GetBaseRole(data.CanonicalTargetsRole)
if err != nil {
return err
}
// Make the targets role have the empty path, when we treat it as a delegation role
roles := []data.DelegationRole{
{
BaseRole: targetsRole,
Paths: []string{""},
},
}
for len(roles) > 0 {
role := roles[0]
roles = roles[1:]
// Check the role metadata
signedTgt, ok := tr.Targets[role.Name]
if !ok {
// The role meta doesn't exist in the repo so continue onward
continue
}
// We're at a prefix of the desired role subtree, so add its delegation role children and continue walking
if strings.HasPrefix(rolePath, role.Name+"/") {
roles = append(roles, signedTgt.GetValidDelegations(role)...)
continue
}
// Determine whether to visit this role or not:
// If the paths validate against the specified targetPath and the rolePath is empty or is in the subtree
// Also check if we are choosing to skip visiting this role on this walk (see ListTargets and GetTargetByName priority)
if isValidPath(targetPath, role) && isAncestorRole(role.Name, rolePath) && !utils.StrSliceContains(skipRoles, role.Name) {
// If we had matching path or role name, visit this target and determine whether or not to keep walking
res := visitTargets(signedTgt, role)
switch typedRes := res.(type) {
case StopWalk:
// If the visitor function signalled a stop, return nil to finish the walk
return nil
case nil:
// If the visitor function signalled to continue, add this role's delegation to the walk
roles = append(roles, signedTgt.GetValidDelegations(role)...)
case error:
// Propagate any errors from the visitor
return typedRes
default:
// Return out with an error if we got a different result
return fmt.Errorf("unexpected return while walking: %v", res)
}
}
}
return nil
}
// helper function that returns whether the candidateChild role name is an ancestor or equal to the candidateAncestor role name
// Will return true if given an empty candidateAncestor role name
// The HasPrefix check is for determining whether the role name for candidateChild is a child (direct or further down the chain)
// of candidateAncestor, for ex: candidateAncestor targets/a and candidateChild targets/a/b/c
func isAncestorRole(candidateChild, candidateAncestor string) bool {
return candidateAncestor == "" || candidateAncestor == candidateChild || strings.HasPrefix(candidateChild, candidateAncestor+"/")
}
// helper function that returns whether the delegation Role is valid against the given path
// Will return true if given an empty candidatePath
func isValidPath(candidatePath string, delgRole data.DelegationRole) bool {
return candidatePath == "" || delgRole.CheckPaths(candidatePath)
}
// AddTargets will attempt to add the given targets specifically to
// the directed role. If the metadata for the role doesn't exist yet,
// AddTargets will create one.
func (tr *Repo) AddTargets(role string, targets data.Files) (data.Files, error) {
err := tr.VerifyCanSign(role)
if err != nil {
return nil, err
}
// check the role's metadata
t, ok := tr.Targets[role]
// check existence of the role's metadata
_, ok := tr.Targets[role]
if !ok { // the targetfile may not exist yet - if not, then create it
var err error
t, err = tr.InitTargets(role)
_, err = tr.InitTargets(role)
if err != nil {
return nil, err
}
}
// VerifyCanSign already makes sure this is not nil
r := tr.keysDB.GetRole(role)
addedTargets := make(data.Files)
addTargetVisitor := func(targetPath string, targetMeta data.FileMeta) func(*data.SignedTargets, data.DelegationRole) interface{} {
return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
// We've already validated the role's target path in our walk, so just modify the metadata
tgt.Signed.Targets[targetPath] = targetMeta
tgt.Dirty = true
// Also add to our new addedTargets map to keep track of every target we've added successfully
addedTargets[targetPath] = targetMeta
return StopWalk{}
}
}
invalid := make(data.Files)
// Walk the role tree while validating the target paths, and add all of our targets
for path, target := range targets {
pathDigest := sha256.Sum256([]byte(path))
pathHex := hex.EncodeToString(pathDigest[:])
if role == data.CanonicalTargetsRole || (r.CheckPaths(path) || r.CheckPrefixes(pathHex)) {
t.Signed.Targets[path] = target
} else {
invalid[path] = target
tr.WalkTargets(path, role, addTargetVisitor(path, target))
}
}
t.Dirty = true
if len(invalid) > 0 {
return invalid, fmt.Errorf("Could not add all targets")
if len(addedTargets) != len(targets) {
return nil, fmt.Errorf("Could not add all targets")
}
return nil, nil
}
@ -597,13 +732,23 @@ func (tr *Repo) RemoveTargets(role string, targets ...string) error {
return err
}
removeTargetVisitor := func(targetPath string) func(*data.SignedTargets, data.DelegationRole) interface{} {
return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
// We've already validated the role path in our walk, so just modify the metadata
// We don't check against the target path against the valid role paths because it's
// possible we got into an invalid state and are trying to fix it
delete(tgt.Signed.Targets, targetPath)
tgt.Dirty = true
return StopWalk{}
}
}
// if the role exists but metadata does not yet, then our work is done
t, ok := tr.Targets[role]
_, ok := tr.Targets[role]
if ok {
for _, path := range targets {
delete(t.Signed.Targets, path)
tr.WalkTargets("", role, removeTargetVisitor(path))
}
t.Dirty = true
}
return nil
@ -644,12 +789,15 @@ func (tr *Repo) SignRoot(expires time.Time) (*data.Signed, error) {
logrus.Debug("signing root...")
tr.Root.Signed.Expires = expires
tr.Root.Signed.Version++
root := tr.keysDB.GetRole(data.CanonicalRootRole)
root, err := tr.GetBaseRole(data.CanonicalRootRole)
if err != nil {
return nil, err
}
signed, err := tr.Root.ToSigned()
if err != nil {
return nil, err
}
signed, err = tr.sign(signed, *root)
signed, err = tr.sign(signed, root)
if err != nil {
return nil, err
}
@ -673,8 +821,22 @@ func (tr *Repo) SignTargets(role string, expires time.Time) (*data.Signed, error
logrus.Debug("errored getting targets data.Signed object")
return nil, err
}
targets := tr.keysDB.GetRole(role)
signed, err = tr.sign(signed, *targets)
var targets data.BaseRole
if role == data.CanonicalTargetsRole {
targets, err = tr.GetBaseRole(role)
} else {
tr, err := tr.GetDelegationRole(role)
if err != nil {
return nil, err
}
targets = tr.BaseRole
}
if err != nil {
return nil, err
}
signed, err = tr.sign(signed, targets)
if err != nil {
logrus.Debug("errored signing ", role)
return nil, err
@ -712,8 +874,11 @@ func (tr *Repo) SignSnapshot(expires time.Time) (*data.Signed, error) {
if err != nil {
return nil, err
}
snapshot := tr.keysDB.GetRole(data.CanonicalSnapshotRole)
signed, err = tr.sign(signed, *snapshot)
snapshot, err := tr.GetBaseRole(data.CanonicalSnapshotRole)
if err != nil {
return nil, err
}
signed, err = tr.sign(signed, snapshot)
if err != nil {
return nil, err
}
@ -738,8 +903,11 @@ func (tr *Repo) SignTimestamp(expires time.Time) (*data.Signed, error) {
if err != nil {
return nil, err
}
timestamp := tr.keysDB.GetRole(data.CanonicalTimestampRole)
signed, err = tr.sign(signed, *timestamp)
timestamp, err := tr.GetBaseRole(data.CanonicalTimestampRole)
if err != nil {
return nil, err
}
signed, err = tr.sign(signed, timestamp)
if err != nil {
return nil, err
}
@ -748,17 +916,10 @@ func (tr *Repo) SignTimestamp(expires time.Time) (*data.Signed, error) {
return signed, nil
}
func (tr Repo) sign(signedData *data.Signed, role data.Role) (*data.Signed, error) {
ks := make([]data.PublicKey, 0, len(role.KeyIDs))
for _, kid := range role.KeyIDs {
k := tr.keysDB.GetKey(kid)
if k == nil {
continue
}
ks = append(ks, k)
}
func (tr Repo) sign(signedData *data.Signed, role data.BaseRole) (*data.Signed, error) {
ks := role.ListKeys()
if len(ks) < 1 {
return nil, keys.ErrInvalidKey
return nil, signed.ErrNoKeys{}
}
err := signed.Sign(tr.cryptoService, signedData, ks...)
if err != nil {

View file

@ -5,6 +5,7 @@ import (
"crypto/sha256"
"crypto/sha512"
"crypto/tls"
"encoding/hex"
"fmt"
"io"
"net/http"
@ -61,6 +62,17 @@ func StrSliceContains(ss []string, s string) bool {
return false
}
// StrSliceRemove removes the the given string from the slice, returning a new slice
func StrSliceRemove(ss []string, s string) []string {
res := []string{}
for _, v := range ss {
if v != s {
res = append(res, v)
}
}
return res
}
// StrSliceContainsI checks if the given string appears in the slice
// in a case insensitive manner
func StrSliceContainsI(ss []string, s string) bool {
@ -146,3 +158,14 @@ func FindRoleIndex(rs []*data.Role, name string) int {
}
return -1
}
// ConsistentName generates the appropriate HTTP URL path for the role,
// based on whether the repo is marked as consistent. The RemoteStore
// is responsible for adding file extensions.
func ConsistentName(role string, hashSha256 []byte) string {
if len(hashSha256) > 0 {
hash := hex.EncodeToString(hashSha256)
return fmt.Sprintf("%s.%s", role, hash)
}
return role
}