Notary delegation integration into docker
Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
This commit is contained in:
parent
6213cc9d04
commit
1c125f50cf
|
|
@ -153,7 +153,7 @@ RUN set -x \
|
||||||
&& rm -rf "$GOPATH"
|
&& rm -rf "$GOPATH"
|
||||||
|
|
||||||
# Install notary server
|
# Install notary server
|
||||||
ENV NOTARY_COMMIT 8e8122eb5528f621afcd4e2854c47302f17392f7
|
ENV NOTARY_COMMIT 30c488b3b4c62fdbc2c1eae7cf3b62ca73f95fad
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& export GOPATH="$(mktemp -d)" \
|
&& export GOPATH="$(mktemp -d)" \
|
||||||
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \
|
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
@ -35,9 +36,14 @@ import (
|
||||||
"github.com/docker/notary/passphrase"
|
"github.com/docker/notary/passphrase"
|
||||||
"github.com/docker/notary/trustmanager"
|
"github.com/docker/notary/trustmanager"
|
||||||
"github.com/docker/notary/tuf/data"
|
"github.com/docker/notary/tuf/data"
|
||||||
|
"github.com/docker/notary/tuf/signed"
|
||||||
|
"github.com/docker/notary/tuf/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
var untrusted bool
|
var (
|
||||||
|
releasesRole = path.Join(data.CanonicalTargetsRole, "releases")
|
||||||
|
untrusted bool
|
||||||
|
)
|
||||||
|
|
||||||
func addTrustedFlags(fs *flag.FlagSet, verify bool) {
|
func addTrustedFlags(fs *flag.FlagSet, verify bool) {
|
||||||
var trusted bool
|
var trusted bool
|
||||||
|
|
@ -238,11 +244,11 @@ func (cli *DockerCli) trustedReference(ref reference.NamedTagged) (reference.Can
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := notaryRepo.GetTargetByName(ref.Tag())
|
t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r, err := convertTarget(*t)
|
r, err := convertTarget(t.Target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
||||||
|
|
@ -264,17 +270,27 @@ func (cli *DockerCli) tagTrusted(trustedRef reference.Canonical, ref reference.N
|
||||||
return cli.client.ImageTag(options)
|
return cli.client.ImageTag(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func notaryError(err error) error {
|
func notaryError(repoName string, err error) error {
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case *json.SyntaxError:
|
case *json.SyntaxError:
|
||||||
logrus.Debugf("Notary syntax error: %s", err)
|
logrus.Debugf("Notary syntax error: %s", err)
|
||||||
return errors.New("no trust data available for remote repository")
|
return fmt.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName)
|
||||||
case client.ErrExpired:
|
case client.ErrExpired:
|
||||||
return fmt.Errorf("remote repository out-of-date: %v", err)
|
return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err)
|
||||||
case trustmanager.ErrKeyNotFound:
|
case trustmanager.ErrKeyNotFound:
|
||||||
return fmt.Errorf("signing keys not found: %v", err)
|
return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err)
|
||||||
case *net.OpError:
|
case *net.OpError:
|
||||||
return fmt.Errorf("error contacting notary server: %v", err)
|
return fmt.Errorf("Error: error contacting notary server: %v", err)
|
||||||
|
case store.ErrMetaNotFound:
|
||||||
|
return fmt.Errorf("Error: trust data missing for remote repository %s: %v", repoName, err)
|
||||||
|
case signed.ErrInvalidKeyType:
|
||||||
|
return fmt.Errorf("Error: trust data mismatch for remote repository %s, could be malicious behavior: %v", repoName, err)
|
||||||
|
case signed.ErrNoKeys:
|
||||||
|
return fmt.Errorf("Error: could not find signing keys for remote repository %s: %v", repoName, err)
|
||||||
|
case signed.ErrLowVersion:
|
||||||
|
return fmt.Errorf("Error: trust data version is lower than expected for remote repository %s, could be malicious behavior: %v", repoName, err)
|
||||||
|
case signed.ErrInsufficientSignatures:
|
||||||
|
return fmt.Errorf("Error: trust data has insufficient signatures for remote repository %s, could be malicious behavior: %v", repoName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|
@ -291,12 +307,12 @@ func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registr
|
||||||
|
|
||||||
if ref.String() == "" {
|
if ref.String() == "" {
|
||||||
// List all targets
|
// List all targets
|
||||||
targets, err := notaryRepo.ListTargets()
|
targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return notaryError(err)
|
return notaryError(repoInfo.FullName(), err)
|
||||||
}
|
}
|
||||||
for _, tgt := range targets {
|
for _, tgt := range targets {
|
||||||
t, err := convertTarget(*tgt)
|
t, err := convertTarget(tgt.Target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.Name())
|
fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.Name())
|
||||||
continue
|
continue
|
||||||
|
|
@ -304,11 +320,11 @@ func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registr
|
||||||
refs = append(refs, t)
|
refs = append(refs, t)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t, err := notaryRepo.GetTargetByName(ref.String())
|
t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return notaryError(err)
|
return notaryError(repoInfo.FullName(), err)
|
||||||
}
|
}
|
||||||
r, err := convertTarget(*t)
|
r, err := convertTarget(t.Target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
||||||
|
|
@ -413,7 +429,7 @@ func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string,
|
||||||
|
|
||||||
repo, err := cli.getNotaryRepository(repoInfo, authConfig)
|
repo, err := cli.getNotaryRepository(repoInfo, authConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(cli.out, "Error establishing connection to notary repository: %s\n", err)
|
fmt.Fprintf(cli.out, "Error establishing connection to notary repository, has a notary server been setup and pointed to by the DOCKER_CONTENT_TRUST_SERVER environment variable?: %s\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,14 +445,14 @@ func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string,
|
||||||
},
|
},
|
||||||
Length: int64(target.size),
|
Length: int64(target.size),
|
||||||
}
|
}
|
||||||
if err := repo.AddTarget(t); err != nil {
|
if err := repo.AddTarget(t, releasesRole); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = repo.Publish()
|
err = repo.Publish()
|
||||||
if _, ok := err.(*client.ErrRepoNotInitialized); !ok {
|
if _, ok := err.(*client.ErrRepoNotInitialized); !ok {
|
||||||
return notaryError(err)
|
return notaryError(repoInfo.FullName(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
keys := repo.CryptoService.ListKeys(data.CanonicalRootRole)
|
keys := repo.CryptoService.ListKeys(data.CanonicalRootRole)
|
||||||
|
|
@ -455,9 +471,9 @@ func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := repo.Initialize(rootKeyID); err != nil {
|
if err := repo.Initialize(rootKeyID); err != nil {
|
||||||
return notaryError(err)
|
return notaryError(repoInfo.FullName(), err)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.FullName())
|
fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.FullName())
|
||||||
|
|
||||||
return notaryError(repo.Publish())
|
return notaryError(repoInfo.FullName(), repo.Publish())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,9 @@ clone git github.com/boltdb/bolt v1.1.0
|
||||||
clone git github.com/docker/distribution 568bf038af6d65b376165d02886b1c7fcaef1f61
|
clone git github.com/docker/distribution 568bf038af6d65b376165d02886b1c7fcaef1f61
|
||||||
clone git github.com/vbatts/tar-split v0.9.11
|
clone git github.com/vbatts/tar-split v0.9.11
|
||||||
|
|
||||||
clone git github.com/docker/notary 45de2828b5e0083bfb4e9a5a781eddb05e2ef9d0
|
# get desired notary commit, might also need to be updated in Dockerfile
|
||||||
|
clone git github.com/docker/notary 30c488b3b4c62fdbc2c1eae7cf3b62ca73f95fad
|
||||||
|
|
||||||
clone git google.golang.org/grpc 174192fc93efcb188fc8f46ca447f0da606b6885 https://github.com/grpc/grpc-go.git
|
clone git google.golang.org/grpc 174192fc93efcb188fc8f46ca447f0da606b6885 https://github.com/grpc/grpc-go.git
|
||||||
clone git github.com/miekg/pkcs11 80f102b5cac759de406949c47f0928b99bd64cdf
|
clone git github.com/miekg/pkcs11 80f102b5cac759de406949c47f0928b99bd64cdf
|
||||||
clone git github.com/jfrazelle/go v1.5.1-1
|
clone git github.com/jfrazelle/go v1.5.1-1
|
||||||
|
|
|
||||||
|
|
@ -5805,7 +5805,7 @@ func (s *DockerTrustSuite) TestTrustedBuildUntrustedTag(c *check.C) {
|
||||||
c.Fatalf("Expected error on trusted build with untrusted tag: %s\n%s", err, out)
|
c.Fatalf("Expected error on trusted build with untrusted tag: %s\n%s", err, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(out, fmt.Sprintf("no trust data available")) {
|
if !strings.Contains(out, fmt.Sprintf("trust data unavailable")) {
|
||||||
c.Fatalf("Unexpected output on trusted build with untrusted tag:\n%s", out)
|
c.Fatalf("Unexpected output on trusted build with untrusted tag:\n%s", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -312,7 +312,7 @@ func (s *DockerTrustSuite) TestUntrustedCreate(c *check.C) {
|
||||||
s.trustedCmd(createCmd)
|
s.trustedCmd(createCmd)
|
||||||
out, _, err := runCommandWithOutput(createCmd)
|
out, _, err := runCommandWithOutput(createCmd)
|
||||||
c.Assert(err, check.Not(check.IsNil))
|
c.Assert(err, check.Not(check.IsNil))
|
||||||
c.Assert(string(out), checker.Contains, "no trust data available", check.Commentf("Missing expected output on trusted create:\n%s", out))
|
c.Assert(string(out), checker.Contains, "trust data unavailable", check.Commentf("Missing expected output on trusted create:\n%s", out))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ func (s *DockerTrustSuite) TestUntrustedPull(c *check.C) {
|
||||||
out, _, err := runCommandWithOutput(pullCmd)
|
out, _, err := runCommandWithOutput(pullCmd)
|
||||||
|
|
||||||
c.Assert(err, check.NotNil, check.Commentf(out))
|
c.Assert(err, check.NotNil, check.Commentf(out))
|
||||||
c.Assert(string(out), checker.Contains, "no trust data available", check.Commentf(out))
|
c.Assert(string(out), checker.Contains, "trust data unavailable", check.Commentf(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerTrustSuite) TestPullWhenCertExpired(c *check.C) {
|
func (s *DockerTrustSuite) TestPullWhenCertExpired(c *check.C) {
|
||||||
|
|
|
||||||
|
|
@ -115,10 +115,17 @@ func (s *DockerTrustSuite) TestTrustedPush(c *check.C) {
|
||||||
out, _, err := runCommandWithOutput(pushCmd)
|
out, _, err := runCommandWithOutput(pushCmd)
|
||||||
c.Assert(err, check.IsNil, check.Commentf("Error running trusted push: %s\n%s", err, out))
|
c.Assert(err, check.IsNil, check.Commentf("Error running trusted push: %s\n%s", err, out))
|
||||||
c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push"))
|
c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push"))
|
||||||
|
|
||||||
|
// Try pull after push
|
||||||
|
pullCmd := exec.Command(dockerBinary, "pull", repoName)
|
||||||
|
s.trustedCmd(pullCmd)
|
||||||
|
out, _, err = runCommandWithOutput(pullCmd)
|
||||||
|
c.Assert(err, check.IsNil, check.Commentf(out))
|
||||||
|
c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerTrustSuite) TestTrustedPushWithEnvPasswords(c *check.C) {
|
func (s *DockerTrustSuite) TestTrustedPushWithEnvPasswords(c *check.C) {
|
||||||
repoName := fmt.Sprintf("%v/dockercli/trusted:latest", privateRegistryURL)
|
repoName := fmt.Sprintf("%v/dockerclienv/trusted:latest", privateRegistryURL)
|
||||||
// tag the image and upload it to the private registry
|
// tag the image and upload it to the private registry
|
||||||
dockerCmd(c, "tag", "busybox", repoName)
|
dockerCmd(c, "tag", "busybox", repoName)
|
||||||
|
|
||||||
|
|
@ -127,6 +134,13 @@ func (s *DockerTrustSuite) TestTrustedPushWithEnvPasswords(c *check.C) {
|
||||||
out, _, err := runCommandWithOutput(pushCmd)
|
out, _, err := runCommandWithOutput(pushCmd)
|
||||||
c.Assert(err, check.IsNil, check.Commentf("Error running trusted push: %s\n%s", err, out))
|
c.Assert(err, check.IsNil, check.Commentf("Error running trusted push: %s\n%s", err, out))
|
||||||
c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push"))
|
c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push"))
|
||||||
|
|
||||||
|
// Try pull after push
|
||||||
|
pullCmd := exec.Command(dockerBinary, "pull", repoName)
|
||||||
|
s.trustedCmd(pullCmd)
|
||||||
|
out, _, err = runCommandWithOutput(pullCmd)
|
||||||
|
c.Assert(err, check.IsNil, check.Commentf(out))
|
||||||
|
c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
// This test ensures backwards compatibility with old ENV variables. Should be
|
// This test ensures backwards compatibility with old ENV variables. Should be
|
||||||
|
|
@ -168,7 +182,7 @@ func (s *DockerTrustSuite) TestTrustedPushWithoutServerAndUntrusted(c *check.C)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerTrustSuite) TestTrustedPushWithExistingTag(c *check.C) {
|
func (s *DockerTrustSuite) TestTrustedPushWithExistingTag(c *check.C) {
|
||||||
repoName := fmt.Sprintf("%v/dockercli/trusted:latest", privateRegistryURL)
|
repoName := fmt.Sprintf("%v/dockerclitag/trusted:latest", privateRegistryURL)
|
||||||
// tag the image and upload it to the private registry
|
// tag the image and upload it to the private registry
|
||||||
dockerCmd(c, "tag", "busybox", repoName)
|
dockerCmd(c, "tag", "busybox", repoName)
|
||||||
dockerCmd(c, "push", repoName)
|
dockerCmd(c, "push", repoName)
|
||||||
|
|
@ -178,6 +192,13 @@ func (s *DockerTrustSuite) TestTrustedPushWithExistingTag(c *check.C) {
|
||||||
out, _, err := runCommandWithOutput(pushCmd)
|
out, _, err := runCommandWithOutput(pushCmd)
|
||||||
c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out))
|
c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out))
|
||||||
c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push with existing tag"))
|
c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push with existing tag"))
|
||||||
|
|
||||||
|
// Try pull after push
|
||||||
|
pullCmd := exec.Command(dockerBinary, "pull", repoName)
|
||||||
|
s.trustedCmd(pullCmd)
|
||||||
|
out, _, err = runCommandWithOutput(pullCmd)
|
||||||
|
c.Assert(err, check.IsNil, check.Commentf(out))
|
||||||
|
c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerTrustSuite) TestTrustedPushWithExistingSignedTag(c *check.C) {
|
func (s *DockerTrustSuite) TestTrustedPushWithExistingSignedTag(c *check.C) {
|
||||||
|
|
|
||||||
|
|
@ -3087,7 +3087,7 @@ func (s *DockerTrustSuite) TestUntrustedRun(c *check.C) {
|
||||||
c.Fatalf("Error expected when running trusted run with:\n%s", out)
|
c.Fatalf("Error expected when running trusted run with:\n%s", out)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(string(out), "no trust data available") {
|
if !strings.Contains(string(out), "trust data unavailable") {
|
||||||
c.Fatalf("Missing expected output on trusted run:\n%s", out)
|
c.Fatalf("Missing expected output on trusted run:\n%s", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@ const notaryURL = "https://" + notaryHost
|
||||||
func newTestNotary(c *check.C) (*testNotary, error) {
|
func newTestNotary(c *check.C) (*testNotary, error) {
|
||||||
template := `{
|
template := `{
|
||||||
"server": {
|
"server": {
|
||||||
"addr": "%s",
|
"http_addr": "%s",
|
||||||
"tls_key_file": "fixtures/notary/localhost.key",
|
"tls_key_file": "%s",
|
||||||
"tls_cert_file": "fixtures/notary/localhost.cert"
|
"tls_cert_file": "%s"
|
||||||
},
|
},
|
||||||
"trust_service": {
|
"trust_service": {
|
||||||
"type": "local",
|
"type": "local",
|
||||||
|
|
@ -39,8 +39,11 @@ func newTestNotary(c *check.C) (*testNotary, error) {
|
||||||
"key_algorithm": "ed25519"
|
"key_algorithm": "ed25519"
|
||||||
},
|
},
|
||||||
"logging": {
|
"logging": {
|
||||||
"level": 5
|
"level": "debug"
|
||||||
}
|
},
|
||||||
|
"storage": {
|
||||||
|
"backend": "memory"
|
||||||
|
}
|
||||||
}`
|
}`
|
||||||
tmp, err := ioutil.TempDir("", "notary-test-")
|
tmp, err := ioutil.TempDir("", "notary-test-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -51,7 +54,12 @@ func newTestNotary(c *check.C) (*testNotary, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := fmt.Fprintf(config, template, notaryHost); err != nil {
|
|
||||||
|
workingDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(config, template, notaryHost, filepath.Join(workingDir, "fixtures/notary/localhost.key"), filepath.Join(workingDir, "fixtures/notary/localhost.cert")); err != nil {
|
||||||
os.RemoveAll(tmp)
|
os.RemoveAll(tmp)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,4 @@ RUN go install \
|
||||||
${NOTARYPKG}/cmd/notary-server
|
${NOTARYPKG}/cmd/notary-server
|
||||||
|
|
||||||
ENTRYPOINT [ "notary-server" ]
|
ENTRYPOINT [ "notary-server" ]
|
||||||
CMD [ "-config", "cmd/notary-server/config.json" ]
|
CMD [ "-config=fixtures/server-config-local.json" ]
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,4 @@ RUN go install \
|
||||||
|
|
||||||
|
|
||||||
ENTRYPOINT [ "notary-signer" ]
|
ENTRYPOINT [ "notary-signer" ]
|
||||||
CMD [ "-config=cmd/notary-signer/config.json" ]
|
CMD [ "-config=fixtures/signer-config-local.json" ]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,52 @@
|
||||||
David Lawrence <david.lawrence@docker.com> (@endophage)
|
# Notary maintainers file
|
||||||
Ying Li <ying.li@docker.com> (@cyli)
|
#
|
||||||
Nathan McCauley <nathan.mccauley@docker.com> (@NathanMcCauley)
|
# This file describes who runs the docker/notary project and how.
|
||||||
Derek McGowan <derek@docker.com> (@dmcgowan)
|
# This is a living document - if you see something out of date or missing, speak up!
|
||||||
Diogo Monica <diogo@docker.com> (@diogomonica)
|
#
|
||||||
|
# It is structured to be consumable by both humans and programs.
|
||||||
|
# To extract its contents programmatically, use any TOML-compliant parser.
|
||||||
|
#
|
||||||
|
# This file is compiled into the MAINTAINERS file in docker/opensource.
|
||||||
|
#
|
||||||
|
[Org]
|
||||||
|
[Org."Core maintainers"]
|
||||||
|
people = [
|
||||||
|
"cyli",
|
||||||
|
"diogomonica",
|
||||||
|
"dmcgowan",
|
||||||
|
"endophage",
|
||||||
|
"nathanmccauley",
|
||||||
|
]
|
||||||
|
|
||||||
|
[people]
|
||||||
|
|
||||||
|
# A reference list of all people associated with the project.
|
||||||
|
# All other sections should refer to people by their canonical key
|
||||||
|
# in the people section.
|
||||||
|
|
||||||
|
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
|
||||||
|
|
||||||
|
[people.cyli]
|
||||||
|
Name = "Ying Li"
|
||||||
|
Email = "ying.li@docker.com"
|
||||||
|
GitHub = "cyli"
|
||||||
|
|
||||||
|
[people.diogomonica]
|
||||||
|
Name = "Diogo Monica"
|
||||||
|
Email = "diogo@docker.com"
|
||||||
|
GitHub = "diogomonica"
|
||||||
|
|
||||||
|
[people.dmcgowan]
|
||||||
|
Name = "Derek McGowan"
|
||||||
|
Email = "derek@docker.com"
|
||||||
|
GitHub = "dmcgowan"
|
||||||
|
|
||||||
|
[people.endophage]
|
||||||
|
Name = "David Lawrence"
|
||||||
|
Email = "david.lawrence@docker.com"
|
||||||
|
GitHub = "endophage"
|
||||||
|
|
||||||
|
[people.nathanmccauley]
|
||||||
|
Name = "Nathan McCauley"
|
||||||
|
Email = "nathan.mccauley@docker.com"
|
||||||
|
GitHub = "nathanmccauley"
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ _space := $(empty) $(empty)
|
||||||
|
|
||||||
# go cover test variables
|
# go cover test variables
|
||||||
COVERDIR=.cover
|
COVERDIR=.cover
|
||||||
COVERPROFILE=$(COVERDIR)/cover.out
|
COVERPROFILE?=$(COVERDIR)/cover.out
|
||||||
COVERMODE=count
|
COVERMODE=count
|
||||||
PKGS = $(shell go list ./... | tr '\n' ' ')
|
PKGS = $(shell go list ./... | tr '\n' ' ')
|
||||||
|
|
||||||
|
|
@ -43,8 +43,8 @@ GO_VERSION = $(shell go version | awk '{print $$3}')
|
||||||
.DEFAULT: default
|
.DEFAULT: default
|
||||||
|
|
||||||
go_version:
|
go_version:
|
||||||
ifneq ("$(GO_VERSION)", "go1.5.1")
|
ifeq (,$(findstring go1.5.,$(GO_VERSION)))
|
||||||
$(error Requires go version 1.5.1 - found $(GO_VERSION))
|
$(error Requires go version 1.5.x - found $(GO_VERSION))
|
||||||
else
|
else
|
||||||
@echo
|
@echo
|
||||||
endif
|
endif
|
||||||
|
|
@ -73,6 +73,11 @@ ${PREFIX}/bin/notary-signer: NOTARY_VERSION $(shell find . -type f -name '*.go')
|
||||||
|
|
||||||
vet: go_version
|
vet: go_version
|
||||||
@echo "+ $@"
|
@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)"
|
||||||
|
else
|
||||||
|
@test -z "$(shell find . -iname *test*.go | grep -v _test.go | grep -v Godeps | xargs -r echo "This file should end with '_test':" | tee /dev/stderr)"
|
||||||
|
endif
|
||||||
@test -z "$$(go tool vet -printf=false . 2>&1 | grep -v Godeps/_workspace/src/ | tee /dev/stderr)"
|
@test -z "$$(go tool vet -printf=false . 2>&1 | grep -v Godeps/_workspace/src/ | tee /dev/stderr)"
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
1.0-rc1
|
0.2
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ curl example.com/install.sh | notary verify example.com/scripts v1 | sh
|
||||||
# Notary Server
|
# Notary Server
|
||||||
|
|
||||||
Notary Server manages TUF data over an HTTP API compatible with the
|
Notary Server manages TUF data over an HTTP API compatible with the
|
||||||
[notary client](../notary/).
|
[notary client](cmd/notary).
|
||||||
|
|
||||||
It may be configured to use either JWT or HTTP Basic Auth for authentication.
|
It may be configured to use either JWT or HTTP Basic Auth for authentication.
|
||||||
Currently it only supports MySQL for storage of the TUF data, we intend to
|
Currently it only supports MySQL for storage of the TUF data, we intend to
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
package keystoremanager
|
package certs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
|
@ -15,18 +13,13 @@ import (
|
||||||
"github.com/docker/notary/tuf/signed"
|
"github.com/docker/notary/tuf/signed"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyStoreManager is an abstraction around the root and non-root key stores,
|
// Manager is an abstraction around trusted root CA stores
|
||||||
// and related CA stores
|
type Manager struct {
|
||||||
type KeyStoreManager struct {
|
|
||||||
KeyStore *trustmanager.KeyFileStore
|
|
||||||
trustedCAStore trustmanager.X509Store
|
trustedCAStore trustmanager.X509Store
|
||||||
trustedCertificateStore trustmanager.X509Store
|
trustedCertificateStore trustmanager.X509Store
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const trustDir = "trusted_certificates"
|
||||||
trustDir = "trusted_certificates"
|
|
||||||
rsaRootKeySize = 4096 // Used for new root keys
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrValidationFail is returned when there is no valid trusted certificates
|
// ErrValidationFail is returned when there is no valid trusted certificates
|
||||||
// being served inside of the roots.json
|
// being served inside of the roots.json
|
||||||
|
|
@ -52,9 +45,9 @@ func (err ErrRootRotationFail) Error() string {
|
||||||
return fmt.Sprintf("could not rotate trust to a new trusted root: %s", err.Reason)
|
return fmt.Sprintf("could not rotate trust to a new trusted root: %s", err.Reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewKeyStoreManager returns an initialized KeyStoreManager, or an error
|
// NewManager returns an initialized Manager, or an error
|
||||||
// if it fails to create the KeyFileStores or load certificates
|
// if it fails to load certificates
|
||||||
func NewKeyStoreManager(baseDir string, keyStore *trustmanager.KeyFileStore) (*KeyStoreManager, error) {
|
func NewManager(baseDir string) (*Manager, error) {
|
||||||
trustPath := filepath.Join(baseDir, trustDir)
|
trustPath := filepath.Join(baseDir, trustDir)
|
||||||
|
|
||||||
// Load all CAs that aren't expired and don't use SHA1
|
// Load all CAs that aren't expired and don't use SHA1
|
||||||
|
|
@ -81,60 +74,32 @@ func NewKeyStoreManager(baseDir string, keyStore *trustmanager.KeyFileStore) (*K
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &KeyStoreManager{
|
return &Manager{
|
||||||
KeyStore: keyStore,
|
|
||||||
trustedCAStore: trustedCAStore,
|
trustedCAStore: trustedCAStore,
|
||||||
trustedCertificateStore: trustedCertificateStore,
|
trustedCertificateStore: trustedCertificateStore,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrustedCertificateStore returns the trusted certificate store being managed
|
// TrustedCertificateStore returns the trusted certificate store being managed
|
||||||
// by this KeyStoreManager
|
// by this Manager
|
||||||
func (km *KeyStoreManager) TrustedCertificateStore() trustmanager.X509Store {
|
func (m *Manager) TrustedCertificateStore() trustmanager.X509Store {
|
||||||
return km.trustedCertificateStore
|
return m.trustedCertificateStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrustedCAStore returns the CA store being managed by this KeyStoreManager
|
// TrustedCAStore returns the CA store being managed by this Manager
|
||||||
func (km *KeyStoreManager) TrustedCAStore() trustmanager.X509Store {
|
func (m *Manager) TrustedCAStore() trustmanager.X509Store {
|
||||||
return km.trustedCAStore
|
return m.trustedCAStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTrustedCert adds a cert to the trusted certificate store (not the CA
|
// AddTrustedCert adds a cert to the trusted certificate store (not the CA
|
||||||
// store)
|
// store)
|
||||||
func (km *KeyStoreManager) AddTrustedCert(cert *x509.Certificate) {
|
func (m *Manager) AddTrustedCert(cert *x509.Certificate) {
|
||||||
km.trustedCertificateStore.AddCert(cert)
|
m.trustedCertificateStore.AddCert(cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTrustedCACert adds a cert to the trusted CA certificate store
|
// AddTrustedCACert adds a cert to the trusted CA certificate store
|
||||||
func (km *KeyStoreManager) AddTrustedCACert(cert *x509.Certificate) {
|
func (m *Manager) AddTrustedCACert(cert *x509.Certificate) {
|
||||||
km.trustedCAStore.AddCert(cert)
|
m.trustedCAStore.AddCert(cert)
|
||||||
}
|
|
||||||
|
|
||||||
// GenRootKey generates a new root key
|
|
||||||
func (km *KeyStoreManager) GenRootKey(algorithm string) (string, error) {
|
|
||||||
var err error
|
|
||||||
var privKey data.PrivateKey
|
|
||||||
|
|
||||||
// We don't want external API callers to rely on internal TUF data types, so
|
|
||||||
// the API here should continue to receive a string algorithm, and ensure
|
|
||||||
// that it is downcased
|
|
||||||
switch strings.ToLower(algorithm) {
|
|
||||||
case data.RSAKey:
|
|
||||||
privKey, err = trustmanager.GenerateRSAKey(rand.Reader, rsaRootKeySize)
|
|
||||||
case data.ECDSAKey:
|
|
||||||
privKey, err = trustmanager.GenerateECDSAKey(rand.Reader)
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("only RSA or ECDSA keys are currently supported. Found: %s", algorithm)
|
|
||||||
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to generate private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changing the root
|
|
||||||
km.KeyStore.AddKey(privKey.ID(), "root", privKey)
|
|
||||||
|
|
||||||
return privKey.ID(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -164,7 +129,7 @@ we are using the current public PKI to validate the first download of the certif
|
||||||
adding an extra layer of security over the normal (SSH style) trust model.
|
adding an extra layer of security over the normal (SSH style) trust model.
|
||||||
We shall call this: TOFUS.
|
We shall call this: TOFUS.
|
||||||
*/
|
*/
|
||||||
func (km *KeyStoreManager) ValidateRoot(root *data.Signed, gun string) error {
|
func (m *Manager) ValidateRoot(root *data.Signed, gun string) error {
|
||||||
logrus.Debugf("entered ValidateRoot with dns: %s", gun)
|
logrus.Debugf("entered ValidateRoot with dns: %s", gun)
|
||||||
signedRoot, err := data.RootFromSigned(root)
|
signedRoot, err := data.RootFromSigned(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -179,7 +144,7 @@ func (km *KeyStoreManager) ValidateRoot(root *data.Signed, gun string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve all the trusted certificates that match this gun
|
// Retrieve all the trusted certificates that match this gun
|
||||||
certsForCN, err := km.trustedCertificateStore.GetCertificatesByCN(gun)
|
certsForCN, err := m.trustedCertificateStore.GetCertificatesByCN(gun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If the error that we get back is different than ErrNoCertificatesFound
|
// If the error that we get back is different than ErrNoCertificatesFound
|
||||||
// we couldn't check if there are any certificates with this CN already
|
// we couldn't check if there are any certificates with this CN already
|
||||||
|
|
@ -218,7 +183,7 @@ func (km *KeyStoreManager) ValidateRoot(root *data.Signed, gun string) error {
|
||||||
// Do root certificate rotation: we trust only the certs present in the new root
|
// Do root certificate rotation: we trust only the certs present in the new root
|
||||||
// First we add all the new certificates (even if they already exist)
|
// First we add all the new certificates (even if they already exist)
|
||||||
for _, cert := range allValidCerts {
|
for _, cert := range allValidCerts {
|
||||||
err := km.trustedCertificateStore.AddCert(cert)
|
err := m.trustedCertificateStore.AddCert(cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If the error is already exists we don't fail the rotation
|
// If the error is already exists we don't fail the rotation
|
||||||
if _, ok := err.(*trustmanager.ErrCertExists); ok {
|
if _, ok := err.(*trustmanager.ErrCertExists); ok {
|
||||||
|
|
@ -232,7 +197,7 @@ func (km *KeyStoreManager) ValidateRoot(root *data.Signed, gun string) error {
|
||||||
// Now we delete old certificates that aren't present in the new root
|
// Now we delete old certificates that aren't present in the new root
|
||||||
for certID, cert := range certsToRemove(certsForCN, allValidCerts) {
|
for certID, cert := range certsToRemove(certsForCN, allValidCerts) {
|
||||||
logrus.Debugf("removing certificate with certID: %s", certID)
|
logrus.Debugf("removing certificate with certID: %s", certID)
|
||||||
err = km.trustedCertificateStore.RemoveCert(cert)
|
err = m.trustedCertificateStore.RemoveCert(cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Debugf("failed to remove trusted certificate with keyID: %s, %v", certID, err)
|
logrus.Debugf("failed to remove trusted certificate with keyID: %s, %v", certID, err)
|
||||||
return &ErrRootRotationFail{Reason: "failed to rotate root keys"}
|
return &ErrRootRotationFail{Reason: "failed to rotate root keys"}
|
||||||
|
|
@ -16,6 +16,8 @@ machine:
|
||||||
BASE_STABLE: ../../../$HOME/.gvm/pkgsets/stable/global/$BASE_DIR
|
BASE_STABLE: ../../../$HOME/.gvm/pkgsets/stable/global/$BASE_DIR
|
||||||
# Workaround Circle parsing dumb bugs and/or YAML wonkyness
|
# Workaround Circle parsing dumb bugs and/or YAML wonkyness
|
||||||
CIRCLE_PAIN: "mode: set"
|
CIRCLE_PAIN: "mode: set"
|
||||||
|
# Put the coverage profile somewhere codecov's script can find it
|
||||||
|
COVERPROFILE: coverage.out
|
||||||
|
|
||||||
hosts:
|
hosts:
|
||||||
# Not used yet
|
# Not used yet
|
||||||
|
|
@ -72,8 +74,10 @@ test:
|
||||||
post:
|
post:
|
||||||
- gvm use stable && make covmerge:
|
- gvm use stable && make covmerge:
|
||||||
timeout: 600
|
timeout: 600
|
||||||
|
parallel: true
|
||||||
pwd: $BASE_STABLE
|
pwd: $BASE_STABLE
|
||||||
|
|
||||||
# Report to codecov.io
|
# Report to codecov.io
|
||||||
# - bash <(curl -s https://codecov.io/bash):
|
- bash <(curl -s https://codecov.io/bash):
|
||||||
# pwd: $BASE_STABLE
|
parallel: true
|
||||||
|
pwd: $BASE_STABLE
|
||||||
|
|
|
||||||
|
|
@ -77,3 +77,26 @@ func (c TufChange) Path() string {
|
||||||
func (c TufChange) Content() []byte {
|
func (c TufChange) Content() []byte {
|
||||||
return c.Data
|
return c.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TufDelegation represents a modification to a target delegation
|
||||||
|
// this includes creating a delegations. This format is used to avoid
|
||||||
|
// unexpected race conditions between humans modifying the same delegation
|
||||||
|
type TufDelegation struct {
|
||||||
|
NewName string `json:"new_name,omitempty"`
|
||||||
|
NewThreshold int `json:"threshold, omitempty"`
|
||||||
|
AddKeys data.KeyList `json:"add_keys, omitempty"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToNewRole creates a fresh role object from the TufDelegation data
|
||||||
|
func (td TufDelegation) ToNewRole(scope string) (*data.Role, error) {
|
||||||
|
name := scope
|
||||||
|
if td.NewName != "" {
|
||||||
|
name = td.NewName
|
||||||
|
}
|
||||||
|
return data.NewRole(name, td.NewThreshold, td.AddKeys.IDs(), td.AddPaths, td.AddPathHashPrefixes)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/notary/certs"
|
||||||
"github.com/docker/notary/client/changelist"
|
"github.com/docker/notary/client/changelist"
|
||||||
"github.com/docker/notary/cryptoservice"
|
"github.com/docker/notary/cryptoservice"
|
||||||
"github.com/docker/notary/keystoremanager"
|
|
||||||
"github.com/docker/notary/trustmanager"
|
"github.com/docker/notary/trustmanager"
|
||||||
"github.com/docker/notary/tuf"
|
"github.com/docker/notary/tuf"
|
||||||
tufclient "github.com/docker/notary/tuf/client"
|
tufclient "github.com/docker/notary/tuf/client"
|
||||||
|
|
@ -52,6 +54,17 @@ type ErrExpired struct {
|
||||||
signed.ErrExpired
|
signed.ErrExpired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrInvalidRemoteRole is returned when the server is requested to manage
|
||||||
|
// an unsupported key type
|
||||||
|
type ErrInvalidRemoteRole struct {
|
||||||
|
Role string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrInvalidRemoteRole) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"notary does not support the server managing the %s key", e.Role)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tufDir = "tuf"
|
tufDir = "tuf"
|
||||||
)
|
)
|
||||||
|
|
@ -63,23 +76,67 @@ var ErrRepositoryNotExist = errors.New("repository does not exist")
|
||||||
// NotaryRepository stores all the information needed to operate on a notary
|
// NotaryRepository stores all the information needed to operate on a notary
|
||||||
// repository.
|
// repository.
|
||||||
type NotaryRepository struct {
|
type NotaryRepository struct {
|
||||||
baseDir string
|
baseDir string
|
||||||
gun string
|
gun string
|
||||||
baseURL string
|
baseURL string
|
||||||
tufRepoPath string
|
tufRepoPath string
|
||||||
fileStore store.MetadataStore
|
fileStore store.MetadataStore
|
||||||
CryptoService signed.CryptoService
|
CryptoService signed.CryptoService
|
||||||
tufRepo *tuf.Repo
|
tufRepo *tuf.Repo
|
||||||
roundTrip http.RoundTripper
|
roundTrip http.RoundTripper
|
||||||
KeyStoreManager *keystoremanager.KeyStoreManager
|
CertManager *certs.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// repositoryFromKeystores is a helper function for NewNotaryRepository that
|
||||||
|
// takes some basic NotaryRepository parameters as well as keystores (in order
|
||||||
|
// of usage preference), and returns a NotaryRepository.
|
||||||
|
func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper,
|
||||||
|
keyStores []trustmanager.KeyStore) (*NotaryRepository, error) {
|
||||||
|
|
||||||
|
certManager, err := certs.NewManager(baseDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoService := cryptoservice.NewCryptoService(gun, keyStores...)
|
||||||
|
|
||||||
|
nRepo := &NotaryRepository{
|
||||||
|
gun: gun,
|
||||||
|
baseDir: baseDir,
|
||||||
|
baseURL: baseURL,
|
||||||
|
tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)),
|
||||||
|
CryptoService: cryptoService,
|
||||||
|
roundTrip: rt,
|
||||||
|
CertManager: certManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStore, err := store.NewFilesystemStore(
|
||||||
|
nRepo.tufRepoPath,
|
||||||
|
"metadata",
|
||||||
|
"json",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nRepo.fileStore = fileStore
|
||||||
|
|
||||||
|
return nRepo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Target represents a simplified version of the data TUF operates on, so external
|
// Target represents a simplified version of the data TUF operates on, so external
|
||||||
// applications don't have to depend on tuf data types.
|
// applications don't have to depend on tuf data types.
|
||||||
type Target struct {
|
type Target struct {
|
||||||
Name string
|
Name string // the name of the target
|
||||||
Hashes data.Hashes
|
Hashes data.Hashes // the hash of the target
|
||||||
Length int64
|
Length int64 // the size in bytes of the target
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetWithRole represents a Target that exists in a particular role - this is
|
||||||
|
// produced by ListTargets and GetTargetByName
|
||||||
|
type TargetWithRole struct {
|
||||||
|
Target
|
||||||
|
Role string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTarget is a helper method that returns a Target
|
// NewTarget is a helper method that returns a Target
|
||||||
|
|
@ -99,18 +156,48 @@ func NewTarget(targetName string, targetPath string) (*Target, error) {
|
||||||
|
|
||||||
// Initialize creates a new repository by using rootKey as the root Key for the
|
// Initialize creates a new repository by using rootKey as the root Key for the
|
||||||
// TUF repository.
|
// TUF repository.
|
||||||
func (r *NotaryRepository) Initialize(rootKeyID string) error {
|
func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...string) error {
|
||||||
privKey, _, err := r.CryptoService.GetPrivateKey(rootKeyID)
|
privKey, _, err := r.CryptoService.GetPrivateKey(rootKeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rootCert, err := cryptoservice.GenerateCertificate(privKey, r.gun)
|
// currently we only support server managing timestamps and snapshots, and
|
||||||
|
// nothing else - timestamps are always managed by the server, and implicit
|
||||||
|
// (do not have to be passed in as part of `serverManagedRoles`, so that
|
||||||
|
// the API of Initialize doens't change).
|
||||||
|
var serverManagesSnapshot bool
|
||||||
|
locallyManagedKeys := []string{
|
||||||
|
data.CanonicalTargetsRole,
|
||||||
|
data.CanonicalSnapshotRole,
|
||||||
|
// root is also locally managed, but that should have been created
|
||||||
|
// already
|
||||||
|
}
|
||||||
|
remotelyManagedKeys := []string{data.CanonicalTimestampRole}
|
||||||
|
for _, role := range serverManagedRoles {
|
||||||
|
switch role {
|
||||||
|
case data.CanonicalTimestampRole:
|
||||||
|
continue // timestamp is already in the right place
|
||||||
|
case data.CanonicalSnapshotRole:
|
||||||
|
// because we put Snapshot last
|
||||||
|
locallyManagedKeys = []string{data.CanonicalTargetsRole}
|
||||||
|
remotelyManagedKeys = append(
|
||||||
|
remotelyManagedKeys, data.CanonicalSnapshotRole)
|
||||||
|
serverManagesSnapshot = true
|
||||||
|
default:
|
||||||
|
return ErrInvalidRemoteRole{Role: role}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hard-coded policy: the generated certificate expires in 10 years.
|
||||||
|
startTime := time.Now()
|
||||||
|
rootCert, err := cryptoservice.GenerateCertificate(
|
||||||
|
privKey, r.gun, startTime, startTime.AddDate(10, 0, 0))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
r.KeyStoreManager.AddTrustedCert(rootCert)
|
r.CertManager.AddTrustedCert(rootCert)
|
||||||
|
|
||||||
// The root key gets stored in the TUF metadata X509 encoded, linking
|
// The root key gets stored in the TUF metadata X509 encoded, linking
|
||||||
// the tuf root.json to our X509 PKI.
|
// the tuf root.json to our X509 PKI.
|
||||||
|
|
@ -127,58 +214,45 @@ func (r *NotaryRepository) Initialize(rootKeyID string) error {
|
||||||
return fmt.Errorf("invalid format for root key: %s", privKey.Algorithm())
|
return fmt.Errorf("invalid format for root key: %s", privKey.Algorithm())
|
||||||
}
|
}
|
||||||
|
|
||||||
// All the timestamp keys are generated by the remote server.
|
|
||||||
remote, err := getRemoteStore(r.baseURL, r.gun, r.roundTrip)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rawTSKey, err := remote.GetKey("timestamp")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
timestampKey, err := data.UnmarshalPublicKey(rawTSKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("got remote %s timestamp key with keyID: %s", timestampKey.Algorithm(), timestampKey.ID())
|
|
||||||
|
|
||||||
// This is currently hardcoding the targets and snapshots keys to ECDSA
|
|
||||||
// Targets and snapshot keys are always generated locally.
|
|
||||||
targetsKey, err := r.CryptoService.Create("targets", data.ECDSAKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
snapshotKey, err := r.CryptoService.Create("snapshot", data.ECDSAKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
kdb := keys.NewDB()
|
kdb := keys.NewDB()
|
||||||
|
err = addKeyForRole(kdb, data.CanonicalRootRole, rootKey)
|
||||||
kdb.AddKey(rootKey)
|
|
||||||
kdb.AddKey(targetsKey)
|
|
||||||
kdb.AddKey(snapshotKey)
|
|
||||||
kdb.AddKey(timestampKey)
|
|
||||||
|
|
||||||
err = initRoles(kdb, rootKey, targetsKey, snapshotKey, timestampKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we want to create all the local keys first so we don't have to
|
||||||
|
// make unnecessary network calls
|
||||||
|
for _, role := range locallyManagedKeys {
|
||||||
|
// This is currently hardcoding the keys to ECDSA.
|
||||||
|
key, err := r.CryptoService.Create(role, data.ECDSAKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addKeyForRole(kdb, role, key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, role := range remotelyManagedKeys {
|
||||||
|
// This key is generated by the remote server.
|
||||||
|
key, err := getRemoteKey(r.baseURL, r.gun, role, r.roundTrip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r.tufRepo = tuf.NewRepo(kdb, r.CryptoService)
|
r.tufRepo = tuf.NewRepo(kdb, r.CryptoService)
|
||||||
|
|
||||||
err = r.tufRepo.InitRoot(false)
|
err = r.tufRepo.InitRoot(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Debug("Error on InitRoot: ", err.Error())
|
logrus.Debug("Error on InitRoot: ", err.Error())
|
||||||
switch err.(type) {
|
return err
|
||||||
case signed.ErrInsufficientSignatures, trustmanager.ErrPasswordInvalid:
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
err = r.tufRepo.InitTargets()
|
_, err = r.tufRepo.InitTargets(data.CanonicalTargetsRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Debug("Error on InitTargets: ", err.Error())
|
logrus.Debug("Error on InitTargets: ", err.Error())
|
||||||
return err
|
return err
|
||||||
|
|
@ -189,11 +263,120 @@ func (r *NotaryRepository) Initialize(rootKeyID string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.saveMetadata()
|
return r.saveMetadata(serverManagesSnapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTarget adds a new target to the repository, forcing a timestamps check from TUF
|
// adds a TUF Change template to the given roles
|
||||||
func (r *NotaryRepository) AddTarget(target *Target) error {
|
func addChange(cl *changelist.FileChangelist, c changelist.Change, roles ...string) error {
|
||||||
|
|
||||||
|
if len(roles) == 0 {
|
||||||
|
roles = []string{data.CanonicalTargetsRole}
|
||||||
|
}
|
||||||
|
|
||||||
|
var changes []changelist.Change
|
||||||
|
for _, role := range roles {
|
||||||
|
role = strings.ToLower(role)
|
||||||
|
|
||||||
|
// Ensure we can only add targets to the CanonicalTargetsRole,
|
||||||
|
// or a Delegation role (which is <CanonicalTargetsRole>/something else)
|
||||||
|
if role != data.CanonicalTargetsRole && !data.IsDelegation(role) {
|
||||||
|
return data.ErrInvalidRole{
|
||||||
|
Role: role,
|
||||||
|
Reason: "cannot add targets to this role",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changes = append(changes, changelist.NewTufChange(
|
||||||
|
c.Action(),
|
||||||
|
role,
|
||||||
|
c.Type(),
|
||||||
|
c.Path(),
|
||||||
|
c.Content(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range changes {
|
||||||
|
if err := cl.Add(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) 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 := changelist.NewTufChange(
|
||||||
|
changelist.ActionDelete,
|
||||||
|
name,
|
||||||
|
changelist.TypeTargetsDelegation,
|
||||||
|
"", // no path
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 appied at publish time.
|
||||||
|
// If roles are unspecified, the default role is "targets".
|
||||||
|
func (r *NotaryRepository) AddTarget(target *Target, roles ...string) error {
|
||||||
|
|
||||||
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -207,32 +390,35 @@ func (r *NotaryRepository) AddTarget(target *Target) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c := changelist.NewTufChange(changelist.ActionCreate, changelist.ScopeTargets, "target", target.Name, metaJSON)
|
template := changelist.NewTufChange(
|
||||||
err = cl.Add(c)
|
changelist.ActionCreate, "", changelist.TypeTargetsTarget,
|
||||||
if err != nil {
|
target.Name, metaJSON)
|
||||||
return err
|
return addChange(cl, template, roles...)
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveTarget creates a new changelist entry to remove a target from the repository
|
// RemoveTarget creates new changelist entries to remove a target from the given
|
||||||
// when the changelist gets applied at publish time
|
// roles in the repository when the changelist gets applied at publish time.
|
||||||
func (r *NotaryRepository) RemoveTarget(targetName string) error {
|
// If roles are unspecified, the default role is "target".
|
||||||
|
func (r *NotaryRepository) RemoveTarget(targetName string, roles ...string) error {
|
||||||
|
|
||||||
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logrus.Debugf("Removing target \"%s\"", targetName)
|
logrus.Debugf("Removing target \"%s\"", targetName)
|
||||||
c := changelist.NewTufChange(changelist.ActionDelete, changelist.ScopeTargets, "target", targetName, nil)
|
template := changelist.NewTufChange(changelist.ActionDelete, "",
|
||||||
err = cl.Add(c)
|
changelist.TypeTargetsTarget, targetName, nil)
|
||||||
if err != nil {
|
return addChange(cl, template, roles...)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListTargets lists all targets for the current repository
|
// ListTargets lists all targets for the current repository. The list of
|
||||||
func (r *NotaryRepository) ListTargets() ([]*Target, error) {
|
// roles should be passed in order from highest to lowest priority.
|
||||||
|
// IMPORTANT: if you pass a set of roles such as [ "targets/a", "targets/x"
|
||||||
|
// "targets/a/b" ], even though "targets/a/b" is part of the "targets/a" subtree
|
||||||
|
// its entries will be strictly shadowed by those in other parts of the "targets/a"
|
||||||
|
// subtree and also the "targets/x" subtree, as we will defer parsing it until
|
||||||
|
// we explicitly reach it in our iteration of the provided list of roles.
|
||||||
|
func (r *NotaryRepository) ListTargets(roles ...string) ([]*TargetWithRole, error) {
|
||||||
c, err := r.bootstrapClient()
|
c, err := r.bootstrapClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -246,17 +432,61 @@ func (r *NotaryRepository) ListTargets() ([]*Target, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetList []*Target
|
if len(roles) == 0 {
|
||||||
for name, meta := range r.tufRepo.Targets["targets"].Signed.Targets {
|
roles = []string{data.CanonicalTargetsRole}
|
||||||
target := &Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}
|
}
|
||||||
targetList = append(targetList, target)
|
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
|
||||||
|
// descendent delegations that appear in roles.
|
||||||
|
r.listSubtree(targets, role, roles...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetList []*TargetWithRole
|
||||||
|
for _, v := range targets {
|
||||||
|
targetList = append(targetList, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return targetList, nil
|
return targetList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTargetByName returns a target given a name
|
func (r *NotaryRepository) listSubtree(targets map[string]*TargetWithRole, role string, exclude ...string) {
|
||||||
func (r *NotaryRepository) GetTargetByName(name string) (*Target, error) {
|
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.
|
||||||
|
// If roles are passed, they should be passed in descending priority and
|
||||||
|
// the target entry found in the subtree of the highest priority 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.bootstrapClient()
|
c, err := r.bootstrapClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -270,14 +500,18 @@ func (r *NotaryRepository) GetTargetByName(name string) (*Target, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
meta, err := c.TargetMeta(name)
|
if len(roles) == 0 {
|
||||||
if meta == nil {
|
roles = append(roles, data.CanonicalTargetsRole)
|
||||||
return nil, fmt.Errorf("No trust data for %s", name)
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("No trust data for %s", name)
|
||||||
|
|
||||||
return &Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChangelist returns the list of the repository's unpublished changes
|
// GetChangelist returns the list of the repository's unpublished changes
|
||||||
|
|
@ -294,30 +528,30 @@ func (r *NotaryRepository) GetChangelist() (changelist.Changelist, error) {
|
||||||
// Publish pushes the local changes in signed material to the remote notary-server
|
// Publish pushes the local changes in signed material to the remote notary-server
|
||||||
// Conceptually it performs an operation similar to a `git rebase`
|
// Conceptually it performs an operation similar to a `git rebase`
|
||||||
func (r *NotaryRepository) Publish() error {
|
func (r *NotaryRepository) Publish() error {
|
||||||
var updateRoot bool
|
var initialPublish bool
|
||||||
var root *data.Signed
|
|
||||||
// attempt to initialize the repo from the remote store
|
// attempt to initialize the repo from the remote store
|
||||||
c, err := r.bootstrapClient()
|
c, err := r.bootstrapClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(store.ErrMetaNotFound); ok {
|
if _, ok := err.(store.ErrMetaNotFound); ok {
|
||||||
// if the remote store return a 404 (translated into ErrMetaNotFound),
|
// if the remote store return a 404 (translated into ErrMetaNotFound),
|
||||||
// the repo hasn't been initialized yet. Attempt to load it from disk.
|
// there is no trust data for yet. Attempt to load it from disk.
|
||||||
err := r.bootstrapRepo()
|
err := r.bootstrapRepo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Repo hasn't been initialized, It must be initialized before
|
// There are lots of reasons there might be an error, such as
|
||||||
// it can be published. Return an error and let caller determine
|
// corrupt metadata. We need better errors from bootstrapRepo.
|
||||||
// what it wants to do.
|
logrus.Debugf("Unable to load repository from local files: %s",
|
||||||
logrus.Debug(err.Error())
|
err.Error())
|
||||||
logrus.Debug("Repository not initialized during Publish")
|
if _, ok := err.(store.ErrMetaNotFound); ok {
|
||||||
return &ErrRepoNotInitialized{}
|
return &ErrRepoNotInitialized{}
|
||||||
}
|
}
|
||||||
// We had local data but the server doesn't know about the repo yet,
|
|
||||||
// ensure we will push the initial root file
|
|
||||||
root, err = r.tufRepo.Root.ToSigned()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updateRoot = true
|
// We had local data but the server doesn't know about the repo yet,
|
||||||
|
// ensure we will push the initial root and targets file. Either or
|
||||||
|
// both of the root and targets may not be marked as Dirty, since
|
||||||
|
// there may not be any changes that update them, so use a
|
||||||
|
// different boolean.
|
||||||
|
initialPublish = true
|
||||||
} else {
|
} else {
|
||||||
// The remote store returned an error other than 404. We're
|
// The remote store returned an error other than 404. We're
|
||||||
// unable to determine if the repo has been initialized or not.
|
// unable to determine if the repo has been initialized or not.
|
||||||
|
|
@ -326,8 +560,8 @@ func (r *NotaryRepository) Publish() error {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If we were successfully able to bootstrap the client (which only pulls
|
// If we were successfully able to bootstrap the client (which only pulls
|
||||||
// root.json), update it the rest of the tuf metadata in preparation for
|
// root.json), update it with the rest of the tuf metadata in
|
||||||
// applying the changelist.
|
// preparation for applying the changelist.
|
||||||
err = c.Update()
|
err = c.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err, ok := err.(signed.ErrExpired); ok {
|
if err, ok := err.(signed.ErrExpired); ok {
|
||||||
|
|
@ -347,24 +581,53 @@ func (r *NotaryRepository) Publish() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// these are the tuf files we will need to update, serialized as JSON before
|
||||||
|
// we send anything to remote
|
||||||
|
updatedFiles := make(map[string][]byte)
|
||||||
|
|
||||||
// check if our root file is nearing expiry. Resign if it is.
|
// check if our root file is nearing expiry. Resign if it is.
|
||||||
if nearExpiry(r.tufRepo.Root) || r.tufRepo.Root.Dirty {
|
if nearExpiry(r.tufRepo.Root) || r.tufRepo.Root.Dirty || initialPublish {
|
||||||
|
rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
root, err = r.tufRepo.SignRoot(data.DefaultExpires("root"))
|
updatedFiles[data.CanonicalRootRole] = rootJSON
|
||||||
if err != nil {
|
}
|
||||||
|
|
||||||
|
// iterate through all the targets files - if they are dirty, sign and update
|
||||||
|
for roleName, roleObj := range r.tufRepo.Targets {
|
||||||
|
if roleObj.Dirty || (roleName == data.CanonicalTargetsRole && initialPublish) {
|
||||||
|
targetsJSON, err := serializeCanonicalRole(r.tufRepo, roleName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updatedFiles[roleName] = targetsJSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we initialized the repo while designating the server as the snapshot
|
||||||
|
// signer, then there won't be a snapshots file. However, we might now
|
||||||
|
// have a local key (if there was a rotation), so initialize one.
|
||||||
|
if r.tufRepo.Snapshot == nil {
|
||||||
|
if err := r.tufRepo.InitSnapshot(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updateRoot = true
|
|
||||||
}
|
}
|
||||||
// we will always resign targets and snapshots
|
|
||||||
targets, err := r.tufRepo.SignTargets("targets", data.DefaultExpires("targets"))
|
snapshotJSON, err := serializeCanonicalRole(
|
||||||
if err != nil {
|
r.tufRepo, data.CanonicalSnapshotRole)
|
||||||
return err
|
|
||||||
}
|
if err == nil {
|
||||||
snapshot, err := r.tufRepo.SignSnapshot(data.DefaultExpires("snapshot"))
|
// Only update the snapshot if we've sucessfully signed it.
|
||||||
if err != nil {
|
updatedFiles[data.CanonicalSnapshotRole] = snapshotJSON
|
||||||
|
} else if _, ok := err.(signed.ErrNoKeys); ok {
|
||||||
|
// If signing fails due to us not having the snapshot key, then
|
||||||
|
// assume the server is going to sign, and do not include any snapshot
|
||||||
|
// data.
|
||||||
|
logrus.Debugf("Client does not have the key to sign snapshot. " +
|
||||||
|
"Assuming that server should sign the snapshot.")
|
||||||
|
} else {
|
||||||
|
logrus.Debugf("Client was unable to sign the snapshot: %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -373,27 +636,7 @@ func (r *NotaryRepository) Publish() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure we can marshal all the json before sending anything to remote
|
err = remote.SetMultiMeta(updatedFiles)
|
||||||
targetsJSON, err := json.Marshal(targets)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
snapshotJSON, err := json.Marshal(snapshot)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
update := make(map[string][]byte)
|
|
||||||
// if we need to update the root, marshal it and push the update to remote
|
|
||||||
if updateRoot {
|
|
||||||
rootJSON, err := json.Marshal(root)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
update["root"] = rootJSON
|
|
||||||
}
|
|
||||||
update["targets"] = targetsJSON
|
|
||||||
update["snapshot"] = snapshotJSON
|
|
||||||
err = remote.SetMultiMeta(update)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -407,6 +650,11 @@ func (r *NotaryRepository) Publish() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bootstrapRepo loads the repository from the local file system. This attempts
|
||||||
|
// to load metadata for all roles. Since server snapshots are supported,
|
||||||
|
// if the snapshot metadata fails to load, that's ok.
|
||||||
|
// 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 {
|
func (r *NotaryRepository) bootstrapRepo() error {
|
||||||
kdb := keys.NewDB()
|
kdb := keys.NewDB()
|
||||||
tufRepo := tuf.NewRepo(kdb, r.CryptoService)
|
tufRepo := tuf.NewRepo(kdb, r.CryptoService)
|
||||||
|
|
@ -435,30 +683,32 @@ func (r *NotaryRepository) bootstrapRepo() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tufRepo.SetTargets("targets", targets)
|
tufRepo.SetTargets("targets", targets)
|
||||||
|
|
||||||
snapshotJSON, err := r.fileStore.GetMeta("snapshot", 0)
|
snapshotJSON, err := r.fileStore.GetMeta("snapshot", 0)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
|
snapshot := &data.SignedSnapshot{}
|
||||||
|
err = json.Unmarshal(snapshotJSON, snapshot)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tufRepo.SetSnapshot(snapshot)
|
||||||
|
} else if _, ok := err.(store.ErrMetaNotFound); !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
snapshot := &data.SignedSnapshot{}
|
|
||||||
err = json.Unmarshal(snapshotJSON, snapshot)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tufRepo.SetSnapshot(snapshot)
|
|
||||||
|
|
||||||
r.tufRepo = tufRepo
|
r.tufRepo = tufRepo
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NotaryRepository) saveMetadata() error {
|
func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error {
|
||||||
logrus.Debugf("Saving changes to Trusted Collection.")
|
logrus.Debugf("Saving changes to Trusted Collection.")
|
||||||
|
|
||||||
signedRoot, err := r.tufRepo.SignRoot(data.DefaultExpires("root"))
|
rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rootJSON, err := json.Marshal(signedRoot)
|
err = r.fileStore.SetMeta(data.CanonicalRootRole, rootJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -476,27 +726,22 @@ func (r *NotaryRepository) saveMetadata() error {
|
||||||
targetsToSave[t] = targetsJSON
|
targetsToSave[t] = targetsJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
signedSnapshot, err := r.tufRepo.SignSnapshot(data.DefaultExpires("snapshot"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
snapshotJSON, err := json.Marshal(signedSnapshot)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.fileStore.SetMeta("root", rootJSON)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for role, blob := range targetsToSave {
|
for role, blob := range targetsToSave {
|
||||||
parentDir := filepath.Dir(role)
|
parentDir := filepath.Dir(role)
|
||||||
os.MkdirAll(parentDir, 0755)
|
os.MkdirAll(parentDir, 0755)
|
||||||
r.fileStore.SetMeta(role, blob)
|
r.fileStore.SetMeta(role, blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.fileStore.SetMeta("snapshot", snapshotJSON)
|
if ignoreSnapshot {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalSnapshotRole)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.fileStore.SetMeta(data.CanonicalSnapshotRole, snapshotJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
|
func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
|
||||||
|
|
@ -515,11 +760,15 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
|
||||||
// the store and it doesn't know about the repo.
|
// the store and it doesn't know about the repo.
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rootJSON, err = r.fileStore.GetMeta("root", maxSize)
|
result, cacheErr := r.fileStore.GetMeta("root", maxSize)
|
||||||
if err != nil {
|
if cacheErr != nil {
|
||||||
// if cache didn't return a root, we cannot proceed
|
// if cache didn't return a root, we cannot proceed - just return
|
||||||
return nil, store.ErrMetaNotFound{}
|
// the original error.
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
rootJSON = result
|
||||||
|
logrus.Debugf(
|
||||||
|
"Using local cache instead of remote due to failure: %s", err.Error())
|
||||||
}
|
}
|
||||||
// can't just unmarshal into SignedRoot because validate root
|
// can't just unmarshal into SignedRoot because validate root
|
||||||
// needs the root.Signed field to still be []byte for signature
|
// needs the root.Signed field to still be []byte for signature
|
||||||
|
|
@ -530,7 +779,7 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.KeyStoreManager.ValidateRoot(root, r.gun)
|
err = r.CertManager.ValidateRoot(root, r.gun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -555,21 +804,32 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RotateKeys removes all existing keys associated with role and adds
|
// RotateKey removes all existing keys associated with the role, and either
|
||||||
// the keys specified by keyIDs to the role. These changes are staged
|
// creates and adds one new key or delegates managing the key to the server.
|
||||||
// in a changelist until publish is called.
|
// These changes are staged in a changelist until publish is called.
|
||||||
func (r *NotaryRepository) RotateKeys() error {
|
func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool) error {
|
||||||
for _, role := range []string{"targets", "snapshot"} {
|
if role == data.CanonicalRootRole || role == data.CanonicalTimestampRole {
|
||||||
key, err := r.CryptoService.Create(role, data.ECDSAKey)
|
return fmt.Errorf(
|
||||||
if err != nil {
|
"notary does not currently support rotating the %s key", role)
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = r.rootFileKeyChange(role, changelist.ActionCreate, key)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
if serverManagesKey && role == data.CanonicalTargetsRole {
|
||||||
|
return ErrInvalidRemoteRole{Role: data.CanonicalTargetsRole}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
pubKey data.PublicKey
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if serverManagesKey {
|
||||||
|
pubKey, err = getRemoteKey(r.baseURL, r.gun, role, r.roundTrip)
|
||||||
|
} else {
|
||||||
|
pubKey, err = r.CryptoService.Create(role, data.ECDSAKey)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.rootFileKeyChange(role, changelist.ActionCreate, pubKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NotaryRepository) rootFileKeyChange(role, action string, key data.PublicKey) error {
|
func (r *NotaryRepository) rootFileKeyChange(role, action string, key data.PublicKey) error {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
|
@ -36,10 +38,11 @@ func applyChangelist(repo *tuf.Repo, cl changelist.Changelist) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
switch c.Scope() {
|
isDel := data.IsDelegation(c.Scope())
|
||||||
case changelist.ScopeTargets:
|
switch {
|
||||||
|
case c.Scope() == changelist.ScopeTargets || isDel:
|
||||||
err = applyTargetsChange(repo, c)
|
err = applyTargetsChange(repo, c)
|
||||||
case changelist.ScopeRoot:
|
case c.Scope() == changelist.ScopeRoot:
|
||||||
err = applyRootChange(repo, c)
|
err = applyRootChange(repo, c)
|
||||||
default:
|
default:
|
||||||
logrus.Debug("scope not supported: ", c.Scope())
|
logrus.Debug("scope not supported: ", c.Scope())
|
||||||
|
|
@ -54,6 +57,89 @@ func applyChangelist(repo *tuf.Repo, cl changelist.Changelist) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyTargetsChange(repo *tuf.Repo, c changelist.Change) error {
|
func applyTargetsChange(repo *tuf.Repo, c changelist.Change) error {
|
||||||
|
switch c.Type() {
|
||||||
|
case changelist.TypeTargetsTarget:
|
||||||
|
return changeTargetMeta(repo, c)
|
||||||
|
case changelist.TypeTargetsDelegation:
|
||||||
|
return changeTargetsDelegation(repo, c)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("only target meta and delegations changes supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error {
|
||||||
|
switch c.Action() {
|
||||||
|
case changelist.ActionCreate:
|
||||||
|
td := changelist.TufDelegation{}
|
||||||
|
err := json.Unmarshal(c.Content(), &td)
|
||||||
|
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
|
||||||
|
return data.ErrInvalidRole{
|
||||||
|
Role: c.Scope(),
|
||||||
|
Reason: "cannot create a role that already exists",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// role doesn't exist, create brand new
|
||||||
|
r, err = td.ToNewRole(c.Scope())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return repo.UpdateDelegations(r, td.AddKeys)
|
||||||
|
case changelist.ActionUpdate:
|
||||||
|
td := changelist.TufDelegation{}
|
||||||
|
err := json.Unmarshal(c.Content(), &td)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r, err := repo.GetDelegation(c.Scope())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// role exists, merge
|
||||||
|
if err := r.AddPaths(td.AddPaths); 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)
|
||||||
|
case changelist.ActionDelete:
|
||||||
|
r := data.Role{Name: c.Scope()}
|
||||||
|
return repo.DeleteDelegation(r)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported action against delegations: %s", c.Action())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// applies a function repeatedly, falling back on the parent role, until it no
|
||||||
|
// longer can
|
||||||
|
func doWithRoleFallback(role string, doFunc func(string) error) error {
|
||||||
|
for role == data.CanonicalTargetsRole || data.IsDelegation(role) {
|
||||||
|
err := doFunc(role)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, ok := err.(data.ErrInvalidRole); !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
role = path.Dir(role)
|
||||||
|
}
|
||||||
|
return data.ErrInvalidRole{Role: role}
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeTargetMeta(repo *tuf.Repo, c changelist.Change) error {
|
||||||
var err error
|
var err error
|
||||||
switch c.Action() {
|
switch c.Action() {
|
||||||
case changelist.ActionCreate:
|
case changelist.ActionCreate:
|
||||||
|
|
@ -64,17 +150,29 @@ func applyTargetsChange(repo *tuf.Repo, c changelist.Change) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
files := data.Files{c.Path(): *meta}
|
files := data.Files{c.Path(): *meta}
|
||||||
_, err = repo.AddTargets(c.Scope(), files)
|
|
||||||
|
err = doWithRoleFallback(c.Scope(), func(role string) error {
|
||||||
|
_, e := repo.AddTargets(role, files)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("couldn't add target to %s: %s", c.Scope(), err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
case changelist.ActionDelete:
|
case changelist.ActionDelete:
|
||||||
logrus.Debug("changelist remove: ", c.Path())
|
logrus.Debug("changelist remove: ", c.Path())
|
||||||
err = repo.RemoveTargets(c.Scope(), c.Path())
|
|
||||||
|
err = doWithRoleFallback(c.Scope(), func(role string) error {
|
||||||
|
return repo.RemoveTargets(role, c.Path())
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("couldn't remove target from %s: %s", c.Scope(), err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logrus.Debug("action not yet supported: ", c.Action())
|
logrus.Debug("action not yet supported: ", c.Action())
|
||||||
}
|
}
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyRootChange(repo *tuf.Repo, c changelist.Change) error {
|
func applyRootChange(repo *tuf.Repo, c changelist.Change) error {
|
||||||
|
|
@ -112,35 +210,56 @@ func nearExpiry(r *data.SignedRoot) bool {
|
||||||
return r.Signed.Expires.Before(plus6mo)
|
return r.Signed.Expires.Before(plus6mo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initRoles(kdb *keys.KeyDB, rootKey, targetsKey, snapshotKey, timestampKey data.PublicKey) error {
|
// Fetches a public key from a remote store, given a gun and role
|
||||||
rootRole, err := data.NewRole("root", 1, []string{rootKey.ID()}, nil, nil)
|
func getRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKey, error) {
|
||||||
|
remote, err := getRemoteStore(url, gun, rt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
targetsRole, err := data.NewRole("targets", 1, []string{targetsKey.ID()}, nil, nil)
|
rawPubKey, err := remote.GetKey(role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
|
||||||
snapshotRole, err := data.NewRole("snapshot", 1, []string{snapshotKey.ID()}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
timestampRole, err := data.NewRole("timestamp", 1, []string{timestampKey.ID()}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := kdb.AddRole(rootRole); err != nil {
|
pubKey, err := data.UnmarshalPublicKey(rawPubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
return err
|
||||||
}
|
}
|
||||||
if err := kdb.AddRole(targetsRole); err != nil {
|
kdb.AddKey(key)
|
||||||
return err
|
if err := kdb.AddRole(theRole); err != nil {
|
||||||
}
|
|
||||||
if err := kdb.AddRole(snapshotRole); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := kdb.AddRole(timestampRole); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
switch {
|
||||||
|
case role == data.CanonicalRootRole:
|
||||||
|
s, err = tufRepo.SignRoot(data.DefaultExpires(role))
|
||||||
|
case role == data.CanonicalSnapshotRole:
|
||||||
|
s, err = tufRepo.SignSnapshot(data.DefaultExpires(role))
|
||||||
|
case tufRepo.Targets[role] != nil:
|
||||||
|
s, err = tufRepo.SignTargets(
|
||||||
|
role, data.DefaultExpires(data.CanonicalTargetsRole))
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("%s not supported role to sign on the client", role)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(s)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,52 +5,23 @@ package client
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/docker/notary/cryptoservice"
|
|
||||||
"github.com/docker/notary/keystoremanager"
|
|
||||||
"github.com/docker/notary/passphrase"
|
"github.com/docker/notary/passphrase"
|
||||||
"github.com/docker/notary/trustmanager"
|
"github.com/docker/notary/trustmanager"
|
||||||
"github.com/docker/notary/tuf/store"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewNotaryRepository is a helper method that returns a new notary repository.
|
// NewNotaryRepository is a helper method that returns a new notary repository.
|
||||||
// It takes the base directory under where all the trust files will be stored
|
// It takes the base directory under where all the trust files will be stored
|
||||||
// (usually ~/.docker/trust/).
|
// (usually ~/.docker/trust/).
|
||||||
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
|
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
|
||||||
retriever passphrase.Retriever) (*NotaryRepository, error) {
|
retriever passphrase.Retriever) (
|
||||||
|
*NotaryRepository, error) {
|
||||||
|
|
||||||
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever)
|
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create private key store in directory: %s", baseDir)
|
return nil, fmt.Errorf("failed to create private key store in directory: %s", baseDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, fileKeyStore)
|
return repositoryFromKeystores(baseDir, gun, baseURL, rt,
|
||||||
if err != nil {
|
[]trustmanager.KeyStore{fileKeyStore})
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cryptoService := cryptoservice.NewCryptoService(gun, keyStoreManager.KeyStore)
|
|
||||||
|
|
||||||
nRepo := &NotaryRepository{
|
|
||||||
gun: gun,
|
|
||||||
baseDir: baseDir,
|
|
||||||
baseURL: baseURL,
|
|
||||||
tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)),
|
|
||||||
CryptoService: cryptoService,
|
|
||||||
roundTrip: rt,
|
|
||||||
KeyStoreManager: keyStoreManager,
|
|
||||||
}
|
|
||||||
|
|
||||||
fileStore, err := store.NewFilesystemStore(
|
|
||||||
nRepo.tufRepoPath,
|
|
||||||
"metadata",
|
|
||||||
"json",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
nRepo.fileStore = fileStore
|
|
||||||
|
|
||||||
return nRepo, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,57 +5,29 @@ package client
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/docker/notary/cryptoservice"
|
|
||||||
"github.com/docker/notary/keystoremanager"
|
|
||||||
"github.com/docker/notary/passphrase"
|
"github.com/docker/notary/passphrase"
|
||||||
"github.com/docker/notary/trustmanager"
|
"github.com/docker/notary/trustmanager"
|
||||||
"github.com/docker/notary/trustmanager/yubikey"
|
"github.com/docker/notary/trustmanager/yubikey"
|
||||||
"github.com/docker/notary/tuf/signed"
|
|
||||||
"github.com/docker/notary/tuf/store"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewNotaryRepository is a helper method that returns a new notary repository.
|
// NewNotaryRepository is a helper method that returns a new notary repository.
|
||||||
// It takes the base directory under where all the trust files will be stored
|
// It takes the base directory under where all the trust files will be stored
|
||||||
// (usually ~/.docker/trust/).
|
// (usually ~/.docker/trust/).
|
||||||
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
|
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
|
||||||
retriever passphrase.Retriever) (*NotaryRepository, error) {
|
retriever passphrase.Retriever) (
|
||||||
|
*NotaryRepository, error) {
|
||||||
|
|
||||||
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever)
|
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create private key store in directory: %s", baseDir)
|
return nil, fmt.Errorf("failed to create private key store in directory: %s", baseDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, fileKeyStore)
|
keyStores := []trustmanager.KeyStore{fileKeyStore}
|
||||||
yubiKeyStore, _ := yubikey.NewYubiKeyStore(fileKeyStore, retriever)
|
yubiKeyStore, _ := yubikey.NewYubiKeyStore(fileKeyStore, retriever)
|
||||||
var cryptoService signed.CryptoService
|
if yubiKeyStore != nil {
|
||||||
if yubiKeyStore == nil {
|
keyStores = append(keyStores, yubiKeyStore)
|
||||||
cryptoService = cryptoservice.NewCryptoService(gun, keyStoreManager.KeyStore)
|
|
||||||
} else {
|
|
||||||
cryptoService = cryptoservice.NewCryptoService(gun, yubiKeyStore, keyStoreManager.KeyStore)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nRepo := &NotaryRepository{
|
return repositoryFromKeystores(baseDir, gun, baseURL, rt, keyStores)
|
||||||
gun: gun,
|
|
||||||
baseDir: baseDir,
|
|
||||||
baseURL: baseURL,
|
|
||||||
tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)),
|
|
||||||
CryptoService: cryptoService,
|
|
||||||
roundTrip: rt,
|
|
||||||
KeyStoreManager: keyStoreManager,
|
|
||||||
}
|
|
||||||
|
|
||||||
fileStore, err := store.NewFilesystemStore(
|
|
||||||
nRepo.tufRepoPath,
|
|
||||||
"metadata",
|
|
||||||
"json",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
nRepo.fileStore = fileStore
|
|
||||||
|
|
||||||
return nRepo, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,35 @@
|
||||||
package cryptoservice
|
package cryptoservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/docker/notary/trustmanager"
|
"github.com/docker/notary/trustmanager"
|
||||||
"github.com/docker/notary/tuf/data"
|
"github.com/docker/notary/tuf/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateCertificate generates an X509 Certificate from a template, given a GUN
|
// GenerateCertificate generates an X509 Certificate from a template, given a GUN and validity interval
|
||||||
func GenerateCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate, error) {
|
func GenerateCertificate(rootKey data.PrivateKey, gun string, startTime, endTime time.Time) (*x509.Certificate, error) {
|
||||||
signer := rootKey.CryptoSigner()
|
signer := rootKey.CryptoSigner()
|
||||||
if signer == nil {
|
if signer == nil {
|
||||||
return nil, fmt.Errorf("key type not supported for Certificate generation: %s\n", rootKey.Algorithm())
|
return nil, fmt.Errorf("key type not supported for Certificate generation: %s\n", rootKey.Algorithm())
|
||||||
}
|
}
|
||||||
|
|
||||||
template, err := trustmanager.NewCertificate(gun)
|
return generateCertificate(signer, gun, startTime, endTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateTestingCertificate generates a non-expired X509 Certificate from a template, given a GUN.
|
||||||
|
// Good enough for tests where expiration does not really matter; do not use if you care about the policy.
|
||||||
|
func GenerateTestingCertificate(signer crypto.Signer, gun string) (*x509.Certificate, error) {
|
||||||
|
startTime := time.Now()
|
||||||
|
return generateCertificate(signer, gun, startTime, startTime.AddDate(10, 0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateCertificate(signer crypto.Signer, gun string, startTime, endTime time.Time) (*x509.Certificate, error) {
|
||||||
|
template, err := trustmanager.NewCertificate(gun, startTime, endTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create the certificate template for: %s (%v)", gun, err)
|
return nil, fmt.Errorf("failed to create the certificate template for: %s (%v)", gun, err)
|
||||||
}
|
}
|
||||||
|
|
@ -26,7 +39,6 @@ func GenerateCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate
|
||||||
return nil, fmt.Errorf("failed to create the certificate for: %s (%v)", gun, err)
|
return nil, fmt.Errorf("failed to create the certificate for: %s (%v)", gun, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode the new certificate into PEM
|
|
||||||
cert, err := x509.ParseCertificate(derBytes)
|
cert, err := x509.ParseCertificate(derBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse the certificate for key: %s (%v)", gun, err)
|
return nil, fmt.Errorf("failed to parse the certificate for key: %s (%v)", gun, err)
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,15 @@ func (cs *CryptoService) GetPrivateKey(keyID string) (k data.PrivateKey, role st
|
||||||
for _, ks := range cs.keyStores {
|
for _, ks := range cs.keyStores {
|
||||||
for _, keyPath := range keyPaths {
|
for _, keyPath := range keyPaths {
|
||||||
k, role, err = ks.GetKey(keyPath)
|
k, role, err = ks.GetKey(keyPath)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch err.(type) {
|
||||||
|
case trustmanager.ErrPasswordInvalid, trustmanager.ErrAttemptsExceeded:
|
||||||
|
return
|
||||||
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return // returns whatever the final values were
|
return // returns whatever the final values were
|
||||||
|
|
@ -111,39 +116,6 @@ func (cs *CryptoService) RemoveKey(keyID string) (err error) {
|
||||||
return // returns whatever the final values were
|
return // returns whatever the final values were
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign returns the signatures for the payload with a set of keyIDs. It ignores
|
|
||||||
// errors to sign and expects the called to validate if the number of returned
|
|
||||||
// signatures is adequate.
|
|
||||||
func (cs *CryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signature, error) {
|
|
||||||
signatures := make([]data.Signature, 0, len(keyIDs))
|
|
||||||
for _, keyID := range keyIDs {
|
|
||||||
privKey, _, err := cs.GetPrivateKey(keyID)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Debugf("error attempting to retrieve private key: %s, %v", keyID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sigAlgo := privKey.SignatureAlgorithm()
|
|
||||||
sig, err := privKey.Sign(rand.Reader, payload, nil)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Debugf("ignoring error attempting to %s sign with keyID: %s, %v",
|
|
||||||
privKey.Algorithm(), keyID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("appending %s signature with Key ID: %s", privKey.Algorithm(), keyID)
|
|
||||||
|
|
||||||
// Append signatures to result array
|
|
||||||
signatures = append(signatures, data.Signature{
|
|
||||||
KeyID: keyID,
|
|
||||||
Method: sigAlgo,
|
|
||||||
Signature: sig[:],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return signatures, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListKeys returns a list of key IDs valid for the given role
|
// ListKeys returns a list of key IDs valid for the given role
|
||||||
func (cs *CryptoService) ListKeys(role string) []string {
|
func (cs *CryptoService) ListKeys(role string) []string {
|
||||||
var res []string
|
var res []string
|
||||||
|
|
|
||||||
|
|
@ -261,12 +261,12 @@ func moveKeysByGUN(oldKeyStore, newKeyStore trustmanager.KeyStore, gun string) e
|
||||||
|
|
||||||
func moveKeys(oldKeyStore, newKeyStore trustmanager.KeyStore) error {
|
func moveKeys(oldKeyStore, newKeyStore trustmanager.KeyStore) error {
|
||||||
for f := range oldKeyStore.ListKeys() {
|
for f := range oldKeyStore.ListKeys() {
|
||||||
privateKey, alias, err := oldKeyStore.GetKey(f)
|
privateKey, role, err := oldKeyStore.GetKey(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = newKeyStore.AddKey(f, alias, privateKey)
|
err = newKeyStore.AddKey(f, role, privateKey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -278,7 +278,10 @@ func moveKeys(oldKeyStore, newKeyStore trustmanager.KeyStore) error {
|
||||||
|
|
||||||
func addKeysToArchive(zipWriter *zip.Writer, newKeyStore *trustmanager.KeyFileStore) error {
|
func addKeysToArchive(zipWriter *zip.Writer, newKeyStore *trustmanager.KeyFileStore) error {
|
||||||
for _, relKeyPath := range newKeyStore.ListFiles() {
|
for _, relKeyPath := range newKeyStore.ListFiles() {
|
||||||
fullKeyPath := filepath.Join(newKeyStore.BaseDir(), relKeyPath)
|
fullKeyPath, err := newKeyStore.GetPath(relKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
fi, err := os.Lstat(fullKeyPath)
|
fi, err := os.Lstat(fullKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -290,7 +293,11 @@ func addKeysToArchive(zipWriter *zip.Writer, newKeyStore *trustmanager.KeyFileSt
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
infoHeader.Name = relKeyPath
|
relPath, err := filepath.Rel(newKeyStore.BaseDir(), fullKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
infoHeader.Name = relPath
|
||||||
|
|
||||||
zipFileEntryWriter, err := zipWriter.CreateHeader(infoHeader)
|
zipFileEntryWriter, err := zipWriter.CreateHeader(infoHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ notaryserver:
|
||||||
- "8080"
|
- "8080"
|
||||||
- "4443:4443"
|
- "4443:4443"
|
||||||
environment:
|
environment:
|
||||||
SERVICE_NAME: notary
|
- SERVICE_NAME=notary
|
||||||
|
command: -config=fixtures/server-config.json
|
||||||
notarysigner:
|
notarysigner:
|
||||||
volumes:
|
volumes:
|
||||||
- /dev/bus/usb/003/010:/dev/bus/usb/002/010
|
- /dev/bus/usb/003/010:/dev/bus/usb/002/010
|
||||||
|
|
@ -17,7 +18,10 @@ notarysigner:
|
||||||
dockerfile: Dockerfile.signer
|
dockerfile: Dockerfile.signer
|
||||||
links:
|
links:
|
||||||
- notarymysql
|
- notarymysql
|
||||||
|
command: -config=fixtures/signer-config.json
|
||||||
notarymysql:
|
notarymysql:
|
||||||
|
volumes:
|
||||||
|
- notarymysql:/var/lib/mysql
|
||||||
build: ./notarymysql/
|
build: ./notarymysql/
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "3306:3306"
|
||||||
|
|
|
||||||
|
|
@ -99,12 +99,16 @@ func PromptRetrieverWithInOut(in io.Reader, out io.Writer, aliasMap map[string]s
|
||||||
return "", true, ErrTooManyAttempts
|
return "", true, ErrTooManyAttempts
|
||||||
}
|
}
|
||||||
|
|
||||||
state, err := term.SaveState(0)
|
// If typing on the terminal, we do not want the terminal to echo the
|
||||||
if err != nil {
|
// password that is typed (so it doesn't display)
|
||||||
return "", false, err
|
if term.IsTerminal(0) {
|
||||||
|
state, err := term.SaveState(0)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
term.DisableEcho(0, state)
|
||||||
|
defer term.RestoreTerminal(0, state)
|
||||||
}
|
}
|
||||||
term.DisableEcho(0, state)
|
|
||||||
defer term.RestoreTerminal(0, state)
|
|
||||||
|
|
||||||
stdin := bufio.NewReader(in)
|
stdin := bufio.NewReader(in)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,13 @@
|
||||||
package trustmanager
|
package trustmanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/notary"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
visible = notary.PubCertPerms
|
|
||||||
private = notary.PrivKeyPerms
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrPathOutsideStore indicates that the returned path would be
|
|
||||||
// outside the store
|
|
||||||
ErrPathOutsideStore = errors.New("path outside file store")
|
|
||||||
)
|
|
||||||
|
|
||||||
// LimitedFileStore implements the bare bones primitives (no hierarchy)
|
|
||||||
type LimitedFileStore interface {
|
|
||||||
Add(fileName string, data []byte) error
|
|
||||||
Remove(fileName string) error
|
|
||||||
Get(fileName string) ([]byte, error)
|
|
||||||
ListFiles() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileStore is the interface for full-featured FileStores
|
|
||||||
type FileStore interface {
|
|
||||||
LimitedFileStore
|
|
||||||
|
|
||||||
RemoveDir(directoryName string) error
|
|
||||||
GetPath(fileName string) (string, error)
|
|
||||||
ListDir(directoryName string) []string
|
|
||||||
BaseDir() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimpleFileStore implements FileStore
|
// SimpleFileStore implements FileStore
|
||||||
type SimpleFileStore struct {
|
type SimpleFileStore struct {
|
||||||
baseDir string
|
baseDir string
|
||||||
|
|
@ -55,6 +23,10 @@ func NewSimpleFileStore(baseDir string, fileExt string) (*SimpleFileStore, error
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(fileExt, ".") {
|
||||||
|
fileExt = "." + fileExt
|
||||||
|
}
|
||||||
|
|
||||||
return &SimpleFileStore{
|
return &SimpleFileStore{
|
||||||
baseDir: baseDir,
|
baseDir: baseDir,
|
||||||
fileExt: fileExt,
|
fileExt: fileExt,
|
||||||
|
|
@ -68,6 +40,10 @@ func NewPrivateSimpleFileStore(baseDir string, fileExt string) (*SimpleFileStore
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(fileExt, ".") {
|
||||||
|
fileExt = "." + fileExt
|
||||||
|
}
|
||||||
|
|
||||||
return &SimpleFileStore{
|
return &SimpleFileStore{
|
||||||
baseDir: baseDir,
|
baseDir: baseDir,
|
||||||
fileExt: fileExt,
|
fileExt: fileExt,
|
||||||
|
|
@ -176,7 +152,8 @@ func (f *SimpleFileStore) list(path string) []string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
files = append(files, fp)
|
trimmed := strings.TrimSuffix(fp, f.fileExt)
|
||||||
|
files = append(files, trimmed)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
@ -185,7 +162,7 @@ func (f *SimpleFileStore) list(path string) []string {
|
||||||
|
|
||||||
// genFileName returns the name using the right extension
|
// genFileName returns the name using the right extension
|
||||||
func (f *SimpleFileStore) genFileName(name string) string {
|
func (f *SimpleFileStore) genFileName(name string) string {
|
||||||
return fmt.Sprintf("%s.%s", name, f.fileExt)
|
return fmt.Sprintf("%s%s", name, f.fileExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BaseDir returns the base directory of the filestore
|
// BaseDir returns the base directory of the filestore
|
||||||
|
|
@ -212,68 +189,3 @@ func createDirectory(dir string, perms os.FileMode) error {
|
||||||
dir = dir + "/"
|
dir = dir + "/"
|
||||||
return os.MkdirAll(dir, perms)
|
return os.MkdirAll(dir, perms)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryFileStore is an implementation of LimitedFileStore that keeps
|
|
||||||
// the contents in memory.
|
|
||||||
type MemoryFileStore struct {
|
|
||||||
sync.Mutex
|
|
||||||
|
|
||||||
files map[string][]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMemoryFileStore creates a MemoryFileStore
|
|
||||||
func NewMemoryFileStore() *MemoryFileStore {
|
|
||||||
return &MemoryFileStore{
|
|
||||||
files: make(map[string][]byte),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrMemFileNotFound is returned for a nonexistent "file" in the memory file
|
|
||||||
// store
|
|
||||||
var ErrMemFileNotFound = errors.New("key not found in memory file store")
|
|
||||||
|
|
||||||
// Add writes data to a file with a given name
|
|
||||||
func (f *MemoryFileStore) Add(name string, data []byte) error {
|
|
||||||
f.Lock()
|
|
||||||
defer f.Unlock()
|
|
||||||
|
|
||||||
f.files[name] = data
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes a file identified by name
|
|
||||||
func (f *MemoryFileStore) Remove(name string) error {
|
|
||||||
f.Lock()
|
|
||||||
defer f.Unlock()
|
|
||||||
|
|
||||||
if _, present := f.files[name]; !present {
|
|
||||||
return ErrMemFileNotFound
|
|
||||||
}
|
|
||||||
delete(f.files, name)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the data given a file name
|
|
||||||
func (f *MemoryFileStore) Get(name string) ([]byte, error) {
|
|
||||||
f.Lock()
|
|
||||||
defer f.Unlock()
|
|
||||||
|
|
||||||
fileData, present := f.files[name]
|
|
||||||
if !present {
|
|
||||||
return nil, ErrMemFileNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListFiles lists all the files inside of a store
|
|
||||||
func (f *MemoryFileStore) ListFiles() []string {
|
|
||||||
var list []string
|
|
||||||
|
|
||||||
for name := range f.files {
|
|
||||||
list = append(list, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package trustmanager
|
package trustmanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/notary/passphrase"
|
"github.com/docker/notary/passphrase"
|
||||||
"github.com/docker/notary/tuf/data"
|
"github.com/docker/notary/tuf/data"
|
||||||
)
|
)
|
||||||
|
|
@ -54,10 +56,10 @@ func (s *KeyFileStore) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddKey stores the contents of a PEM-encoded private key as a PEM block
|
// AddKey stores the contents of a PEM-encoded private key as a PEM block
|
||||||
func (s *KeyFileStore) AddKey(name, alias string, privKey data.PrivateKey) error {
|
func (s *KeyFileStore) AddKey(name, role string, privKey data.PrivateKey) error {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer s.Unlock()
|
defer s.Unlock()
|
||||||
return addKey(s, s.Retriever, s.cachedKeys, name, alias, privKey)
|
return addKey(s, s.Retriever, s.cachedKeys, name, role, privKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKey returns the PrivateKey given a KeyID
|
// GetKey returns the PrivateKey given a KeyID
|
||||||
|
|
@ -153,7 +155,7 @@ func (s *KeyMemoryStore) ImportKey(pemBytes []byte, alias string) error {
|
||||||
return importKey(s, s.Retriever, s.cachedKeys, alias, pemBytes)
|
return importKey(s, s.Retriever, s.cachedKeys, alias, pemBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name, alias string, privKey data.PrivateKey) error {
|
func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
chosenPassphrase string
|
chosenPassphrase string
|
||||||
|
|
@ -162,7 +164,7 @@ func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cached
|
||||||
)
|
)
|
||||||
|
|
||||||
for attempts := 0; ; attempts++ {
|
for attempts := 0; ; attempts++ {
|
||||||
chosenPassphrase, giveup, err = passphraseRetriever(name, alias, true, attempts)
|
chosenPassphrase, giveup, err = passphraseRetriever(name, role, true, attempts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -175,25 +177,37 @@ func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cached
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return encryptAndAddKey(s, chosenPassphrase, cachedKeys, name, alias, privKey)
|
return encryptAndAddKey(s, chosenPassphrase, cachedKeys, name, role, privKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKeyAlias(s LimitedFileStore, keyID string) (string, error) {
|
// getKeyRole finds the role for the given keyID. It attempts to look
|
||||||
files := s.ListFiles()
|
// both in the newer format PEM headers, and also in the legacy filename
|
||||||
|
// format. It returns: the role, whether it was found in the legacy format
|
||||||
|
// (true == legacy), and an error
|
||||||
|
func getKeyRole(s LimitedFileStore, keyID string) (string, bool, error) {
|
||||||
name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(keyID), filepath.Ext(keyID)))
|
name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(keyID), filepath.Ext(keyID)))
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range s.ListFiles() {
|
||||||
filename := filepath.Base(file)
|
filename := filepath.Base(file)
|
||||||
|
|
||||||
if strings.HasPrefix(filename, name) {
|
if strings.HasPrefix(filename, name) {
|
||||||
aliasPlusDotKey := strings.TrimPrefix(filename, name+"_")
|
d, err := s.Get(file)
|
||||||
retVal := strings.TrimSuffix(aliasPlusDotKey, "."+keyExtension)
|
if err != nil {
|
||||||
return retVal, nil
|
return "", false, err
|
||||||
|
}
|
||||||
|
block, _ := pem.Decode(d)
|
||||||
|
if block != nil {
|
||||||
|
if role, ok := block.Headers["role"]; ok {
|
||||||
|
return role, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
role := strings.TrimPrefix(filename, name+"_")
|
||||||
|
return role, true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", &ErrKeyNotFound{KeyID: keyID}
|
return "", false, &ErrKeyNotFound{KeyID: keyID}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKey returns the PrivateKey given a KeyID
|
// GetKey returns the PrivateKey given a KeyID
|
||||||
|
|
@ -208,14 +222,13 @@ func getKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cached
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var retErr error
|
|
||||||
// See if the key is encrypted. If its encrypted we'll fail to parse the private key
|
// See if the key is encrypted. If its encrypted we'll fail to parse the private key
|
||||||
privKey, err := ParsePEMPrivateKey(keyBytes, "")
|
privKey, err := ParsePEMPrivateKey(keyBytes, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
privKey, _, retErr = GetPasswdDecryptBytes(passphraseRetriever, keyBytes, name, string(keyAlias))
|
privKey, _, err = GetPasswdDecryptBytes(passphraseRetriever, keyBytes, name, string(keyAlias))
|
||||||
}
|
if err != nil {
|
||||||
if retErr != nil {
|
return nil, "", err
|
||||||
return nil, "", retErr
|
}
|
||||||
}
|
}
|
||||||
cachedKeys[name] = &cachedKey{alias: keyAlias, key: privKey}
|
cachedKeys[name] = &cachedKey{alias: keyAlias, key: privKey}
|
||||||
return privKey, keyAlias, nil
|
return privKey, keyAlias, nil
|
||||||
|
|
@ -228,44 +241,58 @@ func listKeys(s LimitedFileStore) map[string]string {
|
||||||
|
|
||||||
for _, f := range s.ListFiles() {
|
for _, f := range s.ListFiles() {
|
||||||
// Remove the prefix of the directory from the filename
|
// Remove the prefix of the directory from the filename
|
||||||
if f[:len(rootKeysSubdir)] == rootKeysSubdir {
|
var keyIDFull string
|
||||||
f = strings.TrimPrefix(f, rootKeysSubdir+"/")
|
if strings.HasPrefix(f, rootKeysSubdir+"/") {
|
||||||
|
keyIDFull = strings.TrimPrefix(f, rootKeysSubdir+"/")
|
||||||
} else {
|
} else {
|
||||||
f = strings.TrimPrefix(f, nonRootKeysSubdir+"/")
|
keyIDFull = strings.TrimPrefix(f, nonRootKeysSubdir+"/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the extension from the full filename
|
keyIDFull = strings.TrimSpace(keyIDFull)
|
||||||
// abcde_root.key becomes abcde_root
|
|
||||||
keyIDFull := strings.TrimSpace(strings.TrimSuffix(f, filepath.Ext(f)))
|
|
||||||
|
|
||||||
// If the key does not have a _, it is malformed
|
// If the key does not have a _, we'll attempt to
|
||||||
|
// read it as a PEM
|
||||||
underscoreIndex := strings.LastIndex(keyIDFull, "_")
|
underscoreIndex := strings.LastIndex(keyIDFull, "_")
|
||||||
if underscoreIndex == -1 {
|
if underscoreIndex == -1 {
|
||||||
continue
|
d, err := s.Get(f)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
block, _ := pem.Decode(d)
|
||||||
|
if block == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if role, ok := block.Headers["role"]; ok {
|
||||||
|
keyIDMap[keyIDFull] = role
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The keyID is the first part of the keyname
|
||||||
|
// The KeyAlias is the second part of the keyname
|
||||||
|
// in a key named abcde_root, abcde is the keyID and root is the KeyAlias
|
||||||
|
keyID := keyIDFull[:underscoreIndex]
|
||||||
|
keyAlias := keyIDFull[underscoreIndex+1:]
|
||||||
|
keyIDMap[keyID] = keyAlias
|
||||||
}
|
}
|
||||||
|
|
||||||
// The keyID is the first part of the keyname
|
|
||||||
// The KeyAlias is the second part of the keyname
|
|
||||||
// in a key named abcde_root, abcde is the keyID and root is the KeyAlias
|
|
||||||
keyID := keyIDFull[:underscoreIndex]
|
|
||||||
keyAlias := keyIDFull[underscoreIndex+1:]
|
|
||||||
keyIDMap[keyID] = keyAlias
|
|
||||||
}
|
}
|
||||||
return keyIDMap
|
return keyIDMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveKey removes the key from the keyfilestore
|
// RemoveKey removes the key from the keyfilestore
|
||||||
func removeKey(s LimitedFileStore, cachedKeys map[string]*cachedKey, name string) error {
|
func removeKey(s LimitedFileStore, cachedKeys map[string]*cachedKey, name string) error {
|
||||||
keyAlias, err := getKeyAlias(s, name)
|
role, legacy, err := getKeyRole(s, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(cachedKeys, name)
|
delete(cachedKeys, name)
|
||||||
|
|
||||||
|
if legacy {
|
||||||
|
name = name + "_" + role
|
||||||
|
}
|
||||||
|
|
||||||
// being in a subdirectory is for backwards compatibliity
|
// being in a subdirectory is for backwards compatibliity
|
||||||
filename := name + "_" + keyAlias
|
err = s.Remove(filepath.Join(getSubdir(role), name))
|
||||||
err = s.Remove(filepath.Join(getSubdir(keyAlias), filename))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -283,18 +310,21 @@ func getSubdir(alias string) string {
|
||||||
// Given a key ID, gets the bytes and alias belonging to that key if the key
|
// Given a key ID, gets the bytes and alias belonging to that key if the key
|
||||||
// exists
|
// exists
|
||||||
func getRawKey(s LimitedFileStore, name string) ([]byte, string, error) {
|
func getRawKey(s LimitedFileStore, name string) ([]byte, string, error) {
|
||||||
keyAlias, err := getKeyAlias(s, name)
|
role, legacy, err := getKeyRole(s, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
filename := name + "_" + keyAlias
|
if legacy {
|
||||||
|
name = name + "_" + role
|
||||||
|
}
|
||||||
|
|
||||||
var keyBytes []byte
|
var keyBytes []byte
|
||||||
keyBytes, err = s.Get(filepath.Join(getSubdir(keyAlias), filename))
|
keyBytes, err = s.Get(filepath.Join(getSubdir(role), name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
return keyBytes, keyAlias, nil
|
return keyBytes, role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPasswdDecryptBytes gets the password to decript the given pem bytes.
|
// GetPasswdDecryptBytes gets the password to decript the given pem bytes.
|
||||||
|
|
@ -335,7 +365,7 @@ func GetPasswdDecryptBytes(passphraseRetriever passphrase.Retriever, pemBytes []
|
||||||
return privKey, passwd, nil
|
return privKey, passwd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func encryptAndAddKey(s LimitedFileStore, passwd string, cachedKeys map[string]*cachedKey, name, alias string, privKey data.PrivateKey) error {
|
func encryptAndAddKey(s LimitedFileStore, passwd string, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pemPrivKey []byte
|
pemPrivKey []byte
|
||||||
|
|
@ -343,17 +373,17 @@ func encryptAndAddKey(s LimitedFileStore, passwd string, cachedKeys map[string]*
|
||||||
)
|
)
|
||||||
|
|
||||||
if passwd != "" {
|
if passwd != "" {
|
||||||
pemPrivKey, err = EncryptPrivateKey(privKey, passwd)
|
pemPrivKey, err = EncryptPrivateKey(privKey, role, passwd)
|
||||||
} else {
|
} else {
|
||||||
pemPrivKey, err = KeyToPEM(privKey)
|
pemPrivKey, err = KeyToPEM(privKey, role)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedKeys[name] = &cachedKey{alias: alias, key: privKey}
|
cachedKeys[name] = &cachedKey{alias: role, key: privKey}
|
||||||
return s.Add(filepath.Join(getSubdir(alias), name+"_"+alias), pemPrivKey)
|
return s.Add(filepath.Join(getSubdir(role), name), pemPrivKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func importKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, alias string, pemBytes []byte) error {
|
func importKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, alias string, pemBytes []byte) error {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package trustmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MemoryFileStore is an implementation of LimitedFileStore that keeps
|
||||||
|
// the contents in memory.
|
||||||
|
type MemoryFileStore struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
files map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemoryFileStore creates a MemoryFileStore
|
||||||
|
func NewMemoryFileStore() *MemoryFileStore {
|
||||||
|
return &MemoryFileStore{
|
||||||
|
files: make(map[string][]byte),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add writes data to a file with a given name
|
||||||
|
func (f *MemoryFileStore) Add(name string, data []byte) error {
|
||||||
|
f.Lock()
|
||||||
|
defer f.Unlock()
|
||||||
|
|
||||||
|
f.files[name] = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes a file identified by name
|
||||||
|
func (f *MemoryFileStore) Remove(name string) error {
|
||||||
|
f.Lock()
|
||||||
|
defer f.Unlock()
|
||||||
|
|
||||||
|
if _, present := f.files[name]; !present {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
delete(f.files, name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the data given a file name
|
||||||
|
func (f *MemoryFileStore) Get(name string) ([]byte, error) {
|
||||||
|
f.Lock()
|
||||||
|
defer f.Unlock()
|
||||||
|
|
||||||
|
fileData, present := f.files[name]
|
||||||
|
if !present {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFiles lists all the files inside of a store
|
||||||
|
func (f *MemoryFileStore) ListFiles() []string {
|
||||||
|
var list []string
|
||||||
|
|
||||||
|
for name := range f.files {
|
||||||
|
list = append(list, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package trustmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/docker/notary"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
visible = notary.PubCertPerms
|
||||||
|
private = notary.PrivKeyPerms
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrPathOutsideStore indicates that the returned path would be
|
||||||
|
// outside the store
|
||||||
|
ErrPathOutsideStore = errors.New("path outside file store")
|
||||||
|
)
|
||||||
|
|
||||||
|
// LimitedFileStore implements the bare bones primitives (no hierarchy)
|
||||||
|
type LimitedFileStore interface {
|
||||||
|
// Add writes a file to the specified location, returning an error if this
|
||||||
|
// is not possible (reasons may include permissions errors). The path is cleaned
|
||||||
|
// before being made absolute against the store's base dir.
|
||||||
|
Add(fileName string, data []byte) error
|
||||||
|
|
||||||
|
// Remove deletes a file from the store relative to the store's base directory.
|
||||||
|
// The path is cleaned before being made absolute to ensure no path traversal
|
||||||
|
// outside the base directory is possible.
|
||||||
|
Remove(fileName string) error
|
||||||
|
|
||||||
|
// Get returns the file content found at fileName relative to the base directory
|
||||||
|
// of the file store. The path is cleaned before being made absolute to ensure
|
||||||
|
// path traversal outside the store is not possible. If the file is not found
|
||||||
|
// an error to that effect is returned.
|
||||||
|
Get(fileName string) ([]byte, error)
|
||||||
|
|
||||||
|
// ListFiles returns a list of paths relative to the base directory of the
|
||||||
|
// filestore. Any of these paths must be retrievable via the
|
||||||
|
// LimitedFileStore.Get method.
|
||||||
|
ListFiles() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileStore is the interface for full-featured FileStores
|
||||||
|
type FileStore interface {
|
||||||
|
LimitedFileStore
|
||||||
|
|
||||||
|
RemoveDir(directoryName string) error
|
||||||
|
GetPath(fileName string) (string, error)
|
||||||
|
ListDir(directoryName string) []string
|
||||||
|
BaseDir() string
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,6 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
|
@ -117,11 +116,15 @@ func fingerprintCert(cert *x509.Certificate) (CertID, error) {
|
||||||
|
|
||||||
// loadCertsFromDir receives a store AddCertFromFile for each certificate found
|
// loadCertsFromDir receives a store AddCertFromFile for each certificate found
|
||||||
func loadCertsFromDir(s *X509FileStore) error {
|
func loadCertsFromDir(s *X509FileStore) error {
|
||||||
certFiles := s.fileStore.ListFiles()
|
for _, f := range s.fileStore.ListFiles() {
|
||||||
for _, f := range certFiles {
|
|
||||||
// ListFiles returns relative paths
|
// ListFiles returns relative paths
|
||||||
fullPath := filepath.Join(s.fileStore.BaseDir(), f)
|
data, err := s.fileStore.Get(f)
|
||||||
err := s.AddCertFromFile(fullPath)
|
if err != nil {
|
||||||
|
// the filestore told us it had a file that it then couldn't serve.
|
||||||
|
// this is a serious problem so error immediately
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.AddCertFromPEM(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*ErrCertValidation); ok {
|
if _, ok := err.(*ErrCertValidation); ok {
|
||||||
logrus.Debugf("ignoring certificate, did not pass validation: %s", f)
|
logrus.Debugf("ignoring certificate, did not pass validation: %s", f)
|
||||||
|
|
@ -411,18 +414,26 @@ func blockType(k data.PrivateKey) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyToPEM returns a PEM encoded key from a Private Key
|
// KeyToPEM returns a PEM encoded key from a Private Key
|
||||||
func KeyToPEM(privKey data.PrivateKey) ([]byte, error) {
|
func KeyToPEM(privKey data.PrivateKey, role string) ([]byte, error) {
|
||||||
bt, err := blockType(privKey)
|
bt, err := blockType(privKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return pem.EncodeToMemory(&pem.Block{Type: bt, Bytes: privKey.Private()}), nil
|
block := &pem.Block{
|
||||||
|
Type: bt,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"role": role,
|
||||||
|
},
|
||||||
|
Bytes: privKey.Private(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return pem.EncodeToMemory(block), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptPrivateKey returns an encrypted PEM key given a Privatekey
|
// EncryptPrivateKey returns an encrypted PEM key given a Privatekey
|
||||||
// and a passphrase
|
// and a passphrase
|
||||||
func EncryptPrivateKey(key data.PrivateKey, passphrase string) ([]byte, error) {
|
func EncryptPrivateKey(key data.PrivateKey, role, passphrase string) ([]byte, error) {
|
||||||
bt, err := blockType(key)
|
bt, err := blockType(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -440,6 +451,11 @@ func EncryptPrivateKey(key data.PrivateKey, passphrase string) ([]byte, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if encryptedPEMBlock.Headers == nil {
|
||||||
|
return nil, fmt.Errorf("unable to encrypt key - invalid PEM file produced")
|
||||||
|
}
|
||||||
|
encryptedPEMBlock.Headers["role"] = role
|
||||||
|
|
||||||
return pem.EncodeToMemory(encryptedPEMBlock), nil
|
return pem.EncodeToMemory(encryptedPEMBlock), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -471,12 +487,8 @@ func CertsToKeys(certs []*x509.Certificate) map[string]data.PublicKey {
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCertificate returns an X509 Certificate following a template, given a GUN.
|
// NewCertificate returns an X509 Certificate following a template, given a GUN and validity interval.
|
||||||
func NewCertificate(gun string) (*x509.Certificate, error) {
|
func NewCertificate(gun string, startTime, endTime time.Time) (*x509.Certificate, error) {
|
||||||
notBefore := time.Now()
|
|
||||||
// Certificates will expire in 10 years
|
|
||||||
notAfter := notBefore.Add(time.Hour * 24 * 365 * 10)
|
|
||||||
|
|
||||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
|
|
||||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
|
@ -489,8 +501,8 @@ func NewCertificate(gun string) (*x509.Certificate, error) {
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
CommonName: gun,
|
CommonName: gun,
|
||||||
},
|
},
|
||||||
NotBefore: notBefore,
|
NotBefore: startTime,
|
||||||
NotAfter: notAfter,
|
NotAfter: endTime,
|
||||||
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/notary/passphrase"
|
"github.com/docker/notary/passphrase"
|
||||||
|
|
@ -217,7 +218,9 @@ func addECDSAKey(
|
||||||
|
|
||||||
ecdsaPrivKeyD := ensurePrivateKeySize(ecdsaPrivKey.D.Bytes())
|
ecdsaPrivKeyD := ensurePrivateKeySize(ecdsaPrivKey.D.Bytes())
|
||||||
|
|
||||||
template, err := trustmanager.NewCertificate(role)
|
// Hard-coded policy: the generated certificate expires in 10 years.
|
||||||
|
startTime := time.Now()
|
||||||
|
template, err := trustmanager.NewCertificate(role, startTime, startTime.AddDate(10, 0, 0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create the certificate template: %v", err)
|
return fmt.Errorf("failed to create the certificate template: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -483,6 +486,12 @@ func yubiListKeys(ctx IPKCS11Ctx, session pkcs11.SessionHandle) (keys map[string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we found nothing
|
||||||
|
if cert == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var ecdsaPubKey *ecdsa.PublicKey
|
var ecdsaPubKey *ecdsa.PublicKey
|
||||||
switch cert.PublicKeyAlgorithm {
|
switch cert.PublicKeyAlgorithm {
|
||||||
case x509.ECDSA:
|
case x509.ECDSA:
|
||||||
|
|
@ -790,28 +799,31 @@ func SetupHSMEnv(libraryPath string, libLoader pkcs11LibLoader) (
|
||||||
IPKCS11Ctx, pkcs11.SessionHandle, error) {
|
IPKCS11Ctx, pkcs11.SessionHandle, error) {
|
||||||
|
|
||||||
if libraryPath == "" {
|
if libraryPath == "" {
|
||||||
return nil, 0, errors.New("No library found.")
|
return nil, 0, fmt.Errorf("no library found.")
|
||||||
}
|
}
|
||||||
p := libLoader(libraryPath)
|
p := libLoader(libraryPath)
|
||||||
|
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil, 0, errors.New("Failed to init library")
|
return nil, 0, fmt.Errorf("failed to load library %s", libraryPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.Initialize(); err != nil {
|
if err := p.Initialize(); err != nil {
|
||||||
defer finalizeAndDestroy(p)
|
defer finalizeAndDestroy(p)
|
||||||
return nil, 0, fmt.Errorf("Initialize error %s", err.Error())
|
return nil, 0, fmt.Errorf(
|
||||||
|
"found library %s, but initialize error %s", libraryPath, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
slots, err := p.GetSlotList(true)
|
slots, err := p.GetSlotList(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
defer finalizeAndDestroy(p)
|
defer finalizeAndDestroy(p)
|
||||||
return nil, 0, fmt.Errorf("Failed to list HSM slots %s", err)
|
return nil, 0, fmt.Errorf(
|
||||||
|
"loaded library %s, but failed to list HSM slots %s", libraryPath, err)
|
||||||
}
|
}
|
||||||
// Check to see if we got any slots from the HSM.
|
// Check to see if we got any slots from the HSM.
|
||||||
if len(slots) < 1 {
|
if len(slots) < 1 {
|
||||||
defer finalizeAndDestroy(p)
|
defer finalizeAndDestroy(p)
|
||||||
return nil, 0, fmt.Errorf("No HSM Slots found")
|
return nil, 0, fmt.Errorf(
|
||||||
|
"loaded library %s, but no HSM slots found", libraryPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CKF_SERIAL_SESSION: TRUE if cryptographic functions are performed in serial with the application; FALSE if the functions may be performed in parallel with the application.
|
// CKF_SERIAL_SESSION: TRUE if cryptographic functions are performed in serial with the application; FALSE if the functions may be performed in parallel with the application.
|
||||||
|
|
@ -819,9 +831,12 @@ func SetupHSMEnv(libraryPath string, libLoader pkcs11LibLoader) (
|
||||||
session, err := p.OpenSession(slots[0], pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION)
|
session, err := p.OpenSession(slots[0], pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
defer cleanup(p, session)
|
defer cleanup(p, session)
|
||||||
return nil, 0, fmt.Errorf("Failed to Start Session with HSM %s", err)
|
return nil, 0, fmt.Errorf(
|
||||||
|
"loaded library %s, but failed to start session with HSM %s",
|
||||||
|
libraryPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Initialized PKCS11 library %s and started HSM session", libraryPath)
|
||||||
return p, session, nil
|
return p, session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
|
@ -82,7 +81,7 @@ func (c *Client) update() error {
|
||||||
// In this instance the root has not expired base on time, but is
|
// In this instance the root has not expired base on time, but is
|
||||||
// expired based on the snapshot dictating a new root has been produced.
|
// expired based on the snapshot dictating a new root has been produced.
|
||||||
logrus.Debug(err)
|
logrus.Debug(err)
|
||||||
return tuf.ErrLocalRootExpired{}
|
return err
|
||||||
}
|
}
|
||||||
// will always need top level targets at a minimum
|
// will always need top level targets at a minimum
|
||||||
err = c.downloadTargets("targets")
|
err = c.downloadTargets("targets")
|
||||||
|
|
@ -369,34 +368,52 @@ func (c *Client) downloadSnapshot() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloadTargets is responsible for downloading any targets file
|
// downloadTargets downloads all targets and delegated targets for the repository.
|
||||||
// including delegates roles.
|
// It uses a pre-order tree traversal as it's necessary to download parents first
|
||||||
|
// to obtain the keys to validate children.
|
||||||
func (c *Client) downloadTargets(role string) error {
|
func (c *Client) downloadTargets(role string) error {
|
||||||
role = data.RoleName(role) // this will really only do something for base targets role
|
stack := utils.NewStack()
|
||||||
if c.local.Snapshot == nil {
|
stack.Push(role)
|
||||||
return ErrMissingMeta{role: role}
|
for !stack.Empty() {
|
||||||
}
|
role, err := stack.PopString()
|
||||||
snap := c.local.Snapshot.Signed
|
if err != nil {
|
||||||
root := c.local.Root.Signed
|
return err
|
||||||
r := c.keysDB.GetRole(role)
|
}
|
||||||
if r == nil {
|
role = data.RoleName(role) // this will really only do something for base targets role
|
||||||
return fmt.Errorf("Invalid role: %s", role)
|
if c.local.Snapshot == nil {
|
||||||
}
|
return ErrMissingMeta{role: role}
|
||||||
keyIDs := r.KeyIDs
|
}
|
||||||
s, err := c.getTargetsFile(role, keyIDs, snap.Meta, root.ConsistentSnapshot, r.Threshold)
|
snap := c.local.Snapshot.Signed
|
||||||
if err != nil {
|
root := c.local.Root.Signed
|
||||||
logrus.Error("Error getting targets file:", err)
|
r := c.keysDB.GetRole(role)
|
||||||
return err
|
if r == nil {
|
||||||
}
|
return fmt.Errorf("Invalid role: %s", role)
|
||||||
t, err := data.TargetsFromSigned(s)
|
}
|
||||||
if err != nil {
|
keyIDs := r.KeyIDs
|
||||||
return err
|
s, err := c.getTargetsFile(role, keyIDs, snap.Meta, root.ConsistentSnapshot, r.Threshold)
|
||||||
}
|
if err != nil {
|
||||||
err = c.local.SetTargets(role, t)
|
if _, ok := err.(ErrMissingMeta); ok && role != data.CanonicalTargetsRole {
|
||||||
if err != nil {
|
// if the role meta hasn't been published,
|
||||||
return err
|
// that's ok, continue
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
logrus.Error("Error getting targets file:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t, err := data.TargetsFromSigned(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = c.local.SetTargets(role, t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// push delegated roles contained in the targets file onto the stack
|
||||||
|
for _, r := range t.Signed.Delegations.Roles {
|
||||||
|
stack.Push(r.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -482,17 +499,18 @@ func (c Client) getTargetsFile(role string, keyIDs []string, snapshotMeta data.F
|
||||||
// if we error when setting meta, we should continue.
|
// if we error when setting meta, we should continue.
|
||||||
err = c.cache.SetMeta(role, raw)
|
err = c.cache.SetMeta(role, raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Failed to write snapshot to local cache: %s", err.Error())
|
logrus.Errorf("Failed to write %s to local cache: %s", role, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoleTargetsPath generates the appropriate filename for the targets file,
|
// RoleTargetsPath generates the appropriate HTTP URL for the targets file,
|
||||||
// based on whether the repo is marked as consistent.
|
// based on whether the repo is marked as consistent.
|
||||||
func (c Client) RoleTargetsPath(role string, hashSha256 string, consistent bool) (string, error) {
|
func (c Client) RoleTargetsPath(role string, hashSha256 string, consistent bool) (string, error) {
|
||||||
if consistent {
|
if consistent {
|
||||||
dir := filepath.Dir(role)
|
// 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, "/") {
|
if strings.Contains(role, "/") {
|
||||||
lastSlashIdx := strings.LastIndex(role, "/")
|
lastSlashIdx := strings.LastIndex(role, "/")
|
||||||
role = role[lastSlashIdx+1:]
|
role = role[lastSlashIdx+1:]
|
||||||
|
|
@ -505,42 +523,41 @@ func (c Client) RoleTargetsPath(role string, hashSha256 string, consistent bool)
|
||||||
return role, nil
|
return role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TargetMeta ensures the repo is up to date, downloading the minimum
|
// TargetMeta ensures the repo is up to date. It assumes downloadTargets
|
||||||
// necessary metadata files
|
// has already downloaded all delegated roles
|
||||||
func (c Client) TargetMeta(path string) (*data.FileMeta, error) {
|
func (c Client) TargetMeta(role, path string, excludeRoles ...string) (*data.FileMeta, string) {
|
||||||
c.Update()
|
excl := make(map[string]bool)
|
||||||
var meta *data.FileMeta
|
for _, r := range excludeRoles {
|
||||||
|
excl[r] = true
|
||||||
|
}
|
||||||
|
|
||||||
pathDigest := sha256.Sum256([]byte(path))
|
pathDigest := sha256.Sum256([]byte(path))
|
||||||
pathHex := hex.EncodeToString(pathDigest[:])
|
pathHex := hex.EncodeToString(pathDigest[:])
|
||||||
|
|
||||||
// FIFO list of targets delegations to inspect for target
|
// FIFO list of targets delegations to inspect for target
|
||||||
roles := []string{data.ValidRoles["targets"]}
|
roles := []string{role}
|
||||||
var role string
|
var (
|
||||||
|
meta *data.FileMeta
|
||||||
|
curr string
|
||||||
|
)
|
||||||
for len(roles) > 0 {
|
for len(roles) > 0 {
|
||||||
// have to do these lines here because of order of execution in for statement
|
// have to do these lines here because of order of execution in for statement
|
||||||
role = roles[0]
|
curr = roles[0]
|
||||||
roles = roles[1:]
|
roles = roles[1:]
|
||||||
|
|
||||||
// Download the target role file if necessary
|
meta = c.local.TargetMeta(curr, path)
|
||||||
err := c.downloadTargets(role)
|
|
||||||
if err != nil {
|
|
||||||
// as long as we find a valid target somewhere we're happy.
|
|
||||||
// continue and search other delegated roles if any
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
meta = c.local.TargetMeta(role, path)
|
|
||||||
if meta != nil {
|
if meta != nil {
|
||||||
// we found the target!
|
// we found the target!
|
||||||
return meta, nil
|
return meta, curr
|
||||||
}
|
}
|
||||||
delegations := c.local.TargetDelegations(role, path, pathHex)
|
delegations := c.local.TargetDelegations(curr, path, pathHex)
|
||||||
for _, d := range delegations {
|
for _, d := range delegations {
|
||||||
roles = append(roles, d.Name)
|
if !excl[d.Name] {
|
||||||
|
roles = append(roles, d.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return meta, nil
|
return meta, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadTarget downloads the target to dst from the remote
|
// DownloadTarget downloads the target to dst from the remote
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,15 @@ func (ks *KeyList) UnmarshalJSON(data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IDs generates a list of the hex encoded key IDs in the KeyList
|
||||||
|
func (ks KeyList) IDs() []string {
|
||||||
|
keyIDs := make([]string, 0, len(ks))
|
||||||
|
for _, k := range ks {
|
||||||
|
keyIDs = append(keyIDs, k.ID())
|
||||||
|
}
|
||||||
|
return keyIDs
|
||||||
|
}
|
||||||
|
|
||||||
func typedPublicKey(tk tufKey) PublicKey {
|
func typedPublicKey(tk tufKey) PublicKey {
|
||||||
switch tk.Algorithm() {
|
switch tk.Algorithm() {
|
||||||
case ECDSAKey:
|
case ECDSAKey:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -24,15 +26,28 @@ var ValidRoles = map[string]string{
|
||||||
CanonicalTimestampRole: CanonicalTimestampRole,
|
CanonicalTimestampRole: CanonicalTimestampRole,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrNoSuchRole indicates the roles doesn't exist
|
||||||
|
type ErrNoSuchRole struct {
|
||||||
|
Role string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrNoSuchRole) Error() string {
|
||||||
|
return fmt.Sprintf("role does not exist: %s", e.Role)
|
||||||
|
}
|
||||||
|
|
||||||
// ErrInvalidRole represents an error regarding a role. Typically
|
// ErrInvalidRole represents an error regarding a role. Typically
|
||||||
// something like a role for which sone of the public keys were
|
// something like a role for which sone of the public keys were
|
||||||
// not found in the TUF repo.
|
// not found in the TUF repo.
|
||||||
type ErrInvalidRole struct {
|
type ErrInvalidRole struct {
|
||||||
Role string
|
Role string
|
||||||
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ErrInvalidRole) Error() string {
|
func (e ErrInvalidRole) Error() string {
|
||||||
return fmt.Sprintf("tuf: invalid role %s", e.Role)
|
if e.Reason != "" {
|
||||||
|
return fmt.Sprintf("tuf: invalid role %s. %s", e.Role, e.Reason)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("tuf: invalid role %s.", e.Role)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetValidRoles is a utility function to override some or all of the roles
|
// SetValidRoles is a utility function to override some or all of the roles
|
||||||
|
|
@ -85,10 +100,11 @@ func ValidRole(name string) bool {
|
||||||
if v, ok := ValidRoles[name]; ok {
|
if v, ok := ValidRoles[name]; ok {
|
||||||
return name == v
|
return name == v
|
||||||
}
|
}
|
||||||
targetsBase := fmt.Sprintf("%s/", ValidRoles[CanonicalTargetsRole])
|
|
||||||
if strings.HasPrefix(name, targetsBase) {
|
if IsDelegation(name) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range ValidRoles {
|
for _, v := range ValidRoles {
|
||||||
if name == v {
|
if name == v {
|
||||||
return true
|
return true
|
||||||
|
|
@ -97,6 +113,24 @@ func ValidRole(name string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsDelegation checks if the role is a delegation or a root role
|
||||||
|
func IsDelegation(role string) bool {
|
||||||
|
targetsBase := ValidRoles[CanonicalTargetsRole] + "/"
|
||||||
|
|
||||||
|
delegationRegexp := regexp.MustCompile("^[-a-z0-9_/]+$")
|
||||||
|
whitelistedChars := delegationRegexp.MatchString(role)
|
||||||
|
|
||||||
|
// Limit size of full role string to 255 chars for db column size limit
|
||||||
|
correctLength := len(role) < 256
|
||||||
|
|
||||||
|
// Removes ., .., extra slashes, and trailing slash
|
||||||
|
isClean := path.Clean(role) == role
|
||||||
|
return strings.HasPrefix(role, targetsBase) &&
|
||||||
|
whitelistedChars &&
|
||||||
|
correctLength &&
|
||||||
|
isClean
|
||||||
|
}
|
||||||
|
|
||||||
// RootRole is a cut down role as it appears in the root.json
|
// RootRole is a cut down role as it appears in the root.json
|
||||||
type RootRole struct {
|
type RootRole struct {
|
||||||
KeyIDs []string `json:"keyids"`
|
KeyIDs []string `json:"keyids"`
|
||||||
|
|
@ -115,7 +149,18 @@ type Role struct {
|
||||||
// NewRole creates a new Role object from the given parameters
|
// NewRole creates a new Role object from the given parameters
|
||||||
func NewRole(name string, threshold int, keyIDs, paths, pathHashPrefixes []string) (*Role, error) {
|
func NewRole(name string, threshold int, keyIDs, paths, pathHashPrefixes []string) (*Role, error) {
|
||||||
if len(paths) > 0 && len(pathHashPrefixes) > 0 {
|
if len(paths) > 0 && len(pathHashPrefixes) > 0 {
|
||||||
return nil, ErrInvalidRole{Role: name}
|
return nil, ErrInvalidRole{
|
||||||
|
Role: name,
|
||||||
|
Reason: "roles may not have both Paths and PathHashPrefixes",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if IsDelegation(name) {
|
||||||
|
if len(paths) == 0 && len(pathHashPrefixes) == 0 {
|
||||||
|
return nil, ErrInvalidRole{
|
||||||
|
Role: name,
|
||||||
|
Reason: "roles with no Paths and no PathHashPrefixes will never be able to publish content",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if threshold < 1 {
|
if threshold < 1 {
|
||||||
return nil, ErrInvalidRole{Role: name}
|
return nil, ErrInvalidRole{Role: name}
|
||||||
|
|
@ -173,6 +218,78 @@ func (r Role) CheckPrefixes(hash string) bool {
|
||||||
|
|
||||||
// IsDelegation checks if the role is a delegation or a root role
|
// IsDelegation checks if the role is a delegation or a root role
|
||||||
func (r Role) IsDelegation() bool {
|
func (r Role) IsDelegation() bool {
|
||||||
targetsBase := fmt.Sprintf("%s/", ValidRoles[CanonicalTargetsRole])
|
return IsDelegation(r.Name)
|
||||||
return strings.HasPrefix(r.Name, targetsBase)
|
}
|
||||||
|
|
||||||
|
// AddKeys merges the ids into the current list of role key ids
|
||||||
|
func (r *Role) AddKeys(ids []string) {
|
||||||
|
r.KeyIDs = mergeStrSlices(r.KeyIDs, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPaths merges the paths into the current list of role paths
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemovePaths removes the paths from the current list of role paths
|
||||||
|
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 {
|
||||||
|
have[e] = true
|
||||||
|
}
|
||||||
|
merged := make([]string, len(orig), len(orig)+len(new))
|
||||||
|
copy(merged, orig)
|
||||||
|
for _, e := range new {
|
||||||
|
if !have[e] {
|
||||||
|
merged = append(merged, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
func subtractStrSlices(orig, remove []string) []string {
|
||||||
|
kill := make(map[string]bool)
|
||||||
|
for _, e := range remove {
|
||||||
|
kill[e] = true
|
||||||
|
}
|
||||||
|
var keep []string
|
||||||
|
for _, e := range orig {
|
||||||
|
if !kill[e] {
|
||||||
|
keep = append(keep, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keep
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,15 @@ func (sp *SignedSnapshot) AddMeta(role string, meta FileMeta) {
|
||||||
sp.Dirty = true
|
sp.Dirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
if _, ok := sp.Signed.Meta[role]; ok {
|
||||||
|
delete(sp.Signed.Meta, role)
|
||||||
|
sp.Dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SnapshotFromSigned fully unpacks a Signed object into a SignedSnapshot
|
// SnapshotFromSigned fully unpacks a Signed object into a SignedSnapshot
|
||||||
func SnapshotFromSigned(s *Signed) (*SignedSnapshot, error) {
|
func SnapshotFromSigned(s *Signed) (*SignedSnapshot, error) {
|
||||||
sp := Snapshot{}
|
sp := Snapshot{}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package data
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/jfrazelle/go/canonical/json"
|
"github.com/jfrazelle/go/canonical/json"
|
||||||
)
|
)
|
||||||
|
|
@ -88,7 +89,7 @@ func (t *SignedTargets) AddTarget(path string, meta FileMeta) {
|
||||||
// ensuring the keys either already exist, or are added to the map
|
// ensuring the keys either already exist, or are added to the map
|
||||||
// of delegation keys
|
// of delegation keys
|
||||||
func (t *SignedTargets) AddDelegation(role *Role, keys []*PublicKey) error {
|
func (t *SignedTargets) AddDelegation(role *Role, keys []*PublicKey) error {
|
||||||
return nil
|
return errors.New("Not Implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToSigned partially serializes a SignedTargets for further signing
|
// ToSigned partially serializes a SignedTargets for further signing
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,10 @@ func (e *Ed25519) RemoveKey(keyID string) error {
|
||||||
// ListKeys returns the list of keys IDs for the role
|
// ListKeys returns the list of keys IDs for the role
|
||||||
func (e *Ed25519) ListKeys(role string) []string {
|
func (e *Ed25519) ListKeys(role string) []string {
|
||||||
keyIDs := make([]string, 0, len(e.keys))
|
keyIDs := make([]string, 0, len(e.keys))
|
||||||
for id := range e.keys {
|
for id, edCryptoKey := range e.keys {
|
||||||
keyIDs = append(keyIDs, id)
|
if edCryptoKey.role == role {
|
||||||
|
keyIDs = append(keyIDs, id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return keyIDs
|
return keyIDs
|
||||||
}
|
}
|
||||||
|
|
@ -61,23 +63,6 @@ func (e *Ed25519) ListAllKeys() map[string]string {
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign generates an Ed25519 signature over the data
|
|
||||||
func (e *Ed25519) Sign(keyIDs []string, toSign []byte) ([]data.Signature, error) {
|
|
||||||
signatures := make([]data.Signature, 0, len(keyIDs))
|
|
||||||
for _, keyID := range keyIDs {
|
|
||||||
priv := [ed25519.PrivateKeySize]byte{}
|
|
||||||
copy(priv[:], e.keys[keyID].privKey.Private())
|
|
||||||
sig := ed25519.Sign(&priv, toSign)
|
|
||||||
signatures = append(signatures, data.Signature{
|
|
||||||
KeyID: keyID,
|
|
||||||
Method: data.EDDSASignature,
|
|
||||||
Signature: sig[:],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return signatures, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create generates a new key and returns the public part
|
// Create generates a new key and returns the public part
|
||||||
func (e *Ed25519) Create(role, algorithm string) (data.PublicKey, error) {
|
func (e *Ed25519) Create(role, algorithm string) (data.PublicKey, error) {
|
||||||
if algorithm != data.ED25519Key {
|
if algorithm != data.ED25519Key {
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,10 @@ func (e ErrInvalidKeyLength) Error() string {
|
||||||
|
|
||||||
// ErrNoKeys indicates no signing keys were found when trying to sign
|
// ErrNoKeys indicates no signing keys were found when trying to sign
|
||||||
type ErrNoKeys struct {
|
type ErrNoKeys struct {
|
||||||
keyIDs []string
|
KeyIDs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ErrNoKeys) Error() string {
|
func (e ErrNoKeys) Error() string {
|
||||||
return fmt.Sprintf("could not find necessary signing keys, at least one of these keys must be available: %s",
|
return fmt.Sprintf("could not find necessary signing keys, at least one of these keys must be available: %s",
|
||||||
strings.Join(e.keyIDs, ", "))
|
strings.Join(e.KeyIDs, ", "))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SigningService defines the necessary functions to determine
|
|
||||||
// if a user is able to sign with a key, and to perform signing.
|
|
||||||
type SigningService interface {
|
|
||||||
// Sign takes a slice of keyIDs and a piece of data to sign
|
|
||||||
// and returns a slice of signatures and an error
|
|
||||||
Sign(keyIDs []string, data []byte) ([]data.Signature, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// KeyService provides management of keys locally. It will never
|
// KeyService provides management of keys locally. It will never
|
||||||
// accept or provide private keys. Communication between the KeyService
|
// accept or provide private keys. Communication between the KeyService
|
||||||
// and a SigningService happen behind the Create function.
|
// and a SigningService happen behind the Create function.
|
||||||
|
|
@ -44,10 +36,9 @@ type KeyService interface {
|
||||||
ImportRootKey(source io.Reader) error
|
ImportRootKey(source io.Reader) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// CryptoService defines a unified Signing and Key Service as this
|
// CryptoService is deprecated and all instances of its use should be
|
||||||
// will be most useful for most applications.
|
// replaced with KeyService
|
||||||
type CryptoService interface {
|
type CryptoService interface {
|
||||||
SigningService
|
|
||||||
KeyService
|
KeyService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func Sign(service CryptoService, s *data.Signed, keys ...data.PublicKey) error {
|
||||||
|
|
||||||
// Check to ensure we have at least one signing key
|
// Check to ensure we have at least one signing key
|
||||||
if len(privKeys) == 0 {
|
if len(privKeys) == 0 {
|
||||||
return ErrNoKeys{keyIDs: ids}
|
return ErrNoKeys{KeyIDs: ids}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do signing and generate list of signatures
|
// Do signing and generate list of signatures
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package store
|
package store
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
// ErrMetaNotFound indicates we did not find a particular piece
|
// ErrMetaNotFound indicates we did not find a particular piece
|
||||||
// of metadata in the store
|
// of metadata in the store
|
||||||
type ErrMetaNotFound struct{}
|
type ErrMetaNotFound struct {
|
||||||
|
Role string
|
||||||
|
}
|
||||||
|
|
||||||
func (err ErrMetaNotFound) Error() string {
|
func (err ErrMetaNotFound) Error() string {
|
||||||
return "no trust data available"
|
return fmt.Sprintf("%s trust data unavailable", err.Role)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ func (f *FilesystemStore) GetMeta(name string, size int64) ([]byte, error) {
|
||||||
path := filepath.Join(f.metaDir, fileName)
|
path := filepath.Join(f.metaDir, fileName)
|
||||||
meta, err := ioutil.ReadFile(path)
|
meta, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = ErrMetaNotFound{Role: name}
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return meta, nil
|
return meta, nil
|
||||||
|
|
@ -65,7 +68,18 @@ func (f *FilesystemStore) SetMultiMeta(metas map[string][]byte) error {
|
||||||
func (f *FilesystemStore) SetMeta(name string, meta []byte) error {
|
func (f *FilesystemStore) SetMeta(name string, meta []byte) error {
|
||||||
fileName := fmt.Sprintf("%s.%s", name, f.metaExtension)
|
fileName := fmt.Sprintf("%s.%s", name, f.metaExtension)
|
||||||
path := filepath.Join(f.metaDir, fileName)
|
path := filepath.Join(f.metaDir, fileName)
|
||||||
if err := ioutil.WriteFile(path, meta, 0600); err != nil {
|
|
||||||
|
// Ensures the parent directories of the file we are about to write exist
|
||||||
|
err := os.MkdirAll(filepath.Dir(path), 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if something already exists, just delete it and re-write it
|
||||||
|
os.RemoveAll(path)
|
||||||
|
|
||||||
|
// Write the file to disk
|
||||||
|
if err = ioutil.WriteFile(path, meta, 0600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,18 @@
|
||||||
|
// A Store that can fetch and set metadata on a remote server.
|
||||||
|
// Some API constraints:
|
||||||
|
// - Response bodies for error codes should be unmarshallable as:
|
||||||
|
// {"errors": [{..., "detail": <serialized validation error>}]}
|
||||||
|
// else validation error details, etc. will be unparsable. The errors
|
||||||
|
// should have a github.com/docker/notary/tuf/validation/SerializableError
|
||||||
|
// in the Details field.
|
||||||
|
// If writing your own server, please have a look at
|
||||||
|
// github.com/docker/distribution/registry/api/errcode
|
||||||
|
|
||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -12,6 +23,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/notary/tuf/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrServerUnavailable indicates an error from the server. code allows us to
|
// ErrServerUnavailable indicates an error from the server. code allows us to
|
||||||
|
|
@ -21,7 +33,7 @@ type ErrServerUnavailable struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrServerUnavailable) Error() string {
|
func (err ErrServerUnavailable) Error() string {
|
||||||
return fmt.Sprintf("Unable to reach trust server at this time: %d.", err.code)
|
return fmt.Sprintf("unable to reach trust server at this time: %d.", err.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrMaliciousServer indicates the server returned a response that is highly suspected
|
// ErrMaliciousServer indicates the server returned a response that is highly suspected
|
||||||
|
|
@ -30,7 +42,20 @@ func (err ErrServerUnavailable) Error() string {
|
||||||
type ErrMaliciousServer struct{}
|
type ErrMaliciousServer struct{}
|
||||||
|
|
||||||
func (err ErrMaliciousServer) Error() string {
|
func (err ErrMaliciousServer) Error() string {
|
||||||
return "Trust server returned a bad response."
|
return "trust server returned a bad response."
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidOperation indicates that the server returned a 400 response and
|
||||||
|
// propagate any body we received.
|
||||||
|
type ErrInvalidOperation struct {
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrInvalidOperation) Error() string {
|
||||||
|
if err.msg != "" {
|
||||||
|
return fmt.Sprintf("trust server rejected operation: %s", err.msg)
|
||||||
|
}
|
||||||
|
return "trust server rejected operation."
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPStore manages pulling and pushing metadata from and to a remote
|
// HTTPStore manages pulling and pushing metadata from and to a remote
|
||||||
|
|
@ -70,6 +95,42 @@ func NewHTTPStore(baseURL, metaPrefix, metaExtension, targetsPrefix, keyExtensio
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tryUnmarshalError(resp *http.Response, defaultError error) error {
|
||||||
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return defaultError
|
||||||
|
}
|
||||||
|
var parsedErrors struct {
|
||||||
|
Errors []struct {
|
||||||
|
Detail validation.SerializableError `json:"detail"`
|
||||||
|
} `json:"errors"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(bodyBytes, &parsedErrors); err != nil {
|
||||||
|
return defaultError
|
||||||
|
}
|
||||||
|
if len(parsedErrors.Errors) != 1 {
|
||||||
|
return defaultError
|
||||||
|
}
|
||||||
|
err = parsedErrors.Errors[0].Detail.Error
|
||||||
|
if err == nil {
|
||||||
|
return defaultError
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func translateStatusToError(resp *http.Response) error {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
return nil
|
||||||
|
case http.StatusNotFound:
|
||||||
|
return ErrMetaNotFound{}
|
||||||
|
case http.StatusBadRequest:
|
||||||
|
return tryUnmarshalError(resp, ErrInvalidOperation{})
|
||||||
|
default:
|
||||||
|
return ErrServerUnavailable{code: resp.StatusCode}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetMeta downloads the named meta file with the given size. A short body
|
// 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,
|
// is acceptable because in the case of timestamp.json, the size is a cap,
|
||||||
// not an exact length.
|
// not an exact length.
|
||||||
|
|
@ -87,11 +148,9 @@ func (s HTTPStore) GetMeta(name string, size int64) ([]byte, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if err := translateStatusToError(resp); err != nil {
|
||||||
return nil, ErrMetaNotFound{}
|
|
||||||
} else if resp.StatusCode != http.StatusOK {
|
|
||||||
logrus.Debugf("received HTTP status %d when requesting %s.", resp.StatusCode, name)
|
logrus.Debugf("received HTTP status %d when requesting %s.", resp.StatusCode, name)
|
||||||
return nil, ErrServerUnavailable{code: resp.StatusCode}
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.ContentLength > size {
|
if resp.ContentLength > size {
|
||||||
return nil, ErrMaliciousServer{}
|
return nil, ErrMaliciousServer{}
|
||||||
|
|
@ -120,12 +179,31 @@ func (s HTTPStore) SetMeta(name string, blob []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
return translateStatusToError(resp)
|
||||||
return ErrMetaNotFound{}
|
}
|
||||||
} else if resp.StatusCode != http.StatusOK {
|
|
||||||
return ErrServerUnavailable{code: resp.StatusCode}
|
// NewMultiPartMetaRequest builds a request with the provided metadata updates
|
||||||
|
// in multipart form
|
||||||
|
func NewMultiPartMetaRequest(url string, metas map[string][]byte) (*http.Request, error) {
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
for role, blob := range metas {
|
||||||
|
part, err := writer.CreateFormFile("files", role)
|
||||||
|
_, err = io.Copy(part, bytes.NewBuffer(blob))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
err := writer.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMultiMeta does a single batch upload of multiple pieces of TUF metadata.
|
// SetMultiMeta does a single batch upload of multiple pieces of TUF metadata.
|
||||||
|
|
@ -136,21 +214,7 @@ func (s HTTPStore) SetMultiMeta(metas map[string][]byte) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
body := &bytes.Buffer{}
|
req, err := NewMultiPartMetaRequest(url.String(), metas)
|
||||||
writer := multipart.NewWriter(body)
|
|
||||||
for role, blob := range metas {
|
|
||||||
part, err := writer.CreateFormFile("files", role)
|
|
||||||
_, err = io.Copy(part, bytes.NewBuffer(blob))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = writer.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest("POST", url.String(), body)
|
|
||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -159,12 +223,7 @@ func (s HTTPStore) SetMultiMeta(metas map[string][]byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
return translateStatusToError(resp)
|
||||||
return ErrMetaNotFound{}
|
|
||||||
} else if resp.StatusCode != http.StatusOK {
|
|
||||||
return ErrServerUnavailable{code: resp.StatusCode}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s HTTPStore) buildMetaURL(name string) (*url.URL, error) {
|
func (s HTTPStore) buildMetaURL(name string) (*url.URL, error) {
|
||||||
|
|
@ -212,10 +271,8 @@ func (s HTTPStore) GetTarget(path string) (io.ReadCloser, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if err := translateStatusToError(resp); err != nil {
|
||||||
return nil, ErrMetaNotFound{}
|
return nil, err
|
||||||
} else if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, ErrServerUnavailable{code: resp.StatusCode}
|
|
||||||
}
|
}
|
||||||
return resp.Body, nil
|
return resp.Body, nil
|
||||||
}
|
}
|
||||||
|
|
@ -235,10 +292,8 @@ func (s HTTPStore) GetKey(role string) ([]byte, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if err := translateStatusToError(resp); err != nil {
|
||||||
return nil, ErrMetaNotFound{}
|
return nil, err
|
||||||
} else if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, ErrServerUnavailable{code: resp.StatusCode}
|
|
||||||
}
|
}
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -99,8 +99,24 @@ func (tr *Repo) AddBaseKeys(role string, keys ...data.PublicKey) error {
|
||||||
}
|
}
|
||||||
tr.keysDB.AddRole(r)
|
tr.keysDB.AddRole(r)
|
||||||
tr.Root.Dirty = true
|
tr.Root.Dirty = true
|
||||||
return nil
|
|
||||||
|
|
||||||
|
// also, whichever role was switched out needs to be re-signed
|
||||||
|
// root has already been marked dirty
|
||||||
|
switch role {
|
||||||
|
case data.CanonicalSnapshotRole:
|
||||||
|
if tr.Snapshot != nil {
|
||||||
|
tr.Snapshot.Dirty = true
|
||||||
|
}
|
||||||
|
case data.CanonicalTargetsRole:
|
||||||
|
if target, ok := tr.Targets[data.CanonicalTargetsRole]; ok {
|
||||||
|
target.Dirty = true
|
||||||
|
}
|
||||||
|
case data.CanonicalTimestampRole:
|
||||||
|
if tr.Timestamp != nil {
|
||||||
|
tr.Timestamp.Dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplaceBaseKeys is used to replace all keys for the given role with the new keys
|
// ReplaceBaseKeys is used to replace all keys for the given role with the new keys
|
||||||
|
|
@ -157,24 +173,58 @@ func (tr *Repo) RemoveBaseKeys(role string, keyIDs ...string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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"}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
foundAt := utils.FindRoleIndex(p.Signed.Delegations.Roles, role)
|
||||||
|
if foundAt < 0 {
|
||||||
|
return nil, data.ErrNoSuchRole{Role: role}
|
||||||
|
}
|
||||||
|
return p.Signed.Delegations.Roles[foundAt], nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateDelegations updates the appropriate delegations, either adding
|
// UpdateDelegations updates the appropriate delegations, either adding
|
||||||
// a new delegation or updating an existing one. If keys are
|
// 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
|
// 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.
|
// there already), and the keys will be added to the targets file.
|
||||||
// The "before" argument specifies another role which this new role
|
func (tr *Repo) UpdateDelegations(role *data.Role, keys []data.PublicKey) error {
|
||||||
// will be added in front of (i.e. higher priority) in the delegation list.
|
|
||||||
// An empty before string indicates to add the role to the end of the
|
|
||||||
// delegation list.
|
|
||||||
// A new, empty, targets file will be created for the new role.
|
|
||||||
func (tr *Repo) UpdateDelegations(role *data.Role, keys []data.PublicKey, before string) error {
|
|
||||||
if !role.IsDelegation() || !role.IsValid() {
|
if !role.IsDelegation() || !role.IsValid() {
|
||||||
return data.ErrInvalidRole{Role: role.Name}
|
return data.ErrInvalidRole{Role: role.Name, Reason: "not a valid delegated role"}
|
||||||
}
|
}
|
||||||
parent := filepath.Dir(role.Name)
|
parent := path.Dir(role.Name)
|
||||||
|
|
||||||
|
if err := tr.VerifyCanSign(parent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the parent role's metadata
|
||||||
p, ok := tr.Targets[parent]
|
p, ok := tr.Targets[parent]
|
||||||
if !ok {
|
if !ok { // the parent targetfile may not exist yet - if not, then create it
|
||||||
return data.ErrInvalidRole{Role: role.Name}
|
var err error
|
||||||
|
p, err = tr.InitTargets(parent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
if !utils.StrSliceContains(role.KeyIDs, k.ID()) {
|
if !utils.StrSliceContains(role.KeyIDs, k.ID()) {
|
||||||
role.KeyIDs = append(role.KeyIDs, k.ID())
|
role.KeyIDs = append(role.KeyIDs, k.ID())
|
||||||
|
|
@ -183,24 +233,75 @@ func (tr *Repo) UpdateDelegations(role *data.Role, keys []data.PublicKey, before
|
||||||
tr.keysDB.AddKey(k)
|
tr.keysDB.AddKey(k)
|
||||||
}
|
}
|
||||||
|
|
||||||
i := -1
|
// if the role has fewer keys than the threshold, it
|
||||||
var r *data.Role
|
// will never be able to create a valid targets file
|
||||||
for i, r = range p.Signed.Delegations.Roles {
|
// and should be considered invalid.
|
||||||
if r.Name == role.Name {
|
if len(role.KeyIDs) < role.Threshold {
|
||||||
break
|
return data.ErrInvalidRole{Role: role.Name, Reason: "insufficient keys to meet threshold"}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if i >= 0 {
|
|
||||||
p.Signed.Delegations.Roles[i] = role
|
foundAt := utils.FindRoleIndex(p.Signed.Delegations.Roles, role.Name)
|
||||||
|
|
||||||
|
if foundAt >= 0 {
|
||||||
|
p.Signed.Delegations.Roles[foundAt] = role
|
||||||
} else {
|
} else {
|
||||||
p.Signed.Delegations.Roles = append(p.Signed.Delegations.Roles, role)
|
p.Signed.Delegations.Roles = append(p.Signed.Delegations.Roles, role)
|
||||||
}
|
}
|
||||||
|
// We've made a change to parent. Set it to dirty
|
||||||
p.Dirty = true
|
p.Dirty = true
|
||||||
|
|
||||||
roleTargets := data.NewTargets() // NewTargets always marked Dirty
|
// We don't actually want to create the new delegation metadata yet.
|
||||||
tr.Targets[role.Name] = roleTargets
|
// 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)
|
tr.keysDB.AddRole(role)
|
||||||
|
utils.RemoveUnusedKeys(p)
|
||||||
|
|
||||||
|
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"}
|
||||||
|
}
|
||||||
|
// the role variable must not be used past this assignment for safety
|
||||||
|
name := role.Name
|
||||||
|
|
||||||
|
parent := path.Dir(name)
|
||||||
|
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)
|
||||||
|
|
||||||
|
p, ok := tr.Targets[parent]
|
||||||
|
if !ok {
|
||||||
|
// if there is no parent metadata (the role exists though), then this
|
||||||
|
// is as good as done.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
foundAt := utils.FindRoleIndex(p.Signed.Delegations.Roles, name)
|
||||||
|
|
||||||
|
if foundAt >= 0 {
|
||||||
|
var roles []*data.Role
|
||||||
|
// slice out deleted role
|
||||||
|
roles = append(roles, p.Signed.Delegations.Roles[:foundAt]...)
|
||||||
|
if foundAt+1 < len(p.Signed.Delegations.Roles) {
|
||||||
|
roles = append(roles, p.Signed.Delegations.Roles[foundAt+1:]...)
|
||||||
|
}
|
||||||
|
p.Signed.Delegations.Roles = roles
|
||||||
|
|
||||||
|
utils.RemoveUnusedKeys(p)
|
||||||
|
|
||||||
|
p.Dirty = true
|
||||||
|
} // if the role wasn't found, it's a good as deleted
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -213,7 +314,7 @@ func (tr *Repo) InitRepo(consistent bool) error {
|
||||||
if err := tr.InitRoot(consistent); err != nil {
|
if err := tr.InitRoot(consistent); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := tr.InitTargets(); err != nil {
|
if _, err := tr.InitTargets(data.CanonicalTargetsRole); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := tr.InitSnapshot(); err != nil {
|
if err := tr.InitSnapshot(); err != nil {
|
||||||
|
|
@ -230,7 +331,7 @@ func (tr *Repo) InitRoot(consistent bool) error {
|
||||||
for _, r := range data.ValidRoles {
|
for _, r := range data.ValidRoles {
|
||||||
role := tr.keysDB.GetRole(r)
|
role := tr.keysDB.GetRole(r)
|
||||||
if role == nil {
|
if role == nil {
|
||||||
return data.ErrInvalidRole{Role: data.CanonicalRootRole}
|
return data.ErrInvalidRole{Role: data.CanonicalRootRole, Reason: "root role not initialized in key database"}
|
||||||
}
|
}
|
||||||
rootRoles[r] = &role.RootRole
|
rootRoles[r] = &role.RootRole
|
||||||
for _, kid := range role.KeyIDs {
|
for _, kid := range role.KeyIDs {
|
||||||
|
|
@ -248,20 +349,34 @@ func (tr *Repo) InitRoot(consistent bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitTargets initializes an empty targets
|
// InitTargets initializes an empty targets, and returns the new empty target
|
||||||
func (tr *Repo) InitTargets() error {
|
func (tr *Repo) InitTargets(role string) (*data.SignedTargets, error) {
|
||||||
|
r := data.Role{Name: role}
|
||||||
|
if !r.IsDelegation() && data.CanonicalRole(role) != data.CanonicalTargetsRole {
|
||||||
|
return nil, data.ErrInvalidRole{
|
||||||
|
Role: role,
|
||||||
|
Reason: fmt.Sprintf("role is not a valid targets role name: %s", role),
|
||||||
|
}
|
||||||
|
}
|
||||||
targets := data.NewTargets()
|
targets := data.NewTargets()
|
||||||
tr.Targets[data.ValidRoles["targets"]] = targets
|
tr.Targets[data.RoleName(role)] = targets
|
||||||
return nil
|
return targets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitSnapshot initializes a snapshot based on the current root and targets
|
// InitSnapshot initializes a snapshot based on the current root and targets
|
||||||
func (tr *Repo) InitSnapshot() error {
|
func (tr *Repo) InitSnapshot() error {
|
||||||
|
if tr.Root == nil {
|
||||||
|
return ErrNotLoaded{role: "root"}
|
||||||
|
}
|
||||||
root, err := tr.Root.ToSigned()
|
root, err := tr.Root.ToSigned()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
targets, err := tr.Targets[data.ValidRoles["targets"]].ToSigned()
|
|
||||||
|
if _, ok := tr.Targets[data.RoleName(data.CanonicalTargetsRole)]; !ok {
|
||||||
|
return ErrNotLoaded{role: "targets"}
|
||||||
|
}
|
||||||
|
targets, err := tr.Targets[data.RoleName(data.CanonicalTargetsRole)].ToSigned()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -403,19 +518,61 @@ func (tr Repo) FindTarget(path string) *data.FileMeta {
|
||||||
return walkTargets("targets")
|
return walkTargets("targets")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTargets will attempt to add the given targets specifically to
|
// VerifyCanSign returns nil if the role exists and we have at least one
|
||||||
// the directed role. If the user does not have the signing keys for the role
|
// signing key for the role, false otherwise. This does not check that we have
|
||||||
// the function will return an error and the full slice of targets.
|
// enough signing keys to meet the threshold, since we want to support the use
|
||||||
func (tr *Repo) AddTargets(role string, targets data.Files) (data.Files, error) {
|
// case of multiple signers for a role. It returns an error if the role doesn't
|
||||||
t, ok := tr.Targets[role]
|
// exist or if there are no signing keys.
|
||||||
if !ok {
|
func (tr *Repo) VerifyCanSign(roleName string) error {
|
||||||
return targets, data.ErrInvalidRole{Role: role}
|
role := tr.keysDB.GetRole(roleName)
|
||||||
|
if role == 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)
|
||||||
|
check := []string{keyID}
|
||||||
|
if err == nil {
|
||||||
|
check = append(check, canonicalID)
|
||||||
|
}
|
||||||
|
for _, id := range check {
|
||||||
|
p, _, err := tr.cryptoService.GetPrivateKey(id)
|
||||||
|
if err == nil && p != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return signed.ErrNoKeys{KeyIDs: role.KeyIDs}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
if !ok { // the targetfile may not exist yet - if not, then create it
|
||||||
|
var err error
|
||||||
|
t, err = tr.InitTargets(role)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyCanSign already makes sure this is not nil
|
||||||
|
r := tr.keysDB.GetRole(role)
|
||||||
|
|
||||||
invalid := make(data.Files)
|
invalid := make(data.Files)
|
||||||
for path, target := range targets {
|
for path, target := range targets {
|
||||||
pathDigest := sha256.Sum256([]byte(path))
|
pathDigest := sha256.Sum256([]byte(path))
|
||||||
pathHex := hex.EncodeToString(pathDigest[:])
|
pathHex := hex.EncodeToString(pathDigest[:])
|
||||||
r := tr.keysDB.GetRole(role)
|
|
||||||
if role == data.ValidRoles["targets"] || (r.CheckPaths(path) || r.CheckPrefixes(pathHex)) {
|
if role == data.ValidRoles["targets"] || (r.CheckPaths(path) || r.CheckPrefixes(pathHex)) {
|
||||||
t.Signed.Targets[path] = target
|
t.Signed.Targets[path] = target
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -431,15 +588,19 @@ func (tr *Repo) AddTargets(role string, targets data.Files) (data.Files, error)
|
||||||
|
|
||||||
// RemoveTargets removes the given target (paths) from the given target role (delegation)
|
// RemoveTargets removes the given target (paths) from the given target role (delegation)
|
||||||
func (tr *Repo) RemoveTargets(role string, targets ...string) error {
|
func (tr *Repo) RemoveTargets(role string, targets ...string) error {
|
||||||
t, ok := tr.Targets[role]
|
if err := tr.VerifyCanSign(role); err != nil {
|
||||||
if !ok {
|
return err
|
||||||
return data.ErrInvalidRole{Role: role}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range targets {
|
// if the role exists but metadata does not yet, then our work is done
|
||||||
delete(t.Signed.Targets, path)
|
t, ok := tr.Targets[role]
|
||||||
|
if ok {
|
||||||
|
for _, path := range targets {
|
||||||
|
delete(t.Signed.Targets, path)
|
||||||
|
}
|
||||||
|
t.Dirty = true
|
||||||
}
|
}
|
||||||
t.Dirty = true
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -494,6 +655,12 @@ func (tr *Repo) SignRoot(expires time.Time) (*data.Signed, error) {
|
||||||
// SignTargets signs the targets file for the given top level or delegated targets role
|
// SignTargets signs the targets file for the given top level or delegated targets role
|
||||||
func (tr *Repo) SignTargets(role string, expires time.Time) (*data.Signed, error) {
|
func (tr *Repo) SignTargets(role string, expires time.Time) (*data.Signed, error) {
|
||||||
logrus.Debugf("sign targets called for role %s", role)
|
logrus.Debugf("sign targets called for role %s", role)
|
||||||
|
if _, ok := tr.Targets[role]; !ok {
|
||||||
|
return nil, data.ErrInvalidRole{
|
||||||
|
Role: role,
|
||||||
|
Reason: "SignTargets called with non-existant targets role",
|
||||||
|
}
|
||||||
|
}
|
||||||
tr.Targets[role].Signed.Expires = expires
|
tr.Targets[role].Signed.Expires = expires
|
||||||
tr.Targets[role].Signed.Version++
|
tr.Targets[role].Signed.Version++
|
||||||
signed, err := tr.Targets[role].ToSigned()
|
signed, err := tr.Targets[role].ToSigned()
|
||||||
|
|
@ -532,6 +699,7 @@ func (tr *Repo) SignSnapshot(expires time.Time) (*data.Signed, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
targets.Dirty = false
|
||||||
}
|
}
|
||||||
tr.Snapshot.Signed.Expires = expires
|
tr.Snapshot.Signed.Expires = expires
|
||||||
tr.Snapshot.Signed.Version++
|
tr.Snapshot.Signed.Version++
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RoleList is a list of roles
|
||||||
|
type RoleList []string
|
||||||
|
|
||||||
|
// Len returns the length of the list
|
||||||
|
func (r RoleList) Len() int {
|
||||||
|
return len(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less returns true if the item at i should be sorted
|
||||||
|
// before the item at j. It's an unstable partial ordering
|
||||||
|
// based on the number of segments, separated by "/", in
|
||||||
|
// the role name
|
||||||
|
func (r RoleList) Less(i, j int) bool {
|
||||||
|
segsI := strings.Split(r[i], "/")
|
||||||
|
segsJ := strings.Split(r[j], "/")
|
||||||
|
if len(segsI) == len(segsJ) {
|
||||||
|
return r[i] < r[j]
|
||||||
|
}
|
||||||
|
return len(segsI) < len(segsJ)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap the items at 2 locations in the list
|
||||||
|
func (r RoleList) Swap(i, j int) {
|
||||||
|
r[i], r[j] = r[j], r[i]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrEmptyStack is used when an action that requires some
|
||||||
|
// content is invoked and the stack is empty
|
||||||
|
type ErrEmptyStack struct {
|
||||||
|
action string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrEmptyStack) Error() string {
|
||||||
|
return fmt.Sprintf("attempted to %s with empty stack", err.action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadTypeCast is used by PopX functions when the item
|
||||||
|
// cannot be typed to X
|
||||||
|
type ErrBadTypeCast struct{}
|
||||||
|
|
||||||
|
func (err ErrBadTypeCast) Error() string {
|
||||||
|
return "attempted to do a typed pop and item was not of type"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stack is a simple type agnostic stack implementation
|
||||||
|
type Stack struct {
|
||||||
|
s []interface{}
|
||||||
|
l sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStack create a new stack
|
||||||
|
func NewStack() *Stack {
|
||||||
|
s := &Stack{
|
||||||
|
s: make([]interface{}, 0),
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push adds an item to the top of the stack.
|
||||||
|
func (s *Stack) Push(item interface{}) {
|
||||||
|
s.l.Lock()
|
||||||
|
defer s.l.Unlock()
|
||||||
|
s.s = append(s.s, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop removes and returns the top item on the stack, or returns
|
||||||
|
// ErrEmptyStack if the stack has no content
|
||||||
|
func (s *Stack) Pop() (interface{}, error) {
|
||||||
|
s.l.Lock()
|
||||||
|
defer s.l.Unlock()
|
||||||
|
l := len(s.s)
|
||||||
|
if l > 0 {
|
||||||
|
item := s.s[l-1]
|
||||||
|
s.s = s.s[:l-1]
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
return nil, ErrEmptyStack{action: "Pop"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopString attempts to cast the top item on the stack to the string type.
|
||||||
|
// If this succeeds, it removes and returns the top item. If the item
|
||||||
|
// is not of the string type, ErrBadTypeCast is returned. If the stack
|
||||||
|
// is empty, ErrEmptyStack is returned
|
||||||
|
func (s *Stack) PopString() (string, error) {
|
||||||
|
s.l.Lock()
|
||||||
|
defer s.l.Unlock()
|
||||||
|
l := len(s.s)
|
||||||
|
if l > 0 {
|
||||||
|
item := s.s[l-1]
|
||||||
|
if item, ok := item.(string); ok {
|
||||||
|
s.s = s.s[:l-1]
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
return "", ErrBadTypeCast{}
|
||||||
|
}
|
||||||
|
return "", ErrEmptyStack{action: "PopString"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty returns true if the stack is empty
|
||||||
|
func (s *Stack) Empty() bool {
|
||||||
|
s.l.Lock()
|
||||||
|
defer s.l.Unlock()
|
||||||
|
return len(s.s) == 0
|
||||||
|
}
|
||||||
|
|
@ -105,3 +105,44 @@ func DoHash(alg string, d []byte) []byte {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnusedDelegationKeys prunes a list of keys, returning those that are no
|
||||||
|
// longer in use for a given targets file
|
||||||
|
func UnusedDelegationKeys(t data.SignedTargets) []string {
|
||||||
|
// compare ids to all still active key ids in all active roles
|
||||||
|
// with the targets file
|
||||||
|
found := make(map[string]bool)
|
||||||
|
for _, r := range t.Signed.Delegations.Roles {
|
||||||
|
for _, id := range r.KeyIDs {
|
||||||
|
found[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var discard []string
|
||||||
|
for id := range t.Signed.Delegations.Keys {
|
||||||
|
if !found[id] {
|
||||||
|
discard = append(discard, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return discard
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveUnusedKeys determines which keys in the slice of IDs are no longer
|
||||||
|
// used in the given targets file and removes them from the delegated keys
|
||||||
|
// map
|
||||||
|
func RemoveUnusedKeys(t *data.SignedTargets) {
|
||||||
|
unusedIDs := UnusedDelegationKeys(*t)
|
||||||
|
for _, id := range unusedIDs {
|
||||||
|
delete(t.Signed.Delegations.Keys, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindRoleIndex returns the index of the role named <name> or -1 if no
|
||||||
|
// matching role is found.
|
||||||
|
func FindRoleIndex(rs []*data.Role, name string) int {
|
||||||
|
for i, r := range rs {
|
||||||
|
if r.Name == name {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VALIDATION ERRORS
|
||||||
|
|
||||||
|
// ErrValidation represents a general validation error
|
||||||
|
type ErrValidation struct {
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrValidation) Error() string {
|
||||||
|
return fmt.Sprintf("An error occurred during validation: %s", err.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadHierarchy represents missing metadata. Currently: a missing snapshot
|
||||||
|
// at this current time. When delegations are implemented it will also
|
||||||
|
// represent a missing delegation parent
|
||||||
|
type ErrBadHierarchy struct {
|
||||||
|
Missing string
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrBadHierarchy) Error() string {
|
||||||
|
return fmt.Sprintf("Metadata hierarchy is incomplete: %s", err.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadRoot represents a failure validating the root
|
||||||
|
type ErrBadRoot struct {
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrBadRoot) Error() string {
|
||||||
|
return fmt.Sprintf("The root metadata is invalid: %s", err.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadTargets represents a failure to validate a targets (incl delegations)
|
||||||
|
type ErrBadTargets struct {
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrBadTargets) Error() string {
|
||||||
|
return fmt.Sprintf("The targets metadata is invalid: %s", err.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadSnapshot represents a failure to validate the snapshot
|
||||||
|
type ErrBadSnapshot struct {
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrBadSnapshot) Error() string {
|
||||||
|
return fmt.Sprintf("The snapshot metadata is invalid: %s", err.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// END VALIDATION ERRORS
|
||||||
|
|
||||||
|
// SerializableError is a struct that can be used to serialize an error as JSON
|
||||||
|
type SerializableError struct {
|
||||||
|
Name string
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON attempts to unmarshal the error into the right type
|
||||||
|
func (s *SerializableError) UnmarshalJSON(text []byte) (err error) {
|
||||||
|
var x struct{ Name string }
|
||||||
|
err = json.Unmarshal(text, &x)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var theError error
|
||||||
|
switch x.Name {
|
||||||
|
case "ErrValidation":
|
||||||
|
var e struct{ Error ErrValidation }
|
||||||
|
err = json.Unmarshal(text, &e)
|
||||||
|
theError = e.Error
|
||||||
|
case "ErrBadHierarchy":
|
||||||
|
var e struct{ Error ErrBadHierarchy }
|
||||||
|
err = json.Unmarshal(text, &e)
|
||||||
|
theError = e.Error
|
||||||
|
case "ErrBadRoot":
|
||||||
|
var e struct{ Error ErrBadRoot }
|
||||||
|
err = json.Unmarshal(text, &e)
|
||||||
|
theError = e.Error
|
||||||
|
case "ErrBadTargets":
|
||||||
|
var e struct{ Error ErrBadTargets }
|
||||||
|
err = json.Unmarshal(text, &e)
|
||||||
|
theError = e.Error
|
||||||
|
case "ErrBadSnapshot":
|
||||||
|
var e struct{ Error ErrBadSnapshot }
|
||||||
|
err = json.Unmarshal(text, &e)
|
||||||
|
theError = e.Error
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("do not know how to unmarshal %s", x.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Name = x.Name
|
||||||
|
s.Error = theError
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSerializableError serializes one of the above errors into JSON
|
||||||
|
func NewSerializableError(err error) (*SerializableError, error) {
|
||||||
|
// make sure it's one of our errors
|
||||||
|
var name string
|
||||||
|
switch err.(type) {
|
||||||
|
case ErrValidation:
|
||||||
|
name = "ErrValidation"
|
||||||
|
case ErrBadHierarchy:
|
||||||
|
name = "ErrBadHierarchy"
|
||||||
|
case ErrBadRoot:
|
||||||
|
name = "ErrBadRoot"
|
||||||
|
case ErrBadTargets:
|
||||||
|
name = "ErrBadTargets"
|
||||||
|
case ErrBadSnapshot:
|
||||||
|
name = "ErrBadSnapshot"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("does not support serializing non-validation errors")
|
||||||
|
}
|
||||||
|
return &SerializableError{Name: name, Error: err}, nil
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue