Signed-off-by: cyli <cyli@twistedmatrix.com>
cyli authored on 2016/09/22 08:18:22... | ... |
@@ -176,7 +176,7 @@ RUN set -x \ |
176 | 176 |
&& rm -rf "$GOPATH" |
177 | 177 |
|
178 | 178 |
# Install notary and notary-server |
179 |
-ENV NOTARY_VERSION v0.3.0 |
|
179 |
+ENV NOTARY_VERSION v0.4.2 |
|
180 | 180 |
RUN set -x \ |
181 | 181 |
&& export GOPATH="$(mktemp -d)" \ |
182 | 182 |
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \ |
... | ... |
@@ -121,7 +121,7 @@ RUN set -x \ |
121 | 121 |
&& rm -rf "$GOPATH" |
122 | 122 |
|
123 | 123 |
# Install notary and notary-server |
124 |
-ENV NOTARY_VERSION v0.3.0 |
|
124 |
+ENV NOTARY_VERSION v0.4.2 |
|
125 | 125 |
RUN set -x \ |
126 | 126 |
&& export GOPATH="$(mktemp -d)" \ |
127 | 127 |
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \ |
... | ... |
@@ -120,7 +120,7 @@ RUN set -x \ |
120 | 120 |
&& rm -rf "$GOPATH" |
121 | 121 |
|
122 | 122 |
# Install notary and notary-server |
123 |
-ENV NOTARY_VERSION v0.3.0 |
|
123 |
+ENV NOTARY_VERSION v0.4.2 |
|
124 | 124 |
RUN set -x \ |
125 | 125 |
&& export GOPATH="$(mktemp -d)" \ |
126 | 126 |
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \ |
... | ... |
@@ -139,7 +139,7 @@ RUN set -x \ |
139 | 139 |
&& rm -rf "$GOPATH" |
140 | 140 |
|
141 | 141 |
# Install notary and notary-server |
142 |
-ENV NOTARY_VERSION v0.3.0 |
|
142 |
+ENV NOTARY_VERSION v0.4.2 |
|
143 | 143 |
RUN set -x \ |
144 | 144 |
&& export GOPATH="$(mktemp -d)" \ |
145 | 145 |
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \ |
... | ... |
@@ -131,7 +131,7 @@ RUN set -x \ |
131 | 131 |
&& rm -rf "$GOPATH" |
132 | 132 |
|
133 | 133 |
# Install notary and notary-server |
134 |
-ENV NOTARY_VERSION v0.3.0 |
|
134 |
+ENV NOTARY_VERSION v0.4.2 |
|
135 | 135 |
RUN set -x \ |
136 | 136 |
&& export GOPATH="$(mktemp -d)" \ |
137 | 137 |
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \ |
... | ... |
@@ -99,7 +99,7 @@ clone git github.com/mistifyio/go-zfs 22c9b32c84eb0d0c6f4043b6e90fc94073de92fa |
99 | 99 |
clone git github.com/pborman/uuid v1.0 |
100 | 100 |
|
101 | 101 |
# get desired notary commit, might also need to be updated in Dockerfile |
102 |
-clone git github.com/docker/notary v0.3.0 |
|
102 |
+clone git github.com/docker/notary v0.4.2 |
|
103 | 103 |
|
104 | 104 |
clone git google.golang.org/grpc v1.0.1-GA https://github.com/grpc/grpc-go.git |
105 | 105 |
clone git github.com/miekg/pkcs11 df8ae6ca730422dba20c768ff38ef7d79077a59f |
... | ... |
@@ -1,5 +1,66 @@ |
1 | 1 |
# Changelog |
2 | 2 |
|
3 |
+## [v0.4.2](https://github.com/docker/notary/releases/tag/v0.4.2) 9/30/2016 |
|
4 |
++ Bump the cross compiler to golang 1.7.1, since [1.6.3 builds binaries that could have non-deterministic bugs in OS X Sierra](https://groups.google.com/forum/#!msg/golang-dev/Jho5sBHZgAg/cq6d97S1AwAJ) [#984](https://github.com/docker/notary/pull/984) |
|
5 |
+ |
|
6 |
+## [v0.4.1](https://github.com/docker/notary/releases/tag/v0.4.1) 9/27/2016 |
|
7 |
++ Preliminary Windows support for notary client [#970](https://github.com/docker/notary/pull/970) |
|
8 |
++ Output message to CLI when repo changes have been successfully published [#974](https://github.com/docker/notary/pull/974) |
|
9 |
++ Improved error messages for client authentication errors and for the witness command [#972](https://github.com/docker/notary/pull/972) |
|
10 |
++ Support for finding keys that are anywhere in the notary directory's "private" directory, not just under "private/root_keys" or "private/tuf_keys" [#981](https://github.com/docker/notary/pull/981) |
|
11 |
++ Previously, on any error updating, the client would fall back on the cache. Now we only do so if there is a network error or if the server is unavailable or missing the TUF data. Invalid TUF data will cause the update to fail - for example if there was an invalid root rotation. [#982](https://github.com/docker/notary/pull/982) |
|
12 |
+ |
|
13 |
+## [v0.4.0](https://github.com/docker/notary/releases/tag/v0.4.0) 9/21/2016 |
|
14 |
++ Server-managed key rotations [#889](https://github.com/docker/notary/pull/889) |
|
15 |
++ Remove `timestamp_keys` table, which stored redundant information [#889](https://github.com/docker/notary/pull/889) |
|
16 |
++ Introduce `notary delete` command to delete local and/or remote repo data [#895](https://github.com/docker/notary/pull/895) |
|
17 |
++ Introduce `notary witness` command to stage signatures for specified roles [#875](https://github.com/docker/notary/pull/875) |
|
18 |
++ Add `-p` flag to offline commands to attempt auto-publish [#886](https://github.com/docker/notary/pull/886) [#912](https://github.com/docker/notary/pull/912) [#923](https://github.com/docker/notary/pull/923) |
|
19 |
++ Introduce `notary reset` command to manage staged changes [#959](https://github.com/docker/notary/pull/959) [#856](https://github.com/docker/notary/pull/856) |
|
20 |
++ Add `--rootkey` flag to `notary init` to provide a private root key for a repo [#801](https://github.com/docker/notary/pull/801) |
|
21 |
++ Introduce `notary delegation purge` command to remove a specified key from all delegations [#855](https://github.com/docker/notary/pull/855) |
|
22 |
++ Removed HTTP endpoint from notary-signer [#870](https://github.com/docker/notary/pull/870) |
|
23 |
++ Refactored and unified key storage [#825](https://github.com/docker/notary/pull/825) |
|
24 |
++ Batched key import and export now operate on PEM files (potentially with multiple blocks) instead of ZIP [#825](https://github.com/docker/notary/pull/825) [#882](https://github.com/docker/notary/pull/882) |
|
25 |
++ Add full database integration test-suite [#824](https://github.com/docker/notary/pull/824) [#854](https://github.com/docker/notary/pull/854) [#863](https://github.com/docker/notary/pull/863) |
|
26 |
++ Improve notary-server, trust pinning, and yubikey logging [#798](https://github.com/docker/notary/pull/798) [#858](https://github.com/docker/notary/pull/858) [#891](https://github.com/docker/notary/pull/891) |
|
27 |
++ Warn if certificates for root or delegations are near expiry [#802](https://github.com/docker/notary/pull/802) |
|
28 |
++ Warn if role metadata is near expiry [#786](https://github.com/docker/notary/pull/786) |
|
29 |
++ Reformat CLI table output to use the `text/tabwriter` package [#809](https://github.com/docker/notary/pull/809) |
|
30 |
++ Fix passphrase retrieval attempt counting and terminal detection [#906](https://github.com/docker/notary/pull/906) |
|
31 |
++ Fix listing nested delegations [#864](https://github.com/docker/notary/pull/864) |
|
32 |
++ Bump go version to 1.6.3, fix go1.7 compatibility [#851](https://github.com/docker/notary/pull/851) [#793](https://github.com/docker/notary/pull/793) |
|
33 |
++ Convert docker-compose files to v2 format [#755](https://github.com/docker/notary/pull/755) |
|
34 |
++ Validate root rotations against trust pinning [#800](https://github.com/docker/notary/pull/800) |
|
35 |
++ Update fixture certificates for two-year expiry window [#951](https://github.com/docker/notary/pull/951) |
|
36 |
+ |
|
37 |
+## [v0.3.0](https://github.com/docker/notary/releases/tag/v0.3.0) 5/11/2016 |
|
38 |
++ Root rotations |
|
39 |
++ RethinkDB support as a storage backend for Server and Signer |
|
40 |
++ A new TUF repo builder that merges server and client validation |
|
41 |
++ Trust Pinning: configure known good key IDs and CAs to replace TOFU. |
|
42 |
++ Add --input, --output, and --quiet flags to notary verify command |
|
43 |
++ Remove local certificate store. It was redundant as all certs were also stored in the cached root.json |
|
44 |
++ Cleanup of dead code in client side key storage logic |
|
45 |
++ Update project to Go 1.6.1 |
|
46 |
++ Reorganize vendoring to meet Go 1.6+ standard. Still using Godeps to manage vendored packages |
|
47 |
++ Add targets by hash, no longer necessary to have the original target data available |
|
48 |
++ Active Key ID verification during signature verification |
|
49 |
++ Switch all testing from assert to require, reduces noise in test runs |
|
50 |
++ Use alpine based images for smaller downloads and faster setup times |
|
51 |
++ Clean up out of data signatures when re-signing content |
|
52 |
++ Set cache control headers on HTTP responses from Notary Server |
|
53 |
++ Add sha512 support for targets |
|
54 |
++ Add environment variable for delegation key passphrase |
|
55 |
++ Reduce permissions requested by client from token server |
|
56 |
++ Update formatting for delegation list output |
|
57 |
++ Move SQLite dependency to tests only so it doesn't get built into official images |
|
58 |
++ Fixed asking for password to list private repositories |
|
59 |
++ Enable using notary client with username/password in a scripted fashion |
|
60 |
++ Fix static compilation of client |
|
61 |
++ Enforce TUF version to be >= 1, previously 0 was acceptable although unused |
|
62 |
++ json.RawMessage should always be used as *json.RawMessage due to concepts of addressability in Go and effects on encoding |
|
63 |
+ |
|
3 | 64 |
## [v0.2](https://github.com/docker/notary/releases/tag/v0.2.0) 2/24/2016 |
4 | 65 |
+ Add support for delegation roles in `notary` server and client |
5 | 66 |
+ Add `notary CLI` commands for managing delegation roles: `notary delegation` |
... | ... |
@@ -1,4 +1,4 @@ |
1 |
-FROM golang:1.6.1 |
|
1 |
+FROM golang:1.7.1 |
|
2 | 2 |
|
3 | 3 |
RUN apt-get update && apt-get install -y \ |
4 | 4 |
curl \ |
... | ... |
@@ -8,10 +8,14 @@ RUN apt-get update && apt-get install -y \ |
8 | 8 |
patch \ |
9 | 9 |
tar \ |
10 | 10 |
xz-utils \ |
11 |
+ python \ |
|
12 |
+ python-pip \ |
|
11 | 13 |
--no-install-recommends \ |
12 | 14 |
&& rm -rf /var/lib/apt/lists/* |
13 | 15 |
|
14 |
-RUN go get golang.org/x/tools/cmd/cover |
|
16 |
+RUN useradd -ms /bin/bash notary \ |
|
17 |
+ && pip install codecov \ |
|
18 |
+ && go get golang.org/x/tools/cmd/cover github.com/golang/lint/golint github.com/client9/misspell/cmd/misspell github.com/gordonklaus/ineffassign |
|
15 | 19 |
|
16 | 20 |
# Configure the container for OSX cross compilation |
17 | 21 |
ENV OSX_SDK MacOSX10.11.sdk |
... | ... |
@@ -27,8 +31,7 @@ ENV PATH /osxcross/target/bin:$PATH |
27 | 27 |
ENV NOTARYDIR /go/src/github.com/docker/notary |
28 | 28 |
|
29 | 29 |
COPY . ${NOTARYDIR} |
30 |
- |
|
31 |
-ENV GOPATH ${NOTARYDIR}/Godeps/_workspace:$GOPATH |
|
30 |
+RUN chmod -R a+rw /go |
|
32 | 31 |
|
33 | 32 |
WORKDIR ${NOTARYDIR} |
34 | 33 |
|
... | ... |
@@ -13,13 +13,15 @@ endif |
13 | 13 |
CTIMEVAR=-X $(NOTARY_PKG)/version.GitCommit=$(GITCOMMIT) -X $(NOTARY_PKG)/version.NotaryVersion=$(NOTARY_VERSION) |
14 | 14 |
GO_LDFLAGS=-ldflags "-w $(CTIMEVAR)" |
15 | 15 |
GO_LDFLAGS_STATIC=-ldflags "-w $(CTIMEVAR) -extldflags -static" |
16 |
-GOOSES = darwin linux |
|
16 |
+GOOSES = darwin linux windows |
|
17 | 17 |
NOTARY_BUILDTAGS ?= pkcs11 |
18 | 18 |
NOTARYDIR := /go/src/github.com/docker/notary |
19 | 19 |
|
20 |
-GO_VERSION := $(shell go version | grep "1\.[6-9]\(\.[0-9]+\)*") |
|
21 |
-# check to make sure we have the right version |
|
22 |
-ifeq ($(strip $(GO_VERSION)),) |
|
20 |
+GO_VERSION := $(shell go version | grep "1\.[6-9]\(\.[0-9]+\)*\|devel") |
|
21 |
+# check to make sure we have the right version. development versions of Go are |
|
22 |
+# not officially supported, but allowed for building |
|
23 |
+ |
|
24 |
+ifeq ($(strip $(GO_VERSION))$(SKIPENVCHECK),) |
|
23 | 25 |
$(error Bad Go version - please install Go >= 1.6) |
24 | 26 |
endif |
25 | 27 |
|
... | ... |
@@ -40,13 +42,11 @@ COVERPROFILE?=$(COVERDIR)/cover.out |
40 | 40 |
COVERMODE=count |
41 | 41 |
PKGS ?= $(shell go list -tags "${NOTARY_BUILDTAGS}" ./... | grep -v /vendor/ | tr '\n' ' ') |
42 | 42 |
|
43 |
-GO_VERSION = $(shell go version | awk '{print $$3}') |
|
44 |
- |
|
45 |
-.PHONY: clean all fmt vet lint build test binaries cross cover docker-images notary-dockerfile |
|
43 |
+.PHONY: clean all lint build test binaries cross cover docker-images notary-dockerfile |
|
46 | 44 |
.DELETE_ON_ERROR: cover |
47 | 45 |
.DEFAULT: default |
48 | 46 |
|
49 |
-all: AUTHORS clean fmt vet fmt lint build test binaries |
|
47 |
+all: AUTHORS clean lint build test binaries |
|
50 | 48 |
|
51 | 49 |
AUTHORS: .git/HEAD |
52 | 50 |
git log --format='%aN <%aE>' | sort -fu > $@ |
... | ... |
@@ -90,32 +90,27 @@ ${PREFIX}/bin/static/notary: |
90 | 90 |
@go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary |
91 | 91 |
endif |
92 | 92 |
|
93 |
-vet: |
|
94 |
- @echo "+ $@" |
|
93 |
+ |
|
94 |
+# run all lint functionality - excludes Godep directory, vendoring, binaries, python tests, and git files |
|
95 |
+lint: |
|
96 |
+ @echo "+ $@: golint, go vet, go fmt, misspell, ineffassign" |
|
97 |
+ # golint |
|
98 |
+ @test -z "$(shell find . -type f -name "*.go" -not -path "./vendor/*" -not -name "*.pb.*" -exec golint {} \; | tee /dev/stderr)" |
|
99 |
+ # gofmt |
|
100 |
+ @test -z "$$(gofmt -s -l .| grep -v .pb. | grep -v vendor/ | tee /dev/stderr)" |
|
101 |
+ # govet |
|
95 | 102 |
ifeq ($(shell uname -s), Darwin) |
96 | 103 |
@test -z "$(shell find . -iname *test*.go | grep -v _test.go | grep -v vendor | xargs echo "This file should end with '_test':" | tee /dev/stderr)" |
97 | 104 |
else |
98 | 105 |
@test -z "$(shell find . -iname *test*.go | grep -v _test.go | grep -v vendor | xargs -r echo "This file should end with '_test':" | tee /dev/stderr)" |
99 | 106 |
endif |
100 | 107 |
@test -z "$$(go tool vet -printf=false . 2>&1 | grep -v vendor/ | tee /dev/stderr)" |
101 |
- |
|
102 |
-fmt: |
|
103 |
- @echo "+ $@" |
|
104 |
- @test -z "$$(gofmt -s -l .| grep -v .pb. | grep -v vendor/ | tee /dev/stderr)" |
|
105 |
- |
|
106 |
-lint: |
|
107 |
- @echo "+ $@" |
|
108 |
- @test -z "$(shell find . -type f -name "*.go" -not -path "./vendor/*" -not -name "*.pb.*" -exec golint {} \; | tee /dev/stderr)" |
|
109 |
- |
|
110 |
-# Requires that the following: |
|
111 |
-# go get -u github.com/client9/misspell/cmd/misspell |
|
112 |
-# |
|
113 |
-# be run first |
|
114 |
- |
|
115 |
-# misspell target, don't include Godeps, binaries, python tests, or git files |
|
116 |
-misspell: |
|
117 |
- @echo "+ $@" |
|
118 |
- @test -z "$$(find . -name '*' | grep -v vendor/ | grep -v bin/ | grep -v misc/ | grep -v .git/ | xargs misspell | tee /dev/stderr)" |
|
108 |
+ # misspell - requires that the following be run first: |
|
109 |
+ # go get -u github.com/client9/misspell/cmd/misspell |
|
110 |
+ @test -z "$$(find . -type f | grep -v vendor/ | grep -v bin/ | grep -v misc/ | grep -v .git/ | grep -v \.pdf | xargs misspell | tee /dev/stderr)" |
|
111 |
+ # ineffassign - requires that the following be run first: |
|
112 |
+ # go get -u github.com/gordonklaus/ineffassign |
|
113 |
+ @test -z "$(shell find . -type f -name "*.go" -not -path "./vendor/*" -not -name "*.pb.*" -exec ineffassign {} \; | tee /dev/stderr)" |
|
119 | 114 |
|
120 | 115 |
build: |
121 | 116 |
@echo "+ $@" |
... | ... |
@@ -130,15 +125,13 @@ test: |
130 | 130 |
@echo |
131 | 131 |
go test -tags "${NOTARY_BUILDTAGS}" $(TESTOPTS) $(PKGS) |
132 | 132 |
|
133 |
-test-full: TESTOPTS = |
|
134 |
-test-full: vet lint |
|
135 |
- @echo Note: when testing with a yubikey plugged in, make sure to include 'TESTOPTS="-p 1"' |
|
136 |
- @echo "+ $@" |
|
137 |
- @echo |
|
138 |
- go test -tags "${NOTARY_BUILDTAGS}" $(TESTOPTS) -v $(PKGS) |
|
133 |
+integration: TESTDB = mysql |
|
134 |
+integration: clean |
|
135 |
+ buildscripts/integrationtest.sh $(TESTDB) |
|
139 | 136 |
|
140 |
-integration: |
|
141 |
- buildscripts/integrationtest.sh development.yml |
|
137 |
+testdb: TESTDB = mysql |
|
138 |
+testdb: |
|
139 |
+ buildscripts/dbtests.sh $(TESTDB) |
|
142 | 140 |
|
143 | 141 |
protos: |
144 | 142 |
@protoc --go_out=plugins=grpc:. proto/*.proto |
... | ... |
@@ -148,25 +141,19 @@ protos: |
148 | 148 |
# go get github.com/wadey/gocovmerge; go install github.com/wadey/gocovmerge |
149 | 149 |
# |
150 | 150 |
# be run first |
151 |
- |
|
152 |
-define gocover |
|
153 |
-go test $(OPTS) $(TESTOPTS) -covermode="$(COVERMODE)" -coverprofile="$(COVERDIR)/$(subst /,-,$(1)).$(subst $(_space),.,$(NOTARY_BUILDTAGS)).coverage.txt" "$(1)" || exit 1; |
|
154 |
-endef |
|
155 |
- |
|
151 |
+gen-cover: |
|
156 | 152 |
gen-cover: |
157 | 153 |
@mkdir -p "$(COVERDIR)" |
158 |
- $(foreach PKG,$(PKGS),$(call gocover,$(PKG))) |
|
159 |
- rm -f "$(COVERDIR)"/*testutils*.coverage.txt |
|
154 |
+ python -u buildscripts/covertest.py --coverdir "$(COVERDIR)" --tags "$(NOTARY_BUILDTAGS)" --pkgs="$(PKGS)" --testopts="${TESTOPTS}" |
|
160 | 155 |
|
161 | 156 |
# Generates the cover binaries and runs them all in serial, so this can be used |
162 | 157 |
# run all tests with a yubikey without any problems |
163 |
-cover: OPTS = -tags "${NOTARY_BUILDTAGS}" -coverpkg "$(shell ./coverpkg.sh $(1) $(NOTARY_PKG))" |
|
164 | 158 |
cover: gen-cover covmerge |
165 | 159 |
@go tool cover -html="$(COVERPROFILE)" |
166 | 160 |
|
167 | 161 |
# Generates the cover binaries and runs them all in serial, so this can be used |
168 | 162 |
# run all tests with a yubikey without any problems |
169 |
-ci: OPTS = -tags "${NOTARY_BUILDTAGS}" -race -coverpkg "$(shell ./coverpkg.sh $(1) $(NOTARY_PKG))" |
|
163 |
+ci: override TESTOPTS = -race |
|
170 | 164 |
# Codecov knows how to merge multiple coverage files, so covmerge is not needed |
171 | 165 |
ci: gen-cover |
172 | 166 |
|
... | ... |
@@ -205,10 +192,9 @@ shell: notary-dockerfile |
205 | 205 |
|
206 | 206 |
cross: notary-dockerfile |
207 | 207 |
@rm -rf $(CURDIR)/cross |
208 |
- docker run --rm -v $(CURDIR)/cross:$(NOTARYDIR)/cross -e NOTARY_BUILDTAGS=$(NOTARY_BUILDTAGS) notary buildscripts/cross.sh $(GOOSES) |
|
209 |
- |
|
208 |
+ docker run --rm -v $(CURDIR)/cross:$(NOTARYDIR)/cross -e CTIMEVAR="${CTIMEVAR}" -e NOTARY_BUILDTAGS=$(NOTARY_BUILDTAGS) notary buildscripts/cross.sh $(GOOSES) |
|
210 | 209 |
|
211 | 210 |
clean: |
212 | 211 |
@echo "+ $@" |
213 |
- @rm -rf "$(COVERDIR)" |
|
212 |
+ @rm -rf "$(COVERDIR)" cross |
|
214 | 213 |
@rm -rf "${PREFIX}/bin/notary-server" "${PREFIX}/bin/notary" "${PREFIX}/bin/notary-signer" |
... | ... |
@@ -1,5 +1,5 @@ |
1 | 1 |
# Notary |
2 |
-[![Circle CI](https://circleci.com/gh/docker/notary/tree/master.svg?style=shield)](https://circleci.com/gh/docker/notary/tree/master) [![CodeCov](https://codecov.io/github/docker/notary/coverage.svg?branch=master)](https://codecov.io/github/docker/notary) |
|
2 |
+[![Circle CI](https://circleci.com/gh/docker/notary/tree/master.svg?style=shield)](https://circleci.com/gh/docker/notary/tree/master) [![CodeCov](https://codecov.io/github/docker/notary/coverage.svg?branch=master)](https://codecov.io/github/docker/notary) [![GoReportCard](https://goreportcard.com/badge/docker/notary)](https://goreportcard.com/report/github.com/docker/notary) |
|
3 | 3 |
|
4 | 4 |
The Notary project comprises a [server](cmd/notary-server) and a [client](cmd/notary) for running and interacting |
5 | 5 |
with trusted collections. Please see the [service architecture](docs/service_architecture.md) documentation |
... | ... |
@@ -80,7 +80,8 @@ to use `notary` with Docker images. |
80 | 80 |
|
81 | 81 |
Prerequisites: |
82 | 82 |
|
83 |
-- Go >= 1.6.1 |
|
83 |
+- Go >= 1.7 |
|
84 |
+ |
|
84 | 85 |
- [godep](https://github.com/tools/godep) installed |
85 | 86 |
- libtool development headers installed |
86 | 87 |
- Ubuntu: `apt-get install libltdl-dev` |
... | ... |
@@ -1,87 +1,23 @@ |
1 |
-# Pony-up! |
|
2 | 1 |
machine: |
3 | 2 |
pre: |
4 |
- # Install gvm |
|
5 |
- - bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/1.0.22/binscripts/gvm-installer) |
|
6 | 3 |
# Upgrade docker |
7 |
- - sudo curl -L -o /usr/bin/docker 'https://s3-external-1.amazonaws.com/circle-downloads/docker-1.9.1-circleci' |
|
8 |
- - sudo chmod 0755 /usr/bin/docker |
|
9 |
- |
|
10 |
- post: |
|
11 |
- # Install many go versions |
|
12 |
- - gvm install go1.6.1 -B --name=stable |
|
4 |
+ - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0 |
|
13 | 5 |
# upgrade compose |
14 | 6 |
- sudo pip install --upgrade docker-compose |
15 | 7 |
|
16 | 8 |
services: |
17 | 9 |
- docker |
18 | 10 |
|
19 |
- environment: |
|
20 |
- # Convenient shortcuts to "common" locations |
|
21 |
- CHECKOUT: /home/ubuntu/$CIRCLE_PROJECT_REPONAME |
|
22 |
- BASE_DIR: src/github.com/docker/notary |
|
23 |
- # Trick circle brainflat "no absolute path" behavior |
|
24 |
- BASE_STABLE: ../../../$HOME/.gvm/pkgsets/stable/global/$BASE_DIR |
|
25 |
- # Workaround Circle parsing dumb bugs and/or YAML wonkyness |
|
26 |
- CIRCLE_PAIN: "mode: set" |
|
27 |
- # Put the coverage profile somewhere codecov's script can find it |
|
28 |
- COVERPROFILE: coverage.out |
|
29 |
- |
|
30 |
- hosts: |
|
31 |
- # Not used yet |
|
32 |
- fancy: 127.0.0.1 |
|
33 |
- |
|
34 | 11 |
dependencies: |
35 |
- pre: |
|
36 |
- # Copy the code to the gopath of all go versions |
|
37 |
- - > |
|
38 |
- gvm use stable && |
|
39 |
- mkdir -p "$(dirname $BASE_STABLE)" && |
|
40 |
- cp -R "$CHECKOUT" "$BASE_STABLE" |
|
41 |
- |
|
42 | 12 |
override: |
43 |
- # don't use circleci's default dependency installation step of `go get -d -u ./...` |
|
44 |
- # since we already vendor everything; additionally install linting and misspell tools |
|
45 |
- - > |
|
46 |
- gvm use stable && |
|
47 |
- go get github.com/golang/lint/golint && |
|
48 |
- go get -u github.com/client9/misspell/cmd/misspell |
|
13 |
+ - docker build -t notary_client . |
|
49 | 14 |
|
50 | 15 |
test: |
51 |
- pre: |
|
52 |
- # Output the go versions we are going to test |
|
53 |
- - gvm use stable && go version |
|
54 |
- |
|
55 |
- # CLEAN |
|
56 |
- - gvm use stable && make clean: |
|
57 |
- pwd: $BASE_STABLE |
|
58 |
- |
|
59 |
- # FMT |
|
60 |
- - gvm use stable && make fmt: |
|
61 |
- pwd: $BASE_STABLE |
|
62 |
- |
|
63 |
- # VET |
|
64 |
- - gvm use stable && make vet: |
|
65 |
- pwd: $BASE_STABLE |
|
66 |
- |
|
67 |
- # LINT |
|
68 |
- - gvm use stable && make lint: |
|
69 |
- pwd: $BASE_STABLE |
|
70 |
- |
|
71 |
- # MISSPELL |
|
72 |
- - gvm use stable && make misspell: |
|
73 |
- pwd: $BASE_STABLE |
|
74 |
- |
|
75 | 16 |
override: |
76 |
- # Test stable, and report |
|
77 |
- # hacking this to be parallel |
|
78 |
- - case $CIRCLE_NODE_INDEX in 0) gvm use stable && NOTARY_BUILDTAGS=pkcs11 make ci ;; 1) gvm use stable && NOTARY_BUILDTAGS=none make ci ;; 2) gvm use stable && make integration ;; esac: |
|
17 |
+ # circleci only supports manual parellism |
|
18 |
+ - buildscripts/circle_parallelism.sh: |
|
79 | 19 |
parallel: true |
80 | 20 |
timeout: 600 |
81 |
- pwd: $BASE_STABLE |
|
82 |
- |
|
83 | 21 |
post: |
84 |
- # Report to codecov.io |
|
85 |
- - case $CIRCLE_NODE_INDEX in 0) bash <(curl -s https://codecov.io/bash) ;; 1) bash <(curl -s https://codecov.io/bash) ;; esac: |
|
86 |
- parallel: true |
|
87 |
- pwd: $BASE_STABLE |
|
22 |
+ - docker-compose -f docker-compose.yml down -v |
|
23 |
+ - docker-compose -f docker-compose.rethink.yml down -v |
... | ... |
@@ -4,7 +4,7 @@ import ( |
4 | 4 |
"github.com/docker/notary/tuf/data" |
5 | 5 |
) |
6 | 6 |
|
7 |
-// Scopes for TufChanges are simply the TUF roles. |
|
7 |
+// Scopes for TUFChanges are simply the TUF roles. |
|
8 | 8 |
// Unfortunately because of targets delegations, we can only |
9 | 9 |
// cover the base roles. |
10 | 10 |
const ( |
... | ... |
@@ -14,7 +14,7 @@ const ( |
14 | 14 |
ScopeTimestamp = "timestamp" |
15 | 15 |
) |
16 | 16 |
|
17 |
-// Types for TufChanges are namespaced by the Role they |
|
17 |
+// Types for TUFChanges are namespaced by the Role they |
|
18 | 18 |
// are relevant for. The Root and Targets roles are the |
19 | 19 |
// only ones for which user action can cause a change, as |
20 | 20 |
// all changes in Snapshot and Timestamp are programmatically |
... | ... |
@@ -23,10 +23,11 @@ const ( |
23 | 23 |
TypeRootRole = "role" |
24 | 24 |
TypeTargetsTarget = "target" |
25 | 25 |
TypeTargetsDelegation = "delegation" |
26 |
+ TypeWitness = "witness" |
|
26 | 27 |
) |
27 | 28 |
|
28 |
-// TufChange represents a change to a TUF repo |
|
29 |
-type TufChange struct { |
|
29 |
+// TUFChange represents a change to a TUF repo |
|
30 |
+type TUFChange struct { |
|
30 | 31 |
// Abbreviated because Go doesn't permit a field and method of the same name |
31 | 32 |
Actn string `json:"action"` |
32 | 33 |
Role string `json:"role"` |
... | ... |
@@ -35,16 +36,16 @@ type TufChange struct { |
35 | 35 |
Data []byte `json:"data"` |
36 | 36 |
} |
37 | 37 |
|
38 |
-// TufRootData represents a modification of the keys associated |
|
38 |
+// TUFRootData represents a modification of the keys associated |
|
39 | 39 |
// with a role that appears in the root.json |
40 |
-type TufRootData struct { |
|
40 |
+type TUFRootData struct { |
|
41 | 41 |
Keys data.KeyList `json:"keys"` |
42 | 42 |
RoleName string `json:"role"` |
43 | 43 |
} |
44 | 44 |
|
45 |
-// NewTufChange initializes a tufChange object |
|
46 |
-func NewTufChange(action string, role, changeType, changePath string, content []byte) *TufChange { |
|
47 |
- return &TufChange{ |
|
45 |
+// NewTUFChange initializes a TUFChange object |
|
46 |
+func NewTUFChange(action string, role, changeType, changePath string, content []byte) *TUFChange { |
|
47 |
+ return &TUFChange{ |
|
48 | 48 |
Actn: action, |
49 | 49 |
Role: role, |
50 | 50 |
ChangeType: changeType, |
... | ... |
@@ -54,34 +55,34 @@ func NewTufChange(action string, role, changeType, changePath string, content [] |
54 | 54 |
} |
55 | 55 |
|
56 | 56 |
// Action return c.Actn |
57 |
-func (c TufChange) Action() string { |
|
57 |
+func (c TUFChange) Action() string { |
|
58 | 58 |
return c.Actn |
59 | 59 |
} |
60 | 60 |
|
61 | 61 |
// Scope returns c.Role |
62 |
-func (c TufChange) Scope() string { |
|
62 |
+func (c TUFChange) Scope() string { |
|
63 | 63 |
return c.Role |
64 | 64 |
} |
65 | 65 |
|
66 | 66 |
// Type returns c.ChangeType |
67 |
-func (c TufChange) Type() string { |
|
67 |
+func (c TUFChange) Type() string { |
|
68 | 68 |
return c.ChangeType |
69 | 69 |
} |
70 | 70 |
|
71 | 71 |
// Path return c.ChangePath |
72 |
-func (c TufChange) Path() string { |
|
72 |
+func (c TUFChange) Path() string { |
|
73 | 73 |
return c.ChangePath |
74 | 74 |
} |
75 | 75 |
|
76 | 76 |
// Content returns c.Data |
77 |
-func (c TufChange) Content() []byte { |
|
77 |
+func (c TUFChange) Content() []byte { |
|
78 | 78 |
return c.Data |
79 | 79 |
} |
80 | 80 |
|
81 |
-// TufDelegation represents a modification to a target delegation |
|
81 |
+// TUFDelegation represents a modification to a target delegation |
|
82 | 82 |
// this includes creating a delegations. This format is used to avoid |
83 | 83 |
// unexpected race conditions between humans modifying the same delegation |
84 |
-type TufDelegation struct { |
|
84 |
+type TUFDelegation struct { |
|
85 | 85 |
NewName string `json:"new_name,omitempty"` |
86 | 86 |
NewThreshold int `json:"threshold, omitempty"` |
87 | 87 |
AddKeys data.KeyList `json:"add_keys, omitempty"` |
... | ... |
@@ -91,8 +92,8 @@ type TufDelegation struct { |
91 | 91 |
ClearAllPaths bool `json:"clear_paths,omitempty"` |
92 | 92 |
} |
93 | 93 |
|
94 |
-// ToNewRole creates a fresh role object from the TufDelegation data |
|
95 |
-func (td TufDelegation) ToNewRole(scope string) (*data.Role, error) { |
|
94 |
+// ToNewRole creates a fresh role object from the TUFDelegation data |
|
95 |
+func (td TUFDelegation) ToNewRole(scope string) (*data.Role, error) { |
|
96 | 96 |
name := scope |
97 | 97 |
if td.NewName != "" { |
98 | 98 |
name = td.NewName |
... | ... |
@@ -21,6 +21,24 @@ func (cl *memChangelist) Add(c Change) error { |
21 | 21 |
return nil |
22 | 22 |
} |
23 | 23 |
|
24 |
+// Remove deletes the changes found at the given indices |
|
25 |
+func (cl *memChangelist) Remove(idxs []int) error { |
|
26 |
+ remove := make(map[int]struct{}) |
|
27 |
+ for _, i := range idxs { |
|
28 |
+ remove[i] = struct{}{} |
|
29 |
+ } |
|
30 |
+ var keep []Change |
|
31 |
+ |
|
32 |
+ for i, c := range cl.changes { |
|
33 |
+ if _, ok := remove[i]; ok { |
|
34 |
+ continue |
|
35 |
+ } |
|
36 |
+ keep = append(keep, c) |
|
37 |
+ } |
|
38 |
+ cl.changes = keep |
|
39 |
+ return nil |
|
40 |
+} |
|
41 |
+ |
|
24 | 42 |
// Clear empties the changelist file. |
25 | 43 |
func (cl *memChangelist) Clear(archive string) error { |
26 | 44 |
// appending to a nil list initializes it. |
... | ... |
@@ -5,12 +5,12 @@ import ( |
5 | 5 |
"fmt" |
6 | 6 |
"io/ioutil" |
7 | 7 |
"os" |
8 |
- "path" |
|
9 | 8 |
"sort" |
10 | 9 |
"time" |
11 | 10 |
|
12 | 11 |
"github.com/Sirupsen/logrus" |
13 | 12 |
"github.com/docker/distribution/uuid" |
13 |
+ "path/filepath" |
|
14 | 14 |
) |
15 | 15 |
|
16 | 16 |
// FileChangelist stores all the changes as files |
... | ... |
@@ -46,13 +46,14 @@ func getFileNames(dirName string) ([]os.FileInfo, error) { |
46 | 46 |
} |
47 | 47 |
fileInfos = append(fileInfos, f) |
48 | 48 |
} |
49 |
+ sort.Sort(fileChanges(fileInfos)) |
|
49 | 50 |
return fileInfos, nil |
50 | 51 |
} |
51 | 52 |
|
52 |
-// Read a JSON formatted file from disk; convert to TufChange struct |
|
53 |
-func unmarshalFile(dirname string, f os.FileInfo) (*TufChange, error) { |
|
54 |
- c := &TufChange{} |
|
55 |
- raw, err := ioutil.ReadFile(path.Join(dirname, f.Name())) |
|
53 |
+// Read a JSON formatted file from disk; convert to TUFChange struct |
|
54 |
+func unmarshalFile(dirname string, f os.FileInfo) (*TUFChange, error) { |
|
55 |
+ c := &TUFChange{} |
|
56 |
+ raw, err := ioutil.ReadFile(filepath.Join(dirname, f.Name())) |
|
56 | 57 |
if err != nil { |
57 | 58 |
return c, err |
58 | 59 |
} |
... | ... |
@@ -70,7 +71,6 @@ func (cl FileChangelist) List() []Change { |
70 | 70 |
if err != nil { |
71 | 71 |
return changes |
72 | 72 |
} |
73 |
- sort.Sort(fileChanges(fileInfos)) |
|
74 | 73 |
for _, f := range fileInfos { |
75 | 74 |
c, err := unmarshalFile(cl.dir, f) |
76 | 75 |
if err != nil { |
... | ... |
@@ -89,10 +89,32 @@ func (cl FileChangelist) Add(c Change) error { |
89 | 89 |
return err |
90 | 90 |
} |
91 | 91 |
filename := fmt.Sprintf("%020d_%s.change", time.Now().UnixNano(), uuid.Generate()) |
92 |
- return ioutil.WriteFile(path.Join(cl.dir, filename), cJSON, 0644) |
|
92 |
+ return ioutil.WriteFile(filepath.Join(cl.dir, filename), cJSON, 0644) |
|
93 |
+} |
|
94 |
+ |
|
95 |
+// Remove deletes the changes found at the given indices |
|
96 |
+func (cl FileChangelist) Remove(idxs []int) error { |
|
97 |
+ fileInfos, err := getFileNames(cl.dir) |
|
98 |
+ if err != nil { |
|
99 |
+ return err |
|
100 |
+ } |
|
101 |
+ remove := make(map[int]struct{}) |
|
102 |
+ for _, i := range idxs { |
|
103 |
+ remove[i] = struct{}{} |
|
104 |
+ } |
|
105 |
+ for i, c := range fileInfos { |
|
106 |
+ if _, ok := remove[i]; ok { |
|
107 |
+ file := filepath.Join(cl.dir, c.Name()) |
|
108 |
+ if err := os.Remove(file); err != nil { |
|
109 |
+ logrus.Errorf("could not remove change %d: %s", i, err.Error()) |
|
110 |
+ } |
|
111 |
+ } |
|
112 |
+ } |
|
113 |
+ return nil |
|
93 | 114 |
} |
94 | 115 |
|
95 | 116 |
// Clear clears the change list |
117 |
+// N.B. archiving not currently implemented |
|
96 | 118 |
func (cl FileChangelist) Clear(archive string) error { |
97 | 119 |
dir, err := os.Open(cl.dir) |
98 | 120 |
if err != nil { |
... | ... |
@@ -104,7 +126,7 @@ func (cl FileChangelist) Clear(archive string) error { |
104 | 104 |
return err |
105 | 105 |
} |
106 | 106 |
for _, f := range files { |
107 |
- os.Remove(path.Join(cl.dir, f.Name())) |
|
107 |
+ os.Remove(filepath.Join(cl.dir, f.Name())) |
|
108 | 108 |
} |
109 | 109 |
return nil |
110 | 110 |
} |
... | ... |
@@ -121,7 +143,6 @@ func (cl FileChangelist) NewIterator() (ChangeIterator, error) { |
121 | 121 |
if err != nil { |
122 | 122 |
return &FileChangeListIterator{}, err |
123 | 123 |
} |
124 |
- sort.Sort(fileChanges(fileInfos)) |
|
125 | 124 |
return &FileChangeListIterator{dirname: cl.dir, collection: fileInfos}, nil |
126 | 125 |
} |
127 | 126 |
|
... | ... |
@@ -15,6 +15,9 @@ type Changelist interface { |
15 | 15 |
// to save a copy of the changelist in that location |
16 | 16 |
Clear(archive string) error |
17 | 17 |
|
18 |
+ // Remove deletes the changes corresponding with the indices given |
|
19 |
+ Remove(idxs []int) error |
|
20 |
+ |
|
18 | 21 |
// Close syncronizes any pending writes to the underlying |
19 | 22 |
// storage and closes the file/connection |
20 | 23 |
Close() error |
... | ... |
@@ -16,13 +16,12 @@ import ( |
16 | 16 |
"github.com/docker/notary" |
17 | 17 |
"github.com/docker/notary/client/changelist" |
18 | 18 |
"github.com/docker/notary/cryptoservice" |
19 |
+ store "github.com/docker/notary/storage" |
|
19 | 20 |
"github.com/docker/notary/trustmanager" |
20 | 21 |
"github.com/docker/notary/trustpinning" |
21 | 22 |
"github.com/docker/notary/tuf" |
22 |
- tufclient "github.com/docker/notary/tuf/client" |
|
23 | 23 |
"github.com/docker/notary/tuf/data" |
24 | 24 |
"github.com/docker/notary/tuf/signed" |
25 |
- "github.com/docker/notary/tuf/store" |
|
26 | 25 |
"github.com/docker/notary/tuf/utils" |
27 | 26 |
) |
28 | 27 |
|
... | ... |
@@ -85,6 +84,7 @@ type NotaryRepository struct { |
85 | 85 |
fileStore store.MetadataStore |
86 | 86 |
CryptoService signed.CryptoService |
87 | 87 |
tufRepo *tuf.Repo |
88 |
+ invalid *tuf.Repo // known data that was parsable but deemed invalid |
|
88 | 89 |
roundTrip http.RoundTripper |
89 | 90 |
trustPinning trustpinning.TrustPinConfig |
90 | 91 |
} |
... | ... |
@@ -121,7 +121,7 @@ func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper, |
121 | 121 |
} |
122 | 122 |
|
123 | 123 |
// Target represents a simplified version of the data TUF operates on, so external |
124 |
-// applications don't have to depend on tuf data types. |
|
124 |
+// applications don't have to depend on TUF data types. |
|
125 | 125 |
type Target struct { |
126 | 126 |
Name string // the name of the target |
127 | 127 |
Hashes data.Hashes // the hash of the target |
... | ... |
@@ -159,7 +159,7 @@ func rootCertKey(gun string, privKey data.PrivateKey) (data.PublicKey, error) { |
159 | 159 |
return nil, err |
160 | 160 |
} |
161 | 161 |
|
162 |
- x509PublicKey := trustmanager.CertToKey(cert) |
|
162 |
+ x509PublicKey := utils.CertToKey(cert) |
|
163 | 163 |
if x509PublicKey == nil { |
164 | 164 |
return nil, fmt.Errorf( |
165 | 165 |
"cannot use regenerated certificate: format %s", cert.PublicKeyAlgorithm) |
... | ... |
@@ -173,10 +173,14 @@ func rootCertKey(gun string, privKey data.PrivateKey) (data.PublicKey, error) { |
173 | 173 |
// timestamp key and possibly other serverManagedRoles), but the created repository |
174 | 174 |
// result is only stored on local disk, not published to the server. To do that, |
175 | 175 |
// use r.Publish() eventually. |
176 |
-func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...string) error { |
|
177 |
- privKey, _, err := r.CryptoService.GetPrivateKey(rootKeyID) |
|
178 |
- if err != nil { |
|
179 |
- return err |
|
176 |
+func (r *NotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...string) error { |
|
177 |
+ privKeys := make([]data.PrivateKey, 0, len(rootKeyIDs)) |
|
178 |
+ for _, keyID := range rootKeyIDs { |
|
179 |
+ privKey, _, err := r.CryptoService.GetPrivateKey(keyID) |
|
180 |
+ if err != nil { |
|
181 |
+ return err |
|
182 |
+ } |
|
183 |
+ privKeys = append(privKeys, privKey) |
|
180 | 184 |
} |
181 | 185 |
|
182 | 186 |
// currently we only support server managing timestamps and snapshots, and |
... | ... |
@@ -206,16 +210,20 @@ func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...st |
206 | 206 |
} |
207 | 207 |
} |
208 | 208 |
|
209 |
- rootKey, err := rootCertKey(r.gun, privKey) |
|
210 |
- if err != nil { |
|
211 |
- return err |
|
209 |
+ rootKeys := make([]data.PublicKey, 0, len(privKeys)) |
|
210 |
+ for _, privKey := range privKeys { |
|
211 |
+ rootKey, err := rootCertKey(r.gun, privKey) |
|
212 |
+ if err != nil { |
|
213 |
+ return err |
|
214 |
+ } |
|
215 |
+ rootKeys = append(rootKeys, rootKey) |
|
212 | 216 |
} |
213 | 217 |
|
214 | 218 |
var ( |
215 | 219 |
rootRole = data.NewBaseRole( |
216 | 220 |
data.CanonicalRootRole, |
217 | 221 |
notary.MinThreshold, |
218 |
- rootKey, |
|
222 |
+ rootKeys..., |
|
219 | 223 |
) |
220 | 224 |
timestampRole data.BaseRole |
221 | 225 |
snapshotRole data.BaseRole |
... | ... |
@@ -271,7 +279,7 @@ func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...st |
271 | 271 |
|
272 | 272 |
r.tufRepo = tuf.NewRepo(r.CryptoService) |
273 | 273 |
|
274 |
- err = r.tufRepo.InitRoot( |
|
274 |
+ err := r.tufRepo.InitRoot( |
|
275 | 275 |
rootRole, |
276 | 276 |
timestampRole, |
277 | 277 |
snapshotRole, |
... | ... |
@@ -307,14 +315,14 @@ func addChange(cl *changelist.FileChangelist, c changelist.Change, roles ...stri |
307 | 307 |
for _, role := range roles { |
308 | 308 |
// Ensure we can only add targets to the CanonicalTargetsRole, |
309 | 309 |
// or a Delegation role (which is <CanonicalTargetsRole>/something else) |
310 |
- if role != data.CanonicalTargetsRole && !data.IsDelegation(role) { |
|
310 |
+ if role != data.CanonicalTargetsRole && !data.IsDelegation(role) && !data.IsWildDelegation(role) { |
|
311 | 311 |
return data.ErrInvalidRole{ |
312 | 312 |
Role: role, |
313 | 313 |
Reason: "cannot add targets to this role", |
314 | 314 |
} |
315 | 315 |
} |
316 | 316 |
|
317 |
- changes = append(changes, changelist.NewTufChange( |
|
317 |
+ changes = append(changes, changelist.NewTUFChange( |
|
318 | 318 |
c.Action(), |
319 | 319 |
role, |
320 | 320 |
c.Type(), |
... | ... |
@@ -352,7 +360,7 @@ func (r *NotaryRepository) AddTarget(target *Target, roles ...string) error { |
352 | 352 |
return err |
353 | 353 |
} |
354 | 354 |
|
355 |
- template := changelist.NewTufChange( |
|
355 |
+ template := changelist.NewTUFChange( |
|
356 | 356 |
changelist.ActionCreate, "", changelist.TypeTargetsTarget, |
357 | 357 |
target.Name, metaJSON) |
358 | 358 |
return addChange(cl, template, roles...) |
... | ... |
@@ -368,13 +376,14 @@ func (r *NotaryRepository) RemoveTarget(targetName string, roles ...string) erro |
368 | 368 |
return err |
369 | 369 |
} |
370 | 370 |
logrus.Debugf("Removing target \"%s\"", targetName) |
371 |
- template := changelist.NewTufChange(changelist.ActionDelete, "", |
|
371 |
+ template := changelist.NewTUFChange(changelist.ActionDelete, "", |
|
372 | 372 |
changelist.TypeTargetsTarget, targetName, nil) |
373 | 373 |
return addChange(cl, template, roles...) |
374 | 374 |
} |
375 | 375 |
|
376 | 376 |
// ListTargets lists all targets for the current repository. The list of |
377 | 377 |
// roles should be passed in order from highest to lowest priority. |
378 |
+// |
|
378 | 379 |
// IMPORTANT: if you pass a set of roles such as [ "targets/a", "targets/x" |
379 | 380 |
// "targets/a/b" ], even though "targets/a/b" is part of the "targets/a" subtree |
380 | 381 |
// its entries will be strictly shadowed by those in other parts of the "targets/a" |
... | ... |
@@ -402,11 +411,18 @@ func (r *NotaryRepository) ListTargets(roles ...string) ([]*TargetWithRole, erro |
402 | 402 |
if _, ok := targets[targetName]; ok || !validRole.CheckPaths(targetName) { |
403 | 403 |
continue |
404 | 404 |
} |
405 |
- targets[targetName] = |
|
406 |
- &TargetWithRole{Target: Target{Name: targetName, Hashes: targetMeta.Hashes, Length: targetMeta.Length}, Role: validRole.Name} |
|
405 |
+ targets[targetName] = &TargetWithRole{ |
|
406 |
+ Target: Target{ |
|
407 |
+ Name: targetName, |
|
408 |
+ Hashes: targetMeta.Hashes, |
|
409 |
+ Length: targetMeta.Length, |
|
410 |
+ }, |
|
411 |
+ Role: validRole.Name, |
|
412 |
+ } |
|
407 | 413 |
} |
408 | 414 |
return nil |
409 | 415 |
} |
416 |
+ |
|
410 | 417 |
r.tufRepo.WalkTargets("", role, listVisitorFunc, skipRoles...) |
411 | 418 |
} |
412 | 419 |
|
... | ... |
@@ -462,6 +478,62 @@ func (r *NotaryRepository) GetTargetByName(name string, roles ...string) (*Targe |
462 | 462 |
|
463 | 463 |
} |
464 | 464 |
|
465 |
+// TargetSignedStruct is a struct that contains a Target, the role it was found in, and the list of signatures for that role |
|
466 |
+type TargetSignedStruct struct { |
|
467 |
+ Role data.DelegationRole |
|
468 |
+ Target Target |
|
469 |
+ Signatures []data.Signature |
|
470 |
+} |
|
471 |
+ |
|
472 |
+// GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all |
|
473 |
+// roles, and returns a list of TargetSignedStructs for each time it finds the specified target. |
|
474 |
+// If given an empty string for a target name, it will return back all targets signed into the repository in every role |
|
475 |
+func (r *NotaryRepository) GetAllTargetMetadataByName(name string) ([]TargetSignedStruct, error) { |
|
476 |
+ if err := r.Update(false); err != nil { |
|
477 |
+ return nil, err |
|
478 |
+ } |
|
479 |
+ |
|
480 |
+ var targetInfoList []TargetSignedStruct |
|
481 |
+ |
|
482 |
+ // Define a visitor function to find the specified target |
|
483 |
+ getAllTargetInfoByNameVisitorFunc := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { |
|
484 |
+ if tgt == nil { |
|
485 |
+ return nil |
|
486 |
+ } |
|
487 |
+ // We found a target and validated path compatibility in our walk, |
|
488 |
+ // so add it to our list if we have a match |
|
489 |
+ // if we have an empty name, add all targets, else check if we have it |
|
490 |
+ var targetMetaToAdd data.Files |
|
491 |
+ if name == "" { |
|
492 |
+ targetMetaToAdd = tgt.Signed.Targets |
|
493 |
+ } else { |
|
494 |
+ if meta, ok := tgt.Signed.Targets[name]; ok { |
|
495 |
+ targetMetaToAdd = data.Files{name: meta} |
|
496 |
+ } |
|
497 |
+ } |
|
498 |
+ |
|
499 |
+ for targetName, resultMeta := range targetMetaToAdd { |
|
500 |
+ targetInfo := TargetSignedStruct{ |
|
501 |
+ Role: validRole, |
|
502 |
+ Target: Target{Name: targetName, Hashes: resultMeta.Hashes, Length: resultMeta.Length}, |
|
503 |
+ Signatures: tgt.Signatures, |
|
504 |
+ } |
|
505 |
+ targetInfoList = append(targetInfoList, targetInfo) |
|
506 |
+ } |
|
507 |
+ // continue walking to all child roles |
|
508 |
+ return nil |
|
509 |
+ } |
|
510 |
+ |
|
511 |
+ // Check that we didn't error, and that we found the target at least once |
|
512 |
+ if err := r.tufRepo.WalkTargets(name, "", getAllTargetInfoByNameVisitorFunc); err != nil { |
|
513 |
+ return nil, err |
|
514 |
+ } |
|
515 |
+ if len(targetInfoList) == 0 { |
|
516 |
+ return nil, fmt.Errorf("No valid trust data for %s", name) |
|
517 |
+ } |
|
518 |
+ return targetInfoList, nil |
|
519 |
+} |
|
520 |
+ |
|
465 | 521 |
// GetChangelist returns the list of the repository's unpublished changes |
466 | 522 |
func (r *NotaryRepository) GetChangelist() (changelist.Changelist, error) { |
467 | 523 |
changelistDir := filepath.Join(r.tufRepoPath, "changelist") |
... | ... |
@@ -567,19 +639,19 @@ func (r *NotaryRepository) publish(cl changelist.Changelist) error { |
567 | 567 |
} |
568 | 568 |
} |
569 | 569 |
// apply the changelist to the repo |
570 |
- if err := applyChangelist(r.tufRepo, cl); err != nil { |
|
570 |
+ if err := applyChangelist(r.tufRepo, r.invalid, cl); err != nil { |
|
571 | 571 |
logrus.Debug("Error applying changelist") |
572 | 572 |
return err |
573 | 573 |
} |
574 | 574 |
|
575 |
- // these are the tuf files we will need to update, serialized as JSON before |
|
575 |
+ // these are the TUF files we will need to update, serialized as JSON before |
|
576 | 576 |
// we send anything to remote |
577 | 577 |
updatedFiles := make(map[string][]byte) |
578 | 578 |
|
579 | 579 |
// check if our root file is nearing expiry or dirty. Resign if it is. If |
580 | 580 |
// root is not dirty but we are publishing for the first time, then just |
581 | 581 |
// publish the existing root we have. |
582 |
- if nearExpiry(r.tufRepo.Root) || r.tufRepo.Root.Dirty { |
|
582 |
+ if nearExpiry(r.tufRepo.Root.Signed.SignedCommon) || r.tufRepo.Root.Dirty { |
|
583 | 583 |
rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole) |
584 | 584 |
if err != nil { |
585 | 585 |
return err |
... | ... |
@@ -635,7 +707,7 @@ func (r *NotaryRepository) publish(cl changelist.Changelist) error { |
635 | 635 |
return err |
636 | 636 |
} |
637 | 637 |
|
638 |
- return remote.SetMultiMeta(updatedFiles) |
|
638 |
+ return remote.SetMulti(updatedFiles) |
|
639 | 639 |
} |
640 | 640 |
|
641 | 641 |
// bootstrapRepo loads the repository from the local file system (i.e. |
... | ... |
@@ -649,7 +721,7 @@ func (r *NotaryRepository) bootstrapRepo() error { |
649 | 649 |
logrus.Debugf("Loading trusted collection.") |
650 | 650 |
|
651 | 651 |
for _, role := range data.BaseRoles { |
652 |
- jsonBytes, err := r.fileStore.GetMeta(role, store.NoSizeLimit) |
|
652 |
+ jsonBytes, err := r.fileStore.GetSized(role, store.NoSizeLimit) |
|
653 | 653 |
if err != nil { |
654 | 654 |
if _, ok := err.(store.ErrMetaNotFound); ok && |
655 | 655 |
// server snapshots are supported, and server timestamp management |
... | ... |
@@ -665,7 +737,7 @@ func (r *NotaryRepository) bootstrapRepo() error { |
665 | 665 |
} |
666 | 666 |
} |
667 | 667 |
|
668 |
- tufRepo, err := b.Finish() |
|
668 |
+ tufRepo, _, err := b.Finish() |
|
669 | 669 |
if err == nil { |
670 | 670 |
r.tufRepo = tufRepo |
671 | 671 |
} |
... | ... |
@@ -681,7 +753,7 @@ func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error { |
681 | 681 |
if err != nil { |
682 | 682 |
return err |
683 | 683 |
} |
684 |
- err = r.fileStore.SetMeta(data.CanonicalRootRole, rootJSON) |
|
684 |
+ err = r.fileStore.Set(data.CanonicalRootRole, rootJSON) |
|
685 | 685 |
if err != nil { |
686 | 686 |
return err |
687 | 687 |
} |
... | ... |
@@ -702,7 +774,7 @@ func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error { |
702 | 702 |
for role, blob := range targetsToSave { |
703 | 703 |
parentDir := filepath.Dir(role) |
704 | 704 |
os.MkdirAll(parentDir, 0755) |
705 |
- r.fileStore.SetMeta(role, blob) |
|
705 |
+ r.fileStore.Set(role, blob) |
|
706 | 706 |
} |
707 | 707 |
|
708 | 708 |
if ignoreSnapshot { |
... | ... |
@@ -714,7 +786,7 @@ func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error { |
714 | 714 |
return err |
715 | 715 |
} |
716 | 716 |
|
717 |
- return r.fileStore.SetMeta(data.CanonicalSnapshotRole, snapshotJSON) |
|
717 |
+ return r.fileStore.Set(data.CanonicalSnapshotRole, snapshotJSON) |
|
718 | 718 |
} |
719 | 719 |
|
720 | 720 |
// returns a properly constructed ErrRepositoryNotExist error based on this |
... | ... |
@@ -738,7 +810,7 @@ func (r *NotaryRepository) Update(forWrite bool) error { |
738 | 738 |
} |
739 | 739 |
return err |
740 | 740 |
} |
741 |
- repo, err := c.Update() |
|
741 |
+ repo, invalid, err := c.Update() |
|
742 | 742 |
if err != nil { |
743 | 743 |
// notFound.Resource may include a checksum so when the role is root, |
744 | 744 |
// it will be root or root.<checksum>. Therefore best we can |
... | ... |
@@ -748,7 +820,11 @@ func (r *NotaryRepository) Update(forWrite bool) error { |
748 | 748 |
} |
749 | 749 |
return err |
750 | 750 |
} |
751 |
+ // we can be assured if we are at this stage that the repo we built is good |
|
752 |
+ // no need to test the following function call for an error as it will always be fine should the repo be good- it is! |
|
751 | 753 |
r.tufRepo = repo |
754 |
+ r.invalid = invalid |
|
755 |
+ warnRolesNearExpiry(repo) |
|
752 | 756 |
return nil |
753 | 757 |
} |
754 | 758 |
|
... | ... |
@@ -759,16 +835,16 @@ func (r *NotaryRepository) Update(forWrite bool) error { |
759 | 759 |
// and return an error if the remote repository errors. |
760 | 760 |
// |
761 | 761 |
// Populates a tuf.RepoBuilder with this root metadata (only use |
762 |
-// tufclient.Client.Update to load the rest). |
|
762 |
+// TUFClient.Update to load the rest). |
|
763 | 763 |
// |
764 | 764 |
// Fails if the remote server is reachable and does not know the repo |
765 | 765 |
// (i.e. before the first r.Publish()), in which case the error is |
766 | 766 |
// store.ErrMetaNotFound, or if the root metadata (from whichever source is used) |
767 | 767 |
// is not trusted. |
768 | 768 |
// |
769 |
-// Returns a tufclient.Client for the remote server, which may not be actually |
|
769 |
+// Returns a TUFClient for the remote server, which may not be actually |
|
770 | 770 |
// operational (if the URL is invalid but a root.json is cached). |
771 |
-func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Client, error) { |
|
771 |
+func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*TUFClient, error) { |
|
772 | 772 |
minVersion := 1 |
773 | 773 |
// the old root on disk should not be validated against any trust pinning configuration |
774 | 774 |
// because if we have an old root, it itself is the thing that pins trust |
... | ... |
@@ -781,7 +857,7 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl |
781 | 781 |
// during update which will cause us to download a new root and perform a rotation. |
782 | 782 |
// If we have an old root, and it's valid, then we overwrite the newBuilder to be one |
783 | 783 |
// preloaded with the old root or one which uses the old root for trust bootstrapping. |
784 |
- if rootJSON, err := r.fileStore.GetMeta(data.CanonicalRootRole, store.NoSizeLimit); err == nil { |
|
784 |
+ if rootJSON, err := r.fileStore.GetSized(data.CanonicalRootRole, store.NoSizeLimit); err == nil { |
|
785 | 785 |
// if we can't load the cached root, fail hard because that is how we pin trust |
786 | 786 |
if err := oldBuilder.Load(data.CanonicalRootRole, rootJSON, minVersion, true); err != nil { |
787 | 787 |
return nil, err |
... | ... |
@@ -794,8 +870,9 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl |
794 | 794 |
if err := newBuilder.Load(data.CanonicalRootRole, rootJSON, minVersion, false); err != nil { |
795 | 795 |
// Ok, the old root is expired - we want to download a new one. But we want to use the |
796 | 796 |
// old root to verify the new root, so bootstrap a new builder with the old builder |
797 |
+ // but use the trustpinning to validate the new root |
|
797 | 798 |
minVersion = oldBuilder.GetLoadedVersion(data.CanonicalRootRole) |
798 |
- newBuilder = oldBuilder.BootstrapNewBuilder() |
|
799 |
+ newBuilder = oldBuilder.BootstrapNewBuilderWithNewTrustpin(r.trustPinning) |
|
799 | 800 |
} |
800 | 801 |
} |
801 | 802 |
|
... | ... |
@@ -808,7 +885,7 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl |
808 | 808 |
|
809 | 809 |
// if remote store successfully set up, try and get root from remote |
810 | 810 |
// We don't have any local data to determine the size of root, so try the maximum (though it is restricted at 100MB) |
811 |
- tmpJSON, err := remote.GetMeta(data.CanonicalRootRole, store.NoSizeLimit) |
|
811 |
+ tmpJSON, err := remote.GetSized(data.CanonicalRootRole, store.NoSizeLimit) |
|
812 | 812 |
if err != nil { |
813 | 813 |
// we didn't have a root in cache and were unable to load one from |
814 | 814 |
// the server. Nothing we can do but error. |
... | ... |
@@ -821,7 +898,7 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl |
821 | 821 |
return nil, err |
822 | 822 |
} |
823 | 823 |
|
824 |
- err = r.fileStore.SetMeta(data.CanonicalRootRole, tmpJSON) |
|
824 |
+ err = r.fileStore.Set(data.CanonicalRootRole, tmpJSON) |
|
825 | 825 |
if err != nil { |
826 | 826 |
// if we can't write cache we should still continue, just log error |
827 | 827 |
logrus.Errorf("could not save root to cache: %s", err.Error()) |
... | ... |
@@ -835,7 +912,7 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl |
835 | 835 |
return nil, ErrRepoNotInitialized{} |
836 | 836 |
} |
837 | 837 |
|
838 |
- return tufclient.NewClient(oldBuilder, newBuilder, remote, r.fileStore), nil |
|
838 |
+ return NewTUFClient(oldBuilder, newBuilder, remote, r.fileStore), nil |
|
839 | 839 |
} |
840 | 840 |
|
841 | 841 |
// RotateKey removes all existing keys associated with the role, and either |
... | ... |
@@ -864,7 +941,7 @@ func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool) error { |
864 | 864 |
) |
865 | 865 |
switch serverManagesKey { |
866 | 866 |
case true: |
867 |
- pubKey, err = getRemoteKey(r.baseURL, r.gun, role, r.roundTrip) |
|
867 |
+ pubKey, err = rotateRemoteKey(r.baseURL, r.gun, role, r.roundTrip) |
|
868 | 868 |
errFmtMsg = "unable to rotate remote key: %s" |
869 | 869 |
default: |
870 | 870 |
pubKey, err = r.CryptoService.Create(role, r.gun, data.ECDSAKey) |
... | ... |
@@ -897,7 +974,7 @@ func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool) error { |
897 | 897 |
func (r *NotaryRepository) rootFileKeyChange(cl changelist.Changelist, role, action string, key data.PublicKey) error { |
898 | 898 |
kl := make(data.KeyList, 0, 1) |
899 | 899 |
kl = append(kl, key) |
900 |
- meta := changelist.TufRootData{ |
|
900 |
+ meta := changelist.TUFRootData{ |
|
901 | 901 |
RoleName: role, |
902 | 902 |
Keys: kl, |
903 | 903 |
} |
... | ... |
@@ -906,7 +983,7 @@ func (r *NotaryRepository) rootFileKeyChange(cl changelist.Changelist, role, act |
906 | 906 |
return err |
907 | 907 |
} |
908 | 908 |
|
909 |
- c := changelist.NewTufChange( |
|
909 |
+ c := changelist.NewTUFChange( |
|
910 | 910 |
action, |
911 | 911 |
changelist.ScopeRoot, |
912 | 912 |
changelist.TypeRootRole, |
... | ... |
@@ -917,11 +994,21 @@ func (r *NotaryRepository) rootFileKeyChange(cl changelist.Changelist, role, act |
917 | 917 |
} |
918 | 918 |
|
919 | 919 |
// DeleteTrustData removes the trust data stored for this repo in the TUF cache on the client side |
920 |
-func (r *NotaryRepository) DeleteTrustData() error { |
|
921 |
- // Clear TUF files and cache |
|
922 |
- if err := r.fileStore.RemoveAll(); err != nil { |
|
920 |
+// Note that we will not delete any private key material from local storage |
|
921 |
+func (r *NotaryRepository) DeleteTrustData(deleteRemote bool) error { |
|
922 |
+ // Remove the tufRepoPath directory, which includes local TUF metadata files and changelist information |
|
923 |
+ if err := os.RemoveAll(r.tufRepoPath); err != nil { |
|
923 | 924 |
return fmt.Errorf("error clearing TUF repo data: %v", err) |
924 | 925 |
} |
925 |
- r.tufRepo = tuf.NewRepo(nil) |
|
926 |
+ // Note that this will require admin permission in this NotaryRepository's roundtripper |
|
927 |
+ if deleteRemote { |
|
928 |
+ remote, err := getRemoteStore(r.baseURL, r.gun, r.roundTrip) |
|
929 |
+ if err != nil { |
|
930 |
+ return err |
|
931 |
+ } |
|
932 |
+ if err := remote.RemoveAll(); err != nil { |
|
933 |
+ return err |
|
934 |
+ } |
|
935 |
+ } |
|
926 | 936 |
return nil |
927 | 937 |
} |
... | ... |
@@ -8,8 +8,8 @@ import ( |
8 | 8 |
"github.com/Sirupsen/logrus" |
9 | 9 |
"github.com/docker/notary" |
10 | 10 |
"github.com/docker/notary/client/changelist" |
11 |
+ store "github.com/docker/notary/storage" |
|
11 | 12 |
"github.com/docker/notary/tuf/data" |
12 |
- "github.com/docker/notary/tuf/store" |
|
13 | 13 |
"github.com/docker/notary/tuf/utils" |
14 | 14 |
) |
15 | 15 |
|
... | ... |
@@ -50,7 +50,7 @@ func (r *NotaryRepository) AddDelegationRoleAndKeys(name string, delegationKeys |
50 | 50 |
name, notary.MinThreshold, len(delegationKeys)) |
51 | 51 |
|
52 | 52 |
// Defaulting to threshold of 1, since we don't allow for larger thresholds at the moment. |
53 |
- tdJSON, err := json.Marshal(&changelist.TufDelegation{ |
|
53 |
+ tdJSON, err := json.Marshal(&changelist.TUFDelegation{ |
|
54 | 54 |
NewThreshold: notary.MinThreshold, |
55 | 55 |
AddKeys: data.KeyList(delegationKeys), |
56 | 56 |
}) |
... | ... |
@@ -78,7 +78,7 @@ func (r *NotaryRepository) AddDelegationPaths(name string, paths []string) error |
78 | 78 |
|
79 | 79 |
logrus.Debugf(`Adding %s paths to delegation %s\n`, paths, name) |
80 | 80 |
|
81 |
- tdJSON, err := json.Marshal(&changelist.TufDelegation{ |
|
81 |
+ tdJSON, err := json.Marshal(&changelist.TUFDelegation{ |
|
82 | 82 |
AddPaths: paths, |
83 | 83 |
}) |
84 | 84 |
if err != nil { |
... | ... |
@@ -141,7 +141,7 @@ func (r *NotaryRepository) RemoveDelegationPaths(name string, paths []string) er |
141 | 141 |
|
142 | 142 |
logrus.Debugf(`Removing %s paths from delegation "%s"\n`, paths, name) |
143 | 143 |
|
144 |
- tdJSON, err := json.Marshal(&changelist.TufDelegation{ |
|
144 |
+ tdJSON, err := json.Marshal(&changelist.TUFDelegation{ |
|
145 | 145 |
RemovePaths: paths, |
146 | 146 |
}) |
147 | 147 |
if err != nil { |
... | ... |
@@ -155,9 +155,11 @@ func (r *NotaryRepository) RemoveDelegationPaths(name string, paths []string) er |
155 | 155 |
// RemoveDelegationKeys creates a changelist entry to remove provided keys from an existing delegation. |
156 | 156 |
// When this changelist is applied, if the specified keys are the only keys left in the role, |
157 | 157 |
// the role itself will be deleted in its entirety. |
158 |
+// It can also delete a key from all delegations under a parent using a name |
|
159 |
+// with a wildcard at the end. |
|
158 | 160 |
func (r *NotaryRepository) RemoveDelegationKeys(name string, keyIDs []string) error { |
159 | 161 |
|
160 |
- if !data.IsDelegation(name) { |
|
162 |
+ if !data.IsDelegation(name) && !data.IsWildDelegation(name) { |
|
161 | 163 |
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} |
162 | 164 |
} |
163 | 165 |
|
... | ... |
@@ -169,7 +171,7 @@ func (r *NotaryRepository) RemoveDelegationKeys(name string, keyIDs []string) er |
169 | 169 |
|
170 | 170 |
logrus.Debugf(`Removing %s keys from delegation "%s"\n`, keyIDs, name) |
171 | 171 |
|
172 |
- tdJSON, err := json.Marshal(&changelist.TufDelegation{ |
|
172 |
+ tdJSON, err := json.Marshal(&changelist.TUFDelegation{ |
|
173 | 173 |
RemoveKeys: keyIDs, |
174 | 174 |
}) |
175 | 175 |
if err != nil { |
... | ... |
@@ -195,7 +197,7 @@ func (r *NotaryRepository) ClearDelegationPaths(name string) error { |
195 | 195 |
|
196 | 196 |
logrus.Debugf(`Removing all paths from delegation "%s"\n`, name) |
197 | 197 |
|
198 |
- tdJSON, err := json.Marshal(&changelist.TufDelegation{ |
|
198 |
+ tdJSON, err := json.Marshal(&changelist.TUFDelegation{ |
|
199 | 199 |
ClearAllPaths: true, |
200 | 200 |
}) |
201 | 201 |
if err != nil { |
... | ... |
@@ -206,8 +208,8 @@ func (r *NotaryRepository) ClearDelegationPaths(name string) error { |
206 | 206 |
return addChange(cl, template, name) |
207 | 207 |
} |
208 | 208 |
|
209 |
-func newUpdateDelegationChange(name string, content []byte) *changelist.TufChange { |
|
210 |
- return changelist.NewTufChange( |
|
209 |
+func newUpdateDelegationChange(name string, content []byte) *changelist.TUFChange { |
|
210 |
+ return changelist.NewTUFChange( |
|
211 | 211 |
changelist.ActionUpdate, |
212 | 212 |
name, |
213 | 213 |
changelist.TypeTargetsDelegation, |
... | ... |
@@ -216,8 +218,8 @@ func newUpdateDelegationChange(name string, content []byte) *changelist.TufChang |
216 | 216 |
) |
217 | 217 |
} |
218 | 218 |
|
219 |
-func newCreateDelegationChange(name string, content []byte) *changelist.TufChange { |
|
220 |
- return changelist.NewTufChange( |
|
219 |
+func newCreateDelegationChange(name string, content []byte) *changelist.TUFChange { |
|
220 |
+ return changelist.NewTUFChange( |
|
221 | 221 |
changelist.ActionCreate, |
222 | 222 |
name, |
223 | 223 |
changelist.TypeTargetsDelegation, |
... | ... |
@@ -226,8 +228,8 @@ func newCreateDelegationChange(name string, content []byte) *changelist.TufChang |
226 | 226 |
) |
227 | 227 |
} |
228 | 228 |
|
229 |
-func newDeleteDelegationChange(name string, content []byte) *changelist.TufChange { |
|
230 |
- return changelist.NewTufChange( |
|
229 |
+func newDeleteDelegationChange(name string, content []byte) *changelist.TUFChange { |
|
230 |
+ return changelist.NewTUFChange( |
|
231 | 231 |
changelist.ActionDelete, |
232 | 232 |
name, |
233 | 233 |
changelist.TypeTargetsDelegation, |
... | ... |
@@ -238,7 +240,7 @@ func newDeleteDelegationChange(name string, content []byte) *changelist.TufChang |
238 | 238 |
|
239 | 239 |
// GetDelegationRoles returns the keys and roles of the repository's delegations |
240 | 240 |
// Also converts key IDs to canonical key IDs to keep consistent with signing prompts |
241 |
-func (r *NotaryRepository) GetDelegationRoles() ([]*data.Role, error) { |
|
241 |
+func (r *NotaryRepository) GetDelegationRoles() ([]data.Role, error) { |
|
242 | 242 |
// Update state of the repo to latest |
243 | 243 |
if err := r.Update(false); err != nil { |
244 | 244 |
return nil, err |
... | ... |
@@ -251,7 +253,7 @@ func (r *NotaryRepository) GetDelegationRoles() ([]*data.Role, error) { |
251 | 251 |
} |
252 | 252 |
|
253 | 253 |
// make a copy for traversing nested delegations |
254 |
- allDelegations := []*data.Role{} |
|
254 |
+ allDelegations := []data.Role{} |
|
255 | 255 |
|
256 | 256 |
// Define a visitor function to populate the delegations list and translate their key IDs to canonical IDs |
257 | 257 |
delegationCanonicalListVisitor := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { |
... | ... |
@@ -271,20 +273,23 @@ func (r *NotaryRepository) GetDelegationRoles() ([]*data.Role, error) { |
271 | 271 |
return allDelegations, nil |
272 | 272 |
} |
273 | 273 |
|
274 |
-func translateDelegationsToCanonicalIDs(delegationInfo data.Delegations) ([]*data.Role, error) { |
|
275 |
- canonicalDelegations := make([]*data.Role, len(delegationInfo.Roles)) |
|
276 |
- copy(canonicalDelegations, delegationInfo.Roles) |
|
274 |
+func translateDelegationsToCanonicalIDs(delegationInfo data.Delegations) ([]data.Role, error) { |
|
275 |
+ canonicalDelegations := make([]data.Role, len(delegationInfo.Roles)) |
|
276 |
+ // Do a copy by value to ensure local delegation metadata is untouched |
|
277 |
+ for idx, origRole := range delegationInfo.Roles { |
|
278 |
+ canonicalDelegations[idx] = *origRole |
|
279 |
+ } |
|
277 | 280 |
delegationKeys := delegationInfo.Keys |
278 | 281 |
for i, delegation := range canonicalDelegations { |
279 | 282 |
canonicalKeyIDs := []string{} |
280 | 283 |
for _, keyID := range delegation.KeyIDs { |
281 | 284 |
pubKey, ok := delegationKeys[keyID] |
282 | 285 |
if !ok { |
283 |
- return nil, fmt.Errorf("Could not translate canonical key IDs for %s", delegation.Name) |
|
286 |
+ return []data.Role{}, fmt.Errorf("Could not translate canonical key IDs for %s", delegation.Name) |
|
284 | 287 |
} |
285 | 288 |
canonicalKeyID, err := utils.CanonicalKeyID(pubKey) |
286 | 289 |
if err != nil { |
287 |
- return nil, fmt.Errorf("Could not translate canonical key IDs for %s: %v", delegation.Name, err) |
|
290 |
+ return []data.Role{}, fmt.Errorf("Could not translate canonical key IDs for %s: %v", delegation.Name, err) |
|
288 | 291 |
} |
289 | 292 |
canonicalKeyIDs = append(canonicalKeyIDs, canonicalKeyID) |
290 | 293 |
} |
... | ... |
@@ -4,14 +4,13 @@ import ( |
4 | 4 |
"encoding/json" |
5 | 5 |
"fmt" |
6 | 6 |
"net/http" |
7 |
- "strings" |
|
8 | 7 |
"time" |
9 | 8 |
|
10 | 9 |
"github.com/Sirupsen/logrus" |
11 | 10 |
"github.com/docker/notary/client/changelist" |
12 |
- tuf "github.com/docker/notary/tuf" |
|
11 |
+ store "github.com/docker/notary/storage" |
|
12 |
+ "github.com/docker/notary/tuf" |
|
13 | 13 |
"github.com/docker/notary/tuf/data" |
14 |
- "github.com/docker/notary/tuf/store" |
|
15 | 14 |
"github.com/docker/notary/tuf/utils" |
16 | 15 |
) |
17 | 16 |
|
... | ... |
@@ -30,7 +29,7 @@ func getRemoteStore(baseURL, gun string, rt http.RoundTripper) (store.RemoteStor |
30 | 30 |
return s, err |
31 | 31 |
} |
32 | 32 |
|
33 |
-func applyChangelist(repo *tuf.Repo, cl changelist.Changelist) error { |
|
33 |
+func applyChangelist(repo *tuf.Repo, invalid *tuf.Repo, cl changelist.Changelist) error { |
|
34 | 34 |
it, err := cl.NewIterator() |
35 | 35 |
if err != nil { |
36 | 36 |
return err |
... | ... |
@@ -41,30 +40,33 @@ func applyChangelist(repo *tuf.Repo, cl changelist.Changelist) error { |
41 | 41 |
if err != nil { |
42 | 42 |
return err |
43 | 43 |
} |
44 |
- isDel := data.IsDelegation(c.Scope()) |
|
44 |
+ isDel := data.IsDelegation(c.Scope()) || data.IsWildDelegation(c.Scope()) |
|
45 | 45 |
switch { |
46 | 46 |
case c.Scope() == changelist.ScopeTargets || isDel: |
47 |
- err = applyTargetsChange(repo, c) |
|
47 |
+ err = applyTargetsChange(repo, invalid, c) |
|
48 | 48 |
case c.Scope() == changelist.ScopeRoot: |
49 | 49 |
err = applyRootChange(repo, c) |
50 | 50 |
default: |
51 |
- logrus.Debug("scope not supported: ", c.Scope()) |
|
51 |
+ return fmt.Errorf("scope not supported: %s", c.Scope()) |
|
52 | 52 |
} |
53 |
- index++ |
|
54 | 53 |
if err != nil { |
54 |
+ logrus.Debugf("error attempting to apply change #%d: %s, on scope: %s path: %s type: %s", index, c.Action(), c.Scope(), c.Path(), c.Type()) |
|
55 | 55 |
return err |
56 | 56 |
} |
57 |
+ index++ |
|
57 | 58 |
} |
58 | 59 |
logrus.Debugf("applied %d change(s)", index) |
59 | 60 |
return nil |
60 | 61 |
} |
61 | 62 |
|
62 |
-func applyTargetsChange(repo *tuf.Repo, c changelist.Change) error { |
|
63 |
+func applyTargetsChange(repo *tuf.Repo, invalid *tuf.Repo, c changelist.Change) error { |
|
63 | 64 |
switch c.Type() { |
64 | 65 |
case changelist.TypeTargetsTarget: |
65 | 66 |
return changeTargetMeta(repo, c) |
66 | 67 |
case changelist.TypeTargetsDelegation: |
67 | 68 |
return changeTargetsDelegation(repo, c) |
69 |
+ case changelist.TypeWitness: |
|
70 |
+ return witnessTargets(repo, invalid, c.Scope()) |
|
68 | 71 |
default: |
69 | 72 |
return fmt.Errorf("only target meta and delegations changes supported") |
70 | 73 |
} |
... | ... |
@@ -73,7 +75,7 @@ func applyTargetsChange(repo *tuf.Repo, c changelist.Change) error { |
73 | 73 |
func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error { |
74 | 74 |
switch c.Action() { |
75 | 75 |
case changelist.ActionCreate: |
76 |
- td := changelist.TufDelegation{} |
|
76 |
+ td := changelist.TUFDelegation{} |
|
77 | 77 |
err := json.Unmarshal(c.Content(), &td) |
78 | 78 |
if err != nil { |
79 | 79 |
return err |
... | ... |
@@ -87,11 +89,15 @@ func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error { |
87 | 87 |
} |
88 | 88 |
return repo.UpdateDelegationPaths(c.Scope(), td.AddPaths, []string{}, false) |
89 | 89 |
case changelist.ActionUpdate: |
90 |
- td := changelist.TufDelegation{} |
|
90 |
+ td := changelist.TUFDelegation{} |
|
91 | 91 |
err := json.Unmarshal(c.Content(), &td) |
92 | 92 |
if err != nil { |
93 | 93 |
return err |
94 | 94 |
} |
95 |
+ if data.IsWildDelegation(c.Scope()) { |
|
96 |
+ return repo.PurgeDelegationKeys(c.Scope(), td.RemoveKeys) |
|
97 |
+ } |
|
98 |
+ |
|
95 | 99 |
delgRole, err := repo.GetDelegationRole(c.Scope()) |
96 | 100 |
if err != nil { |
97 | 101 |
return err |
... | ... |
@@ -112,10 +118,6 @@ func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error { |
112 | 112 |
removeTUFKeyIDs = append(removeTUFKeyIDs, canonicalToTUFID[canonID]) |
113 | 113 |
} |
114 | 114 |
|
115 |
- // If we specify the only keys left delete the role, else just delete specified keys |
|
116 |
- if strings.Join(delgRole.ListKeyIDs(), ";") == strings.Join(removeTUFKeyIDs, ";") && len(td.AddKeys) == 0 { |
|
117 |
- return repo.DeleteDelegation(c.Scope()) |
|
118 |
- } |
|
119 | 115 |
err = repo.UpdateDelegationKeys(c.Scope(), td.AddKeys, removeTUFKeyIDs, td.NewThreshold) |
120 | 116 |
if err != nil { |
121 | 117 |
return err |
... | ... |
@@ -155,7 +157,7 @@ func changeTargetMeta(repo *tuf.Repo, c changelist.Change) error { |
155 | 155 |
} |
156 | 156 |
|
157 | 157 |
default: |
158 |
- logrus.Debug("action not yet supported: ", c.Action()) |
|
158 |
+ err = fmt.Errorf("action not yet supported: %s", c.Action()) |
|
159 | 159 |
} |
160 | 160 |
return err |
161 | 161 |
} |
... | ... |
@@ -166,7 +168,7 @@ func applyRootChange(repo *tuf.Repo, c changelist.Change) error { |
166 | 166 |
case changelist.TypeRootRole: |
167 | 167 |
err = applyRootRoleChange(repo, c) |
168 | 168 |
default: |
169 |
- logrus.Debug("type of root change not yet supported: ", c.Type()) |
|
169 |
+ err = fmt.Errorf("type of root change not yet supported: %s", c.Type()) |
|
170 | 170 |
} |
171 | 171 |
return err // might be nil |
172 | 172 |
} |
... | ... |
@@ -175,7 +177,7 @@ func applyRootRoleChange(repo *tuf.Repo, c changelist.Change) error { |
175 | 175 |
switch c.Action() { |
176 | 176 |
case changelist.ActionCreate: |
177 | 177 |
// replaces all keys for a role |
178 |
- d := &changelist.TufRootData{} |
|
178 |
+ d := &changelist.TUFRootData{} |
|
179 | 179 |
err := json.Unmarshal(c.Content(), d) |
180 | 180 |
if err != nil { |
181 | 181 |
return err |
... | ... |
@@ -185,14 +187,34 @@ func applyRootRoleChange(repo *tuf.Repo, c changelist.Change) error { |
185 | 185 |
return err |
186 | 186 |
} |
187 | 187 |
default: |
188 |
- logrus.Debug("action not yet supported for root: ", c.Action()) |
|
188 |
+ return fmt.Errorf("action not yet supported for root: %s", c.Action()) |
|
189 | 189 |
} |
190 | 190 |
return nil |
191 | 191 |
} |
192 | 192 |
|
193 |
-func nearExpiry(r *data.SignedRoot) bool { |
|
193 |
+func nearExpiry(r data.SignedCommon) bool { |
|
194 | 194 |
plus6mo := time.Now().AddDate(0, 6, 0) |
195 |
- return r.Signed.Expires.Before(plus6mo) |
|
195 |
+ return r.Expires.Before(plus6mo) |
|
196 |
+} |
|
197 |
+ |
|
198 |
+func warnRolesNearExpiry(r *tuf.Repo) { |
|
199 |
+ //get every role and its respective signed common and call nearExpiry on it |
|
200 |
+ //Root check |
|
201 |
+ if nearExpiry(r.Root.Signed.SignedCommon) { |
|
202 |
+ logrus.Warn("root is nearing expiry, you should re-sign the role metadata") |
|
203 |
+ } |
|
204 |
+ //Targets and delegations check |
|
205 |
+ for role, signedTOrD := range r.Targets { |
|
206 |
+ //signedTOrD is of type *data.SignedTargets |
|
207 |
+ if nearExpiry(signedTOrD.Signed.SignedCommon) { |
|
208 |
+ logrus.Warn(role, " metadata is nearing expiry, you should re-sign the role metadata") |
|
209 |
+ } |
|
210 |
+ } |
|
211 |
+ //Snapshot check |
|
212 |
+ if nearExpiry(r.Snapshot.Signed.SignedCommon) { |
|
213 |
+ logrus.Warn("snapshot is nearing expiry, you should re-sign the role metadata") |
|
214 |
+ } |
|
215 |
+ //do not need to worry about Timestamp, notary signer will re-sign with the timestamp key |
|
196 | 216 |
} |
197 | 217 |
|
198 | 218 |
// Fetches a public key from a remote store, given a gun and role |
... | ... |
@@ -214,7 +236,26 @@ func getRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKey, |
214 | 214 |
return pubKey, nil |
215 | 215 |
} |
216 | 216 |
|
217 |
-// signs and serializes the metadata for a canonical role in a tuf repo to JSON |
|
217 |
+// Rotates a private key in a remote store and returns the public key component |
|
218 |
+func rotateRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKey, error) { |
|
219 |
+ remote, err := getRemoteStore(url, gun, rt) |
|
220 |
+ if err != nil { |
|
221 |
+ return nil, err |
|
222 |
+ } |
|
223 |
+ rawPubKey, err := remote.RotateKey(role) |
|
224 |
+ if err != nil { |
|
225 |
+ return nil, err |
|
226 |
+ } |
|
227 |
+ |
|
228 |
+ pubKey, err := data.UnmarshalPublicKey(rawPubKey) |
|
229 |
+ if err != nil { |
|
230 |
+ return nil, err |
|
231 |
+ } |
|
232 |
+ |
|
233 |
+ return pubKey, nil |
|
234 |
+} |
|
235 |
+ |
|
236 |
+// signs and serializes the metadata for a canonical role in a TUF repo to JSON |
|
218 | 237 |
func serializeCanonicalRole(tufRepo *tuf.Repo, role string) (out []byte, err error) { |
219 | 238 |
var s *data.Signed |
220 | 239 |
switch { |
... | ... |
@@ -6,7 +6,7 @@ import ( |
6 | 6 |
"fmt" |
7 | 7 |
"net/http" |
8 | 8 |
|
9 |
- "github.com/docker/notary/passphrase" |
|
9 |
+ "github.com/docker/notary" |
|
10 | 10 |
"github.com/docker/notary/trustmanager" |
11 | 11 |
"github.com/docker/notary/trustpinning" |
12 | 12 |
) |
... | ... |
@@ -16,7 +16,7 @@ import ( |
16 | 16 |
// (This is normally defaults to "~/.notary" or "~/.docker/trust" when enabling |
17 | 17 |
// docker content trust). |
18 | 18 |
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, |
19 |
- retriever passphrase.Retriever, trustPinning trustpinning.TrustPinConfig) ( |
|
19 |
+ retriever notary.PassRetriever, trustPinning trustpinning.TrustPinConfig) ( |
|
20 | 20 |
*NotaryRepository, error) { |
21 | 21 |
|
22 | 22 |
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever) |
... | ... |
@@ -6,7 +6,7 @@ import ( |
6 | 6 |
"fmt" |
7 | 7 |
"net/http" |
8 | 8 |
|
9 |
- "github.com/docker/notary/passphrase" |
|
9 |
+ "github.com/docker/notary" |
|
10 | 10 |
"github.com/docker/notary/trustmanager" |
11 | 11 |
"github.com/docker/notary/trustmanager/yubikey" |
12 | 12 |
"github.com/docker/notary/trustpinning" |
... | ... |
@@ -16,7 +16,7 @@ import ( |
16 | 16 |
// It takes the base directory under where all the trust files will be stored |
17 | 17 |
// (usually ~/.docker/trust/). |
18 | 18 |
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, |
19 |
- retriever passphrase.Retriever, trustPinning trustpinning.TrustPinConfig) ( |
|
19 |
+ retriever notary.PassRetriever, trustPinning trustpinning.TrustPinConfig) ( |
|
20 | 20 |
*NotaryRepository, error) { |
21 | 21 |
|
22 | 22 |
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever) |
23 | 23 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,239 @@ |
0 |
+package client |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "encoding/json" |
|
4 |
+ |
|
5 |
+ "github.com/Sirupsen/logrus" |
|
6 |
+ "github.com/docker/notary" |
|
7 |
+ store "github.com/docker/notary/storage" |
|
8 |
+ "github.com/docker/notary/tuf" |
|
9 |
+ "github.com/docker/notary/tuf/data" |
|
10 |
+ "github.com/docker/notary/tuf/signed" |
|
11 |
+) |
|
12 |
+ |
|
13 |
+// TUFClient is a usability wrapper around a raw TUF repo |
|
14 |
+type TUFClient struct { |
|
15 |
+ remote store.RemoteStore |
|
16 |
+ cache store.MetadataStore |
|
17 |
+ oldBuilder tuf.RepoBuilder |
|
18 |
+ newBuilder tuf.RepoBuilder |
|
19 |
+} |
|
20 |
+ |
|
21 |
+// NewTUFClient initialized a TUFClient with the given repo, remote source of content, and cache |
|
22 |
+func NewTUFClient(oldBuilder, newBuilder tuf.RepoBuilder, remote store.RemoteStore, cache store.MetadataStore) *TUFClient { |
|
23 |
+ return &TUFClient{ |
|
24 |
+ oldBuilder: oldBuilder, |
|
25 |
+ newBuilder: newBuilder, |
|
26 |
+ remote: remote, |
|
27 |
+ cache: cache, |
|
28 |
+ } |
|
29 |
+} |
|
30 |
+ |
|
31 |
+// Update performs an update to the TUF repo as defined by the TUF spec |
|
32 |
+func (c *TUFClient) Update() (*tuf.Repo, *tuf.Repo, error) { |
|
33 |
+ // 1. Get timestamp |
|
34 |
+ // a. If timestamp error (verification, expired, etc...) download new root and return to 1. |
|
35 |
+ // 2. Check if local snapshot is up to date |
|
36 |
+ // a. If out of date, get updated snapshot |
|
37 |
+ // i. If snapshot error, download new root and return to 1. |
|
38 |
+ // 3. Check if root correct against snapshot |
|
39 |
+ // a. If incorrect, download new root and return to 1. |
|
40 |
+ // 4. Iteratively download and search targets and delegations to find target meta |
|
41 |
+ logrus.Debug("updating TUF client") |
|
42 |
+ err := c.update() |
|
43 |
+ if err != nil { |
|
44 |
+ logrus.Debug("Error occurred. Root will be downloaded and another update attempted") |
|
45 |
+ logrus.Debug("Resetting the TUF builder...") |
|
46 |
+ |
|
47 |
+ c.newBuilder = c.newBuilder.BootstrapNewBuilder() |
|
48 |
+ |
|
49 |
+ if err := c.downloadRoot(); err != nil { |
|
50 |
+ logrus.Debug("Client Update (Root):", err) |
|
51 |
+ return nil, nil, err |
|
52 |
+ } |
|
53 |
+ // If we error again, we now have the latest root and just want to fail |
|
54 |
+ // out as there's no expectation the problem can be resolved automatically |
|
55 |
+ logrus.Debug("retrying TUF client update") |
|
56 |
+ if err := c.update(); err != nil { |
|
57 |
+ return nil, nil, err |
|
58 |
+ } |
|
59 |
+ } |
|
60 |
+ return c.newBuilder.Finish() |
|
61 |
+} |
|
62 |
+ |
|
63 |
+func (c *TUFClient) update() error { |
|
64 |
+ if err := c.downloadTimestamp(); err != nil { |
|
65 |
+ logrus.Debugf("Client Update (Timestamp): %s", err.Error()) |
|
66 |
+ return err |
|
67 |
+ } |
|
68 |
+ if err := c.downloadSnapshot(); err != nil { |
|
69 |
+ logrus.Debugf("Client Update (Snapshot): %s", err.Error()) |
|
70 |
+ return err |
|
71 |
+ } |
|
72 |
+ // will always need top level targets at a minimum |
|
73 |
+ if err := c.downloadTargets(); err != nil { |
|
74 |
+ logrus.Debugf("Client Update (Targets): %s", err.Error()) |
|
75 |
+ return err |
|
76 |
+ } |
|
77 |
+ return nil |
|
78 |
+} |
|
79 |
+ |
|
80 |
+// downloadRoot is responsible for downloading the root.json |
|
81 |
+func (c *TUFClient) downloadRoot() error { |
|
82 |
+ role := data.CanonicalRootRole |
|
83 |
+ consistentInfo := c.newBuilder.GetConsistentInfo(role) |
|
84 |
+ |
|
85 |
+ // We can't read an exact size for the root metadata without risking getting stuck in the TUF update cycle |
|
86 |
+ // since it's possible that downloading timestamp/snapshot metadata may fail due to a signature mismatch |
|
87 |
+ if !consistentInfo.ChecksumKnown() { |
|
88 |
+ logrus.Debugf("Loading root with no expected checksum") |
|
89 |
+ |
|
90 |
+ // get the cached root, if it exists, just for version checking |
|
91 |
+ cachedRoot, _ := c.cache.GetSized(role, -1) |
|
92 |
+ // prefer to download a new root |
|
93 |
+ _, remoteErr := c.tryLoadRemote(consistentInfo, cachedRoot) |
|
94 |
+ return remoteErr |
|
95 |
+ } |
|
96 |
+ |
|
97 |
+ _, err := c.tryLoadCacheThenRemote(consistentInfo) |
|
98 |
+ return err |
|
99 |
+} |
|
100 |
+ |
|
101 |
+// downloadTimestamp is responsible for downloading the timestamp.json |
|
102 |
+// Timestamps are special in that we ALWAYS attempt to download and only |
|
103 |
+// use cache if the download fails (and the cache is still valid). |
|
104 |
+func (c *TUFClient) downloadTimestamp() error { |
|
105 |
+ logrus.Debug("Loading timestamp...") |
|
106 |
+ role := data.CanonicalTimestampRole |
|
107 |
+ consistentInfo := c.newBuilder.GetConsistentInfo(role) |
|
108 |
+ |
|
109 |
+ // always get the remote timestamp, since it supersedes the local one |
|
110 |
+ cachedTS, cachedErr := c.cache.GetSized(role, notary.MaxTimestampSize) |
|
111 |
+ _, remoteErr := c.tryLoadRemote(consistentInfo, cachedTS) |
|
112 |
+ |
|
113 |
+ // check that there was no remote error, or if there was a network problem |
|
114 |
+ // If there was a validation error, we should error out so we can download a new root or fail the update |
|
115 |
+ switch remoteErr.(type) { |
|
116 |
+ case nil: |
|
117 |
+ return nil |
|
118 |
+ case store.ErrMetaNotFound, store.ErrServerUnavailable, store.ErrOffline, store.NetworkError: |
|
119 |
+ break |
|
120 |
+ default: |
|
121 |
+ return remoteErr |
|
122 |
+ } |
|
123 |
+ |
|
124 |
+ // since it was a network error: get the cached timestamp, if it exists |
|
125 |
+ if cachedErr != nil { |
|
126 |
+ logrus.Debug("no cached or remote timestamp available") |
|
127 |
+ return remoteErr |
|
128 |
+ } |
|
129 |
+ |
|
130 |
+ logrus.Warn("Error while downloading remote metadata, using cached timestamp - this might not be the latest version available remotely") |
|
131 |
+ err := c.newBuilder.Load(role, cachedTS, 1, false) |
|
132 |
+ if err == nil { |
|
133 |
+ logrus.Debug("successfully verified cached timestamp") |
|
134 |
+ } |
|
135 |
+ return err |
|
136 |
+ |
|
137 |
+} |
|
138 |
+ |
|
139 |
+// downloadSnapshot is responsible for downloading the snapshot.json |
|
140 |
+func (c *TUFClient) downloadSnapshot() error { |
|
141 |
+ logrus.Debug("Loading snapshot...") |
|
142 |
+ role := data.CanonicalSnapshotRole |
|
143 |
+ consistentInfo := c.newBuilder.GetConsistentInfo(role) |
|
144 |
+ |
|
145 |
+ _, err := c.tryLoadCacheThenRemote(consistentInfo) |
|
146 |
+ return err |
|
147 |
+} |
|
148 |
+ |
|
149 |
+// downloadTargets downloads all targets and delegated targets for the repository. |
|
150 |
+// It uses a pre-order tree traversal as it's necessary to download parents first |
|
151 |
+// to obtain the keys to validate children. |
|
152 |
+func (c *TUFClient) downloadTargets() error { |
|
153 |
+ toDownload := []data.DelegationRole{{ |
|
154 |
+ BaseRole: data.BaseRole{Name: data.CanonicalTargetsRole}, |
|
155 |
+ Paths: []string{""}, |
|
156 |
+ }} |
|
157 |
+ |
|
158 |
+ for len(toDownload) > 0 { |
|
159 |
+ role := toDownload[0] |
|
160 |
+ toDownload = toDownload[1:] |
|
161 |
+ |
|
162 |
+ consistentInfo := c.newBuilder.GetConsistentInfo(role.Name) |
|
163 |
+ if !consistentInfo.ChecksumKnown() { |
|
164 |
+ logrus.Debugf("skipping %s because there is no checksum for it", role.Name) |
|
165 |
+ continue |
|
166 |
+ } |
|
167 |
+ |
|
168 |
+ children, err := c.getTargetsFile(role, consistentInfo) |
|
169 |
+ switch err.(type) { |
|
170 |
+ case signed.ErrExpired, signed.ErrRoleThreshold: |
|
171 |
+ if role.Name == data.CanonicalTargetsRole { |
|
172 |
+ return err |
|
173 |
+ } |
|
174 |
+ logrus.Warnf("Error getting %s: %s", role.Name, err) |
|
175 |
+ break |
|
176 |
+ case nil: |
|
177 |
+ toDownload = append(children, toDownload...) |
|
178 |
+ default: |
|
179 |
+ return err |
|
180 |
+ } |
|
181 |
+ } |
|
182 |
+ return nil |
|
183 |
+} |
|
184 |
+ |
|
185 |
+func (c TUFClient) getTargetsFile(role data.DelegationRole, ci tuf.ConsistentInfo) ([]data.DelegationRole, error) { |
|
186 |
+ logrus.Debugf("Loading %s...", role.Name) |
|
187 |
+ tgs := &data.SignedTargets{} |
|
188 |
+ |
|
189 |
+ raw, err := c.tryLoadCacheThenRemote(ci) |
|
190 |
+ if err != nil { |
|
191 |
+ return nil, err |
|
192 |
+ } |
|
193 |
+ |
|
194 |
+ // we know it unmarshals because if `tryLoadCacheThenRemote` didn't fail, then |
|
195 |
+ // the raw has already been loaded into the builder |
|
196 |
+ json.Unmarshal(raw, tgs) |
|
197 |
+ return tgs.GetValidDelegations(role), nil |
|
198 |
+} |
|
199 |
+ |
|
200 |
+func (c *TUFClient) tryLoadCacheThenRemote(consistentInfo tuf.ConsistentInfo) ([]byte, error) { |
|
201 |
+ cachedTS, err := c.cache.GetSized(consistentInfo.RoleName, consistentInfo.Length()) |
|
202 |
+ if err != nil { |
|
203 |
+ logrus.Debugf("no %s in cache, must download", consistentInfo.RoleName) |
|
204 |
+ return c.tryLoadRemote(consistentInfo, nil) |
|
205 |
+ } |
|
206 |
+ |
|
207 |
+ if err = c.newBuilder.Load(consistentInfo.RoleName, cachedTS, 1, false); err == nil { |
|
208 |
+ logrus.Debugf("successfully verified cached %s", consistentInfo.RoleName) |
|
209 |
+ return cachedTS, nil |
|
210 |
+ } |
|
211 |
+ |
|
212 |
+ logrus.Debugf("cached %s is invalid (must download): %s", consistentInfo.RoleName, err) |
|
213 |
+ return c.tryLoadRemote(consistentInfo, cachedTS) |
|
214 |
+} |
|
215 |
+ |
|
216 |
+func (c *TUFClient) tryLoadRemote(consistentInfo tuf.ConsistentInfo, old []byte) ([]byte, error) { |
|
217 |
+ consistentName := consistentInfo.ConsistentName() |
|
218 |
+ raw, err := c.remote.GetSized(consistentName, consistentInfo.Length()) |
|
219 |
+ if err != nil { |
|
220 |
+ logrus.Debugf("error downloading %s: %s", consistentName, err) |
|
221 |
+ return old, err |
|
222 |
+ } |
|
223 |
+ |
|
224 |
+ // try to load the old data into the old builder - only use it to validate |
|
225 |
+ // versions if it loads successfully. If it errors, then the loaded version |
|
226 |
+ // will be 1 |
|
227 |
+ c.oldBuilder.Load(consistentInfo.RoleName, old, 1, true) |
|
228 |
+ minVersion := c.oldBuilder.GetLoadedVersion(consistentInfo.RoleName) |
|
229 |
+ if err := c.newBuilder.Load(consistentInfo.RoleName, raw, minVersion, false); err != nil { |
|
230 |
+ logrus.Debugf("downloaded %s is invalid: %s", consistentName, err) |
|
231 |
+ return raw, err |
|
232 |
+ } |
|
233 |
+ logrus.Debugf("successfully verified downloaded %s", consistentName) |
|
234 |
+ if err := c.cache.Set(consistentInfo.RoleName, raw); err != nil { |
|
235 |
+ logrus.Debugf("Unable to write %s to cache: %s", consistentInfo.RoleName, err) |
|
236 |
+ } |
|
237 |
+ return raw, nil |
|
238 |
+} |
0 | 239 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,69 @@ |
0 |
+package client |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "path/filepath" |
|
4 |
+ |
|
5 |
+ "github.com/docker/notary/client/changelist" |
|
6 |
+ "github.com/docker/notary/tuf" |
|
7 |
+ "github.com/docker/notary/tuf/data" |
|
8 |
+) |
|
9 |
+ |
|
10 |
+// Witness creates change objects to witness (i.e. re-sign) the given |
|
11 |
+// roles on the next publish. One change is created per role |
|
12 |
+func (r *NotaryRepository) Witness(roles ...string) ([]string, error) { |
|
13 |
+ cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) |
|
14 |
+ if err != nil { |
|
15 |
+ return nil, err |
|
16 |
+ } |
|
17 |
+ defer cl.Close() |
|
18 |
+ |
|
19 |
+ successful := make([]string, 0, len(roles)) |
|
20 |
+ for _, role := range roles { |
|
21 |
+ // scope is role |
|
22 |
+ c := changelist.NewTUFChange( |
|
23 |
+ changelist.ActionUpdate, |
|
24 |
+ role, |
|
25 |
+ changelist.TypeWitness, |
|
26 |
+ "", |
|
27 |
+ nil, |
|
28 |
+ ) |
|
29 |
+ err = cl.Add(c) |
|
30 |
+ if err != nil { |
|
31 |
+ break |
|
32 |
+ } |
|
33 |
+ successful = append(successful, role) |
|
34 |
+ } |
|
35 |
+ return successful, err |
|
36 |
+} |
|
37 |
+ |
|
38 |
+func witnessTargets(repo *tuf.Repo, invalid *tuf.Repo, role string) error { |
|
39 |
+ if r, ok := repo.Targets[role]; ok { |
|
40 |
+ // role is already valid, mark for re-signing/updating |
|
41 |
+ r.Dirty = true |
|
42 |
+ return nil |
|
43 |
+ } |
|
44 |
+ |
|
45 |
+ if roleObj, err := repo.GetDelegationRole(role); err == nil && invalid != nil { |
|
46 |
+ // A role with a threshold > len(keys) is technically invalid, but we let it build in the builder because |
|
47 |
+ // we want to be able to download the role (which may still have targets on it), add more keys, and then |
|
48 |
+ // witness the role, thus bringing it back to valid. However, if no keys have been added before witnessing, |
|
49 |
+ // then it is still an invalid role, and can't be witnessed because nothing can bring it back to valid. |
|
50 |
+ if roleObj.Threshold > len(roleObj.Keys) { |
|
51 |
+ return data.ErrInvalidRole{ |
|
52 |
+ Role: role, |
|
53 |
+ Reason: "role does not specify enough valid signing keys to meet its required threshold", |
|
54 |
+ } |
|
55 |
+ } |
|
56 |
+ if r, ok := invalid.Targets[role]; ok { |
|
57 |
+ // role is recognized but invalid, move to valid data and mark for re-signing |
|
58 |
+ repo.Targets[role] = r |
|
59 |
+ r.Dirty = true |
|
60 |
+ return nil |
|
61 |
+ } |
|
62 |
+ } |
|
63 |
+ // role isn't recognized, even as invalid |
|
64 |
+ return data.ErrInvalidRole{ |
|
65 |
+ Role: role, |
|
66 |
+ Reason: "this role is not known", |
|
67 |
+ } |
|
68 |
+} |
... | ... |
@@ -3,16 +3,20 @@ codecov: |
3 | 3 |
# 2 builds on circleci, 1 jenkins build |
4 | 4 |
after_n_builds: 3 |
5 | 5 |
coverage: |
6 |
+ range: "50...100" |
|
6 | 7 |
status: |
7 | 8 |
# project will give us the diff in the total code coverage between a commit |
8 | 9 |
# and its parent |
9 | 10 |
project: |
10 | 11 |
default: |
11 | 12 |
target: auto |
13 |
+ threshold: "0.05%" |
|
12 | 14 |
# patch would give us the code coverage of the diff only |
13 | 15 |
patch: false |
14 | 16 |
# changes tells us if there are unexpected code coverage changes in other files |
15 | 17 |
# which were not changed by the diff |
16 | 18 |
changes: false |
19 |
+ ignore: # ignore testutils for coverage |
|
20 |
+ - "tuf/testutils/*" |
|
17 | 21 |
comment: off |
18 | 22 |
|
... | ... |
@@ -1,8 +1,6 @@ |
1 | 1 |
package notary |
2 | 2 |
|
3 |
-import ( |
|
4 |
- "time" |
|
5 |
-) |
|
3 |
+import "time" |
|
6 | 4 |
|
7 | 5 |
// application wide constants |
8 | 6 |
const ( |
... | ... |
@@ -34,6 +32,8 @@ const ( |
34 | 34 |
RootKeysSubdir = "root_keys" |
35 | 35 |
// NonRootKeysSubdir is the subdirectory under PrivDir where non-root private keys are stored |
36 | 36 |
NonRootKeysSubdir = "tuf_keys" |
37 |
+ // KeyExtension is the file extension to use for private key files |
|
38 |
+ KeyExtension = "key" |
|
37 | 39 |
|
38 | 40 |
// Day is a duration of one day |
39 | 41 |
Day = 24 * time.Hour |
... | ... |
@@ -56,6 +56,8 @@ const ( |
56 | 56 |
MemoryBackend = "memory" |
57 | 57 |
SQLiteBackend = "sqlite3" |
58 | 58 |
RethinkDBBackend = "rethinkdb" |
59 |
+ |
|
60 |
+ DefaultImportRole = "delegation" |
|
59 | 61 |
) |
60 | 62 |
|
61 | 63 |
// NotaryDefaultExpiries is the construct used to configure the default expiry times of |
62 | 64 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,16 @@ |
0 |
+// +build !windows |
|
1 |
+ |
|
2 |
+package notary |
|
3 |
+ |
|
4 |
+import ( |
|
5 |
+ "os" |
|
6 |
+ "syscall" |
|
7 |
+) |
|
8 |
+ |
|
9 |
+// NotarySupportedSignals contains the signals we would like to capture: |
|
10 |
+// - SIGUSR1, indicates a increment of the log level. |
|
11 |
+// - SIGUSR2, indicates a decrement of the log level. |
|
12 |
+var NotarySupportedSignals = []os.Signal{ |
|
13 |
+ syscall.SIGUSR1, |
|
14 |
+ syscall.SIGUSR2, |
|
15 |
+} |
0 | 8 |
deleted file mode 100755 |
... | ... |
@@ -1,10 +0,0 @@ |
1 |
-#!/usr/bin/env bash |
|
2 |
- |
|
3 |
-# Given a subpackage and the containing package, figures out which packages |
|
4 |
-# need to be passed to `go test -coverpkg`: this includes all of the |
|
5 |
-# subpackage's dependencies within the containing package, as well as the |
|
6 |
-# subpackage itself. |
|
7 |
- |
|
8 |
-DEPENDENCIES="$(go list -f $'{{range $f := .Deps}}{{$f}}\n{{end}}' ${1} | grep ${2} | grep -v ${2}/vendor)" |
|
9 |
- |
|
10 |
-echo "${1} ${DEPENDENCIES}" | xargs echo -n | tr ' ' ',' |
... | ... |
@@ -7,8 +7,8 @@ import ( |
7 | 7 |
"fmt" |
8 | 8 |
"time" |
9 | 9 |
|
10 |
- "github.com/docker/notary/trustmanager" |
|
11 | 10 |
"github.com/docker/notary/tuf/data" |
11 |
+ "github.com/docker/notary/tuf/utils" |
|
12 | 12 |
) |
13 | 13 |
|
14 | 14 |
// GenerateCertificate generates an X509 Certificate from a template, given a GUN and validity interval |
... | ... |
@@ -22,7 +22,7 @@ func GenerateCertificate(rootKey data.PrivateKey, gun string, startTime, endTime |
22 | 22 |
} |
23 | 23 |
|
24 | 24 |
func generateCertificate(signer crypto.Signer, gun string, startTime, endTime time.Time) (*x509.Certificate, error) { |
25 |
- template, err := trustmanager.NewCertificate(gun, startTime, endTime) |
|
25 |
+ template, err := utils.NewCertificate(gun, startTime, endTime) |
|
26 | 26 |
if err != nil { |
27 | 27 |
return nil, fmt.Errorf("failed to create the certificate template for: %s (%v)", gun, err) |
28 | 28 |
} |
... | ... |
@@ -4,13 +4,24 @@ import ( |
4 | 4 |
"crypto/rand" |
5 | 5 |
"fmt" |
6 | 6 |
|
7 |
+ "crypto/x509" |
|
8 |
+ "encoding/pem" |
|
9 |
+ "errors" |
|
7 | 10 |
"github.com/Sirupsen/logrus" |
11 |
+ "github.com/docker/notary" |
|
8 | 12 |
"github.com/docker/notary/trustmanager" |
9 | 13 |
"github.com/docker/notary/tuf/data" |
14 |
+ "github.com/docker/notary/tuf/utils" |
|
10 | 15 |
) |
11 | 16 |
|
12 |
-const ( |
|
13 |
- rsaKeySize = 2048 // Used for snapshots and targets keys |
|
17 |
+var ( |
|
18 |
+ // ErrNoValidPrivateKey is returned if a key being imported doesn't |
|
19 |
+ // look like a private key |
|
20 |
+ ErrNoValidPrivateKey = errors.New("no valid private key found") |
|
21 |
+ |
|
22 |
+ // ErrRootKeyNotEncrypted is returned if a root key being imported is |
|
23 |
+ // unencrypted |
|
24 |
+ ErrRootKeyNotEncrypted = errors.New("only encrypted root keys may be imported") |
|
14 | 25 |
) |
15 | 26 |
|
16 | 27 |
// CryptoService implements Sign and Create, holding a specific GUN and keystore to |
... | ... |
@@ -31,17 +42,17 @@ func (cs *CryptoService) Create(role, gun, algorithm string) (data.PublicKey, er |
31 | 31 |
|
32 | 32 |
switch algorithm { |
33 | 33 |
case data.RSAKey: |
34 |
- privKey, err = trustmanager.GenerateRSAKey(rand.Reader, rsaKeySize) |
|
34 |
+ privKey, err = utils.GenerateRSAKey(rand.Reader, notary.MinRSABitSize) |
|
35 | 35 |
if err != nil { |
36 | 36 |
return nil, fmt.Errorf("failed to generate RSA key: %v", err) |
37 | 37 |
} |
38 | 38 |
case data.ECDSAKey: |
39 |
- privKey, err = trustmanager.GenerateECDSAKey(rand.Reader) |
|
39 |
+ privKey, err = utils.GenerateECDSAKey(rand.Reader) |
|
40 | 40 |
if err != nil { |
41 | 41 |
return nil, fmt.Errorf("failed to generate EC key: %v", err) |
42 | 42 |
} |
43 | 43 |
case data.ED25519Key: |
44 |
- privKey, err = trustmanager.GenerateED25519Key(rand.Reader) |
|
44 |
+ privKey, err = utils.GenerateED25519Key(rand.Reader) |
|
45 | 45 |
if err != nil { |
46 | 46 |
return nil, fmt.Errorf("failed to generate ED25519 key: %v", err) |
47 | 47 |
} |
... | ... |
@@ -153,3 +164,18 @@ func (cs *CryptoService) ListAllKeys() map[string]string { |
153 | 153 |
} |
154 | 154 |
return res |
155 | 155 |
} |
156 |
+ |
|
157 |
+// CheckRootKeyIsEncrypted makes sure the root key is encrypted. We have |
|
158 |
+// internal assumptions that depend on this. |
|
159 |
+func CheckRootKeyIsEncrypted(pemBytes []byte) error { |
|
160 |
+ block, _ := pem.Decode(pemBytes) |
|
161 |
+ if block == nil { |
|
162 |
+ return ErrNoValidPrivateKey |
|
163 |
+ } |
|
164 |
+ |
|
165 |
+ if !x509.IsEncryptedPEMBlock(block) { |
|
166 |
+ return ErrRootKeyNotEncrypted |
|
167 |
+ } |
|
168 |
+ |
|
169 |
+ return nil |
|
170 |
+} |
156 | 171 |
deleted file mode 100644 |
... | ... |
@@ -1,313 +0,0 @@ |
1 |
-package cryptoservice |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "archive/zip" |
|
5 |
- "crypto/x509" |
|
6 |
- "encoding/pem" |
|
7 |
- "errors" |
|
8 |
- "io" |
|
9 |
- "io/ioutil" |
|
10 |
- "os" |
|
11 |
- "path/filepath" |
|
12 |
- "strings" |
|
13 |
- |
|
14 |
- "github.com/docker/notary/passphrase" |
|
15 |
- "github.com/docker/notary/trustmanager" |
|
16 |
-) |
|
17 |
- |
|
18 |
-const zipMadeByUNIX = 3 << 8 |
|
19 |
- |
|
20 |
-var ( |
|
21 |
- // ErrNoValidPrivateKey is returned if a key being imported doesn't |
|
22 |
- // look like a private key |
|
23 |
- ErrNoValidPrivateKey = errors.New("no valid private key found") |
|
24 |
- |
|
25 |
- // ErrRootKeyNotEncrypted is returned if a root key being imported is |
|
26 |
- // unencrypted |
|
27 |
- ErrRootKeyNotEncrypted = errors.New("only encrypted root keys may be imported") |
|
28 |
- |
|
29 |
- // ErrNoKeysFoundForGUN is returned if no keys are found for the |
|
30 |
- // specified GUN during export |
|
31 |
- ErrNoKeysFoundForGUN = errors.New("no keys found for specified GUN") |
|
32 |
-) |
|
33 |
- |
|
34 |
-// ExportKey exports the specified private key to an io.Writer in PEM format. |
|
35 |
-// The key's existing encryption is preserved. |
|
36 |
-func (cs *CryptoService) ExportKey(dest io.Writer, keyID, role string) error { |
|
37 |
- var ( |
|
38 |
- pemBytes []byte |
|
39 |
- err error |
|
40 |
- ) |
|
41 |
- |
|
42 |
- for _, ks := range cs.keyStores { |
|
43 |
- pemBytes, err = ks.ExportKey(keyID) |
|
44 |
- if err != nil { |
|
45 |
- continue |
|
46 |
- } |
|
47 |
- } |
|
48 |
- if err != nil { |
|
49 |
- return err |
|
50 |
- } |
|
51 |
- |
|
52 |
- nBytes, err := dest.Write(pemBytes) |
|
53 |
- if err != nil { |
|
54 |
- return err |
|
55 |
- } |
|
56 |
- if nBytes != len(pemBytes) { |
|
57 |
- return errors.New("Unable to finish writing exported key.") |
|
58 |
- } |
|
59 |
- return nil |
|
60 |
-} |
|
61 |
- |
|
62 |
-// ExportKeyReencrypt exports the specified private key to an io.Writer in |
|
63 |
-// PEM format. The key is reencrypted with a new passphrase. |
|
64 |
-func (cs *CryptoService) ExportKeyReencrypt(dest io.Writer, keyID string, newPassphraseRetriever passphrase.Retriever) error { |
|
65 |
- privateKey, _, err := cs.GetPrivateKey(keyID) |
|
66 |
- if err != nil { |
|
67 |
- return err |
|
68 |
- } |
|
69 |
- |
|
70 |
- keyInfo, err := cs.GetKeyInfo(keyID) |
|
71 |
- if err != nil { |
|
72 |
- return err |
|
73 |
- } |
|
74 |
- |
|
75 |
- // Create temporary keystore to use as a staging area |
|
76 |
- tempBaseDir, err := ioutil.TempDir("", "notary-key-export-") |
|
77 |
- defer os.RemoveAll(tempBaseDir) |
|
78 |
- |
|
79 |
- tempKeyStore, err := trustmanager.NewKeyFileStore(tempBaseDir, newPassphraseRetriever) |
|
80 |
- if err != nil { |
|
81 |
- return err |
|
82 |
- } |
|
83 |
- |
|
84 |
- err = tempKeyStore.AddKey(keyInfo, privateKey) |
|
85 |
- if err != nil { |
|
86 |
- return err |
|
87 |
- } |
|
88 |
- |
|
89 |
- pemBytes, err := tempKeyStore.ExportKey(keyID) |
|
90 |
- if err != nil { |
|
91 |
- return err |
|
92 |
- } |
|
93 |
- nBytes, err := dest.Write(pemBytes) |
|
94 |
- if err != nil { |
|
95 |
- return err |
|
96 |
- } |
|
97 |
- if nBytes != len(pemBytes) { |
|
98 |
- return errors.New("Unable to finish writing exported key.") |
|
99 |
- } |
|
100 |
- return nil |
|
101 |
-} |
|
102 |
- |
|
103 |
-// ExportAllKeys exports all keys to an io.Writer in zip format. |
|
104 |
-// newPassphraseRetriever will be used to obtain passphrases to use to encrypt the existing keys. |
|
105 |
-func (cs *CryptoService) ExportAllKeys(dest io.Writer, newPassphraseRetriever passphrase.Retriever) error { |
|
106 |
- tempBaseDir, err := ioutil.TempDir("", "notary-key-export-") |
|
107 |
- defer os.RemoveAll(tempBaseDir) |
|
108 |
- |
|
109 |
- // Create temporary keystore to use as a staging area |
|
110 |
- tempKeyStore, err := trustmanager.NewKeyFileStore(tempBaseDir, newPassphraseRetriever) |
|
111 |
- if err != nil { |
|
112 |
- return err |
|
113 |
- } |
|
114 |
- |
|
115 |
- for _, ks := range cs.keyStores { |
|
116 |
- if err := moveKeys(ks, tempKeyStore); err != nil { |
|
117 |
- return err |
|
118 |
- } |
|
119 |
- } |
|
120 |
- |
|
121 |
- zipWriter := zip.NewWriter(dest) |
|
122 |
- |
|
123 |
- if err := addKeysToArchive(zipWriter, tempKeyStore); err != nil { |
|
124 |
- return err |
|
125 |
- } |
|
126 |
- |
|
127 |
- zipWriter.Close() |
|
128 |
- |
|
129 |
- return nil |
|
130 |
-} |
|
131 |
- |
|
132 |
-// ImportKeysZip imports keys from a zip file provided as an zip.Reader. The |
|
133 |
-// keys in the root_keys directory are left encrypted, but the other keys are |
|
134 |
-// decrypted with the specified passphrase. |
|
135 |
-func (cs *CryptoService) ImportKeysZip(zipReader zip.Reader, retriever passphrase.Retriever) error { |
|
136 |
- // Temporarily store the keys in maps, so we can bail early if there's |
|
137 |
- // an error (for example, wrong passphrase), without leaving the key |
|
138 |
- // store in an inconsistent state |
|
139 |
- newKeys := make(map[string][]byte) |
|
140 |
- |
|
141 |
- // Iterate through the files in the archive. Don't add the keys |
|
142 |
- for _, f := range zipReader.File { |
|
143 |
- fNameTrimmed := strings.TrimSuffix(f.Name, filepath.Ext(f.Name)) |
|
144 |
- rc, err := f.Open() |
|
145 |
- if err != nil { |
|
146 |
- return err |
|
147 |
- } |
|
148 |
- defer rc.Close() |
|
149 |
- |
|
150 |
- fileBytes, err := ioutil.ReadAll(rc) |
|
151 |
- if err != nil { |
|
152 |
- return nil |
|
153 |
- } |
|
154 |
- |
|
155 |
- // Note that using / as a separator is okay here - the zip |
|
156 |
- // package guarantees that the separator will be / |
|
157 |
- if fNameTrimmed[len(fNameTrimmed)-5:] == "_root" { |
|
158 |
- if err = CheckRootKeyIsEncrypted(fileBytes); err != nil { |
|
159 |
- return err |
|
160 |
- } |
|
161 |
- } |
|
162 |
- newKeys[fNameTrimmed] = fileBytes |
|
163 |
- } |
|
164 |
- |
|
165 |
- for keyName, pemBytes := range newKeys { |
|
166 |
- // Get the key role information as well as its data.PrivateKey representation |
|
167 |
- _, keyInfo, err := trustmanager.KeyInfoFromPEM(pemBytes, keyName) |
|
168 |
- if err != nil { |
|
169 |
- return err |
|
170 |
- } |
|
171 |
- privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, "") |
|
172 |
- if err != nil { |
|
173 |
- privKey, _, err = trustmanager.GetPasswdDecryptBytes(retriever, pemBytes, "", "imported "+keyInfo.Role) |
|
174 |
- if err != nil { |
|
175 |
- return err |
|
176 |
- } |
|
177 |
- } |
|
178 |
- // Add the key to our cryptoservice, will add to the first successful keystore |
|
179 |
- if err = cs.AddKey(keyInfo.Role, keyInfo.Gun, privKey); err != nil { |
|
180 |
- return err |
|
181 |
- } |
|
182 |
- } |
|
183 |
- |
|
184 |
- return nil |
|
185 |
-} |
|
186 |
- |
|
187 |
-// ExportKeysByGUN exports all keys associated with a specified GUN to an |
|
188 |
-// io.Writer in zip format. passphraseRetriever is used to select new passphrases to use to |
|
189 |
-// encrypt the keys. |
|
190 |
-func (cs *CryptoService) ExportKeysByGUN(dest io.Writer, gun string, passphraseRetriever passphrase.Retriever) error { |
|
191 |
- tempBaseDir, err := ioutil.TempDir("", "notary-key-export-") |
|
192 |
- defer os.RemoveAll(tempBaseDir) |
|
193 |
- |
|
194 |
- // Create temporary keystore to use as a staging area |
|
195 |
- tempKeyStore, err := trustmanager.NewKeyFileStore(tempBaseDir, passphraseRetriever) |
|
196 |
- if err != nil { |
|
197 |
- return err |
|
198 |
- } |
|
199 |
- |
|
200 |
- for _, ks := range cs.keyStores { |
|
201 |
- if err := moveKeysByGUN(ks, tempKeyStore, gun); err != nil { |
|
202 |
- return err |
|
203 |
- } |
|
204 |
- } |
|
205 |
- |
|
206 |
- zipWriter := zip.NewWriter(dest) |
|
207 |
- |
|
208 |
- if len(tempKeyStore.ListKeys()) == 0 { |
|
209 |
- return ErrNoKeysFoundForGUN |
|
210 |
- } |
|
211 |
- |
|
212 |
- if err := addKeysToArchive(zipWriter, tempKeyStore); err != nil { |
|
213 |
- return err |
|
214 |
- } |
|
215 |
- |
|
216 |
- zipWriter.Close() |
|
217 |
- |
|
218 |
- return nil |
|
219 |
-} |
|
220 |
- |
|
221 |
-func moveKeysByGUN(oldKeyStore, newKeyStore trustmanager.KeyStore, gun string) error { |
|
222 |
- for keyID, keyInfo := range oldKeyStore.ListKeys() { |
|
223 |
- // Skip keys that aren't associated with this GUN |
|
224 |
- if keyInfo.Gun != gun { |
|
225 |
- continue |
|
226 |
- } |
|
227 |
- |
|
228 |
- privKey, _, err := oldKeyStore.GetKey(keyID) |
|
229 |
- if err != nil { |
|
230 |
- return err |
|
231 |
- } |
|
232 |
- |
|
233 |
- err = newKeyStore.AddKey(keyInfo, privKey) |
|
234 |
- if err != nil { |
|
235 |
- return err |
|
236 |
- } |
|
237 |
- } |
|
238 |
- |
|
239 |
- return nil |
|
240 |
-} |
|
241 |
- |
|
242 |
-func moveKeys(oldKeyStore, newKeyStore trustmanager.KeyStore) error { |
|
243 |
- for keyID, keyInfo := range oldKeyStore.ListKeys() { |
|
244 |
- privateKey, _, err := oldKeyStore.GetKey(keyID) |
|
245 |
- if err != nil { |
|
246 |
- return err |
|
247 |
- } |
|
248 |
- |
|
249 |
- err = newKeyStore.AddKey(keyInfo, privateKey) |
|
250 |
- |
|
251 |
- if err != nil { |
|
252 |
- return err |
|
253 |
- } |
|
254 |
- } |
|
255 |
- |
|
256 |
- return nil |
|
257 |
-} |
|
258 |
- |
|
259 |
-func addKeysToArchive(zipWriter *zip.Writer, newKeyStore *trustmanager.KeyFileStore) error { |
|
260 |
- for _, relKeyPath := range newKeyStore.ListFiles() { |
|
261 |
- fullKeyPath, err := newKeyStore.GetPath(relKeyPath) |
|
262 |
- if err != nil { |
|
263 |
- return err |
|
264 |
- } |
|
265 |
- |
|
266 |
- fi, err := os.Lstat(fullKeyPath) |
|
267 |
- if err != nil { |
|
268 |
- return err |
|
269 |
- } |
|
270 |
- |
|
271 |
- infoHeader, err := zip.FileInfoHeader(fi) |
|
272 |
- if err != nil { |
|
273 |
- return err |
|
274 |
- } |
|
275 |
- |
|
276 |
- relPath, err := filepath.Rel(newKeyStore.BaseDir(), fullKeyPath) |
|
277 |
- if err != nil { |
|
278 |
- return err |
|
279 |
- } |
|
280 |
- infoHeader.Name = relPath |
|
281 |
- |
|
282 |
- zipFileEntryWriter, err := zipWriter.CreateHeader(infoHeader) |
|
283 |
- if err != nil { |
|
284 |
- return err |
|
285 |
- } |
|
286 |
- |
|
287 |
- fileContents, err := ioutil.ReadFile(fullKeyPath) |
|
288 |
- if err != nil { |
|
289 |
- return err |
|
290 |
- } |
|
291 |
- |
|
292 |
- if _, err = zipFileEntryWriter.Write(fileContents); err != nil { |
|
293 |
- return err |
|
294 |
- } |
|
295 |
- } |
|
296 |
- |
|
297 |
- return nil |
|
298 |
-} |
|
299 |
- |
|
300 |
-// CheckRootKeyIsEncrypted makes sure the root key is encrypted. We have |
|
301 |
-// internal assumptions that depend on this. |
|
302 |
-func CheckRootKeyIsEncrypted(pemBytes []byte) error { |
|
303 |
- block, _ := pem.Decode(pemBytes) |
|
304 |
- if block == nil { |
|
305 |
- return ErrNoValidPrivateKey |
|
306 |
- } |
|
307 |
- |
|
308 |
- if !x509.IsEncryptedPEMBlock(block) { |
|
309 |
- return ErrRootKeyNotEncrypted |
|
310 |
- } |
|
311 |
- |
|
312 |
- return nil |
|
313 |
-} |
314 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,60 @@ |
0 |
+version: "2" |
|
1 |
+services: |
|
2 |
+ server: |
|
3 |
+ build: |
|
4 |
+ context: . |
|
5 |
+ dockerfile: server.Dockerfile |
|
6 |
+ networks: |
|
7 |
+ mdb: |
|
8 |
+ sig: |
|
9 |
+ srv: |
|
10 |
+ aliases: |
|
11 |
+ - notary-server |
|
12 |
+ entrypoint: /usr/bin/env sh |
|
13 |
+ command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.json" |
|
14 |
+ depends_on: |
|
15 |
+ - mysql |
|
16 |
+ - signer |
|
17 |
+ signer: |
|
18 |
+ build: |
|
19 |
+ context: . |
|
20 |
+ dockerfile: signer.Dockerfile |
|
21 |
+ networks: |
|
22 |
+ mdb: |
|
23 |
+ sig: |
|
24 |
+ aliases: |
|
25 |
+ - notarysigner |
|
26 |
+ entrypoint: /usr/bin/env sh |
|
27 |
+ command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.json" |
|
28 |
+ depends_on: |
|
29 |
+ - mysql |
|
30 |
+ mysql: |
|
31 |
+ networks: |
|
32 |
+ - mdb |
|
33 |
+ volumes: |
|
34 |
+ - ./notarymysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d |
|
35 |
+ image: mariadb:10.1.10 |
|
36 |
+ environment: |
|
37 |
+ - TERM=dumb |
|
38 |
+ - MYSQL_ALLOW_EMPTY_PASSWORD="true" |
|
39 |
+ command: mysqld --innodb_file_per_table |
|
40 |
+ client: |
|
41 |
+ build: |
|
42 |
+ context: . |
|
43 |
+ dockerfile: Dockerfile |
|
44 |
+ env_file: buildscripts/env.list |
|
45 |
+ command: buildscripts/testclient.py |
|
46 |
+ volumes: |
|
47 |
+ - ./test_output:/test_output |
|
48 |
+ networks: |
|
49 |
+ - mdb |
|
50 |
+ - srv |
|
51 |
+ depends_on: |
|
52 |
+ - server |
|
53 |
+networks: |
|
54 |
+ mdb: |
|
55 |
+ external: false |
|
56 |
+ sig: |
|
57 |
+ external: false |
|
58 |
+ srv: |
|
59 |
+ external: false |
... | ... |
@@ -11,8 +11,6 @@ services: |
11 | 11 |
links: |
12 | 12 |
- rdb-proxy:rdb-proxy.rdb |
13 | 13 |
- signer |
14 |
- environment: |
|
15 |
- - SERVICE_NAME=notary_server |
|
16 | 14 |
ports: |
17 | 15 |
- "8080" |
18 | 16 |
- "4443:4443" |
... | ... |
@@ -32,14 +30,12 @@ services: |
32 | 32 |
- notarysigner |
33 | 33 |
links: |
34 | 34 |
- rdb-proxy:rdb-proxy.rdb |
35 |
- environment: |
|
36 |
- - SERVICE_NAME=notary_signer |
|
37 | 35 |
entrypoint: /usr/bin/env sh |
38 | 36 |
command: -c "sh migrations/rethink_migrate.sh && notary-signer -config=fixtures/signer-config.rethink.json" |
39 | 37 |
depends_on: |
40 | 38 |
- rdb-proxy |
41 | 39 |
rdb-01: |
42 |
- image: jlhawn/rethinkdb:2.3.0 |
|
40 |
+ image: jlhawn/rethinkdb:2.3.4 |
|
43 | 41 |
volumes: |
44 | 42 |
- ./fixtures/rethinkdb:/tls |
45 | 43 |
- rdb-01-data:/var/data |
... | ... |
@@ -51,7 +47,7 @@ services: |
51 | 51 |
- rdb-01.rdb |
52 | 52 |
command: "--bind all --no-http-admin --server-name rdb_01 --canonical-address rdb-01.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" |
53 | 53 |
rdb-02: |
54 |
- image: jlhawn/rethinkdb:2.3.0 |
|
54 |
+ image: jlhawn/rethinkdb:2.3.4 |
|
55 | 55 |
volumes: |
56 | 56 |
- ./fixtures/rethinkdb:/tls |
57 | 57 |
- rdb-02-data:/var/data |
... | ... |
@@ -63,7 +59,7 @@ services: |
63 | 63 |
- rdb-02.rdb |
64 | 64 |
command: "--bind all --no-http-admin --server-name rdb_02 --canonical-address rdb-02.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" |
65 | 65 |
rdb-03: |
66 |
- image: jlhawn/rethinkdb:2.3.0 |
|
66 |
+ image: jlhawn/rethinkdb:2.3.4 |
|
67 | 67 |
volumes: |
68 | 68 |
- ./fixtures/rethinkdb:/tls |
69 | 69 |
- rdb-03-data:/var/data |
... | ... |
@@ -75,7 +71,7 @@ services: |
75 | 75 |
- rdb-03.rdb |
76 | 76 |
command: "--bind all --no-http-admin --server-name rdb_03 --canonical-address rdb-03.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" |
77 | 77 |
rdb-proxy: |
78 |
- image: jlhawn/rethinkdb:2.3.0 |
|
78 |
+ image: jlhawn/rethinkdb:2.3.4 |
|
79 | 79 |
ports: |
80 | 80 |
- "8080:8080" |
81 | 81 |
volumes: |
... | ... |
@@ -91,16 +87,17 @@ services: |
91 | 91 |
- rdb-02 |
92 | 92 |
- rdb-03 |
93 | 93 |
client: |
94 |
+ build: |
|
95 |
+ context: . |
|
96 |
+ dockerfile: Dockerfile |
|
94 | 97 |
volumes: |
95 | 98 |
- ./test_output:/test_output |
96 | 99 |
networks: |
97 | 100 |
- rdb |
98 |
- build: |
|
99 |
- context: . |
|
100 |
- dockerfile: Dockerfile |
|
101 |
+ env_file: buildscripts/env.list |
|
101 | 102 |
links: |
102 | 103 |
- server:notary-server |
103 |
- command: buildscripts/testclient.sh |
|
104 |
+ command: buildscripts/testclient.py |
|
104 | 105 |
volumes: |
105 | 106 |
rdb-01-data: |
106 | 107 |
external: false |
... | ... |
@@ -110,4 +107,4 @@ volumes: |
110 | 110 |
external: false |
111 | 111 |
networks: |
112 | 112 |
rdb: |
113 |
- external: false |
|
114 | 113 |
\ No newline at end of file |
114 |
+ external: false |
115 | 115 |
deleted file mode 100644 |
... | ... |
@@ -1,36 +0,0 @@ |
1 |
-server: |
|
2 |
- build: . |
|
3 |
- dockerfile: server.Dockerfile |
|
4 |
- links: |
|
5 |
- - mysql |
|
6 |
- - signer |
|
7 |
- - signer:notarysigner |
|
8 |
- environment: |
|
9 |
- - SERVICE_NAME=notary_server |
|
10 |
- entrypoint: /usr/bin/env sh |
|
11 |
- command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.json" |
|
12 |
-signer: |
|
13 |
- build: . |
|
14 |
- dockerfile: signer.Dockerfile |
|
15 |
- links: |
|
16 |
- - mysql |
|
17 |
- environment: |
|
18 |
- - SERVICE_NAME=notary_signer |
|
19 |
- entrypoint: /usr/bin/env sh |
|
20 |
- command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.json" |
|
21 |
-mysql: |
|
22 |
- volumes: |
|
23 |
- - ./notarymysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d |
|
24 |
- image: mariadb:10.1.10 |
|
25 |
- environment: |
|
26 |
- - TERM=dumb |
|
27 |
- - MYSQL_ALLOW_EMPTY_PASSWORD="true" |
|
28 |
- command: mysqld --innodb_file_per_table |
|
29 |
-client: |
|
30 |
- volumes: |
|
31 |
- - ./test_output:/test_output |
|
32 |
- build: . |
|
33 |
- dockerfile: Dockerfile |
|
34 |
- links: |
|
35 |
- - server:notary-server |
|
36 |
- command: buildscripts/testclient.sh |
... | ... |
@@ -1,7 +1,7 @@ |
1 | 1 |
version: "2" |
2 | 2 |
services: |
3 | 3 |
server: |
4 |
- build: |
|
4 |
+ build: |
|
5 | 5 |
context: . |
6 | 6 |
dockerfile: server.Dockerfile |
7 | 7 |
volumes: |
... | ... |
@@ -11,17 +11,14 @@ services: |
11 | 11 |
links: |
12 | 12 |
- rdb-proxy:rdb-proxy.rdb |
13 | 13 |
- signer |
14 |
- environment: |
|
15 |
- - SERVICE_NAME=notary_server |
|
16 | 14 |
ports: |
17 |
- - "8080" |
|
18 | 15 |
- "4443:4443" |
19 | 16 |
entrypoint: /usr/bin/env sh |
20 | 17 |
command: -c "sh migrations/rethink_migrate.sh && notary-server -config=fixtures/server-config.rethink.json" |
21 | 18 |
depends_on: |
22 | 19 |
- rdb-proxy |
23 | 20 |
signer: |
24 |
- build: |
|
21 |
+ build: |
|
25 | 22 |
context: . |
26 | 23 |
dockerfile: signer.Dockerfile |
27 | 24 |
volumes: |
... | ... |
@@ -32,50 +29,47 @@ services: |
32 | 32 |
- notarysigner |
33 | 33 |
links: |
34 | 34 |
- rdb-proxy:rdb-proxy.rdb |
35 |
- environment: |
|
36 |
- - SERVICE_NAME=notary_signer |
|
37 | 35 |
entrypoint: /usr/bin/env sh |
38 | 36 |
command: -c "sh migrations/rethink_migrate.sh && notary-signer -config=fixtures/signer-config.rethink.json" |
39 | 37 |
depends_on: |
40 | 38 |
- rdb-proxy |
41 | 39 |
rdb-01: |
42 |
- image: jlhawn/rethinkdb:2.3.0 |
|
40 |
+ image: jlhawn/rethinkdb:2.3.4 |
|
43 | 41 |
volumes: |
44 | 42 |
- ./fixtures/rethinkdb:/tls |
45 | 43 |
- rdb-01-data:/var/data |
46 | 44 |
networks: |
47 | 45 |
rdb: |
48 | 46 |
aliases: |
49 |
- - rdb |
|
50 |
- - rdb.rdb |
|
51 | 47 |
- rdb-01.rdb |
52 |
- command: "--bind all --no-http-admin --server-name rdb_01 --canonical-address rdb-01.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" |
|
48 |
+ command: "--bind all --no-http-admin --server-name rdb_01 --canonical-address rdb-01.rdb --directory /var/data/rethinkdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" |
|
53 | 49 |
rdb-02: |
54 |
- image: jlhawn/rethinkdb:2.3.0 |
|
50 |
+ image: jlhawn/rethinkdb:2.3.4 |
|
55 | 51 |
volumes: |
56 | 52 |
- ./fixtures/rethinkdb:/tls |
57 | 53 |
- rdb-02-data:/var/data |
58 | 54 |
networks: |
59 | 55 |
rdb: |
60 | 56 |
aliases: |
61 |
- - rdb |
|
62 |
- - rdb.rdb |
|
63 | 57 |
- rdb-02.rdb |
64 |
- command: "--bind all --no-http-admin --server-name rdb_02 --canonical-address rdb-02.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" |
|
58 |
+ command: "--bind all --no-http-admin --server-name rdb_02 --canonical-address rdb-02.rdb --directory /var/data/rethinkdb --join rdb-01 --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" |
|
59 |
+ depends_on: |
|
60 |
+ - rdb-01 |
|
65 | 61 |
rdb-03: |
66 |
- image: jlhawn/rethinkdb:2.3.0 |
|
62 |
+ image: jlhawn/rethinkdb:2.3.4 |
|
67 | 63 |
volumes: |
68 | 64 |
- ./fixtures/rethinkdb:/tls |
69 | 65 |
- rdb-03-data:/var/data |
70 | 66 |
networks: |
71 | 67 |
rdb: |
72 | 68 |
aliases: |
73 |
- - rdb |
|
74 |
- - rdb.rdb |
|
75 | 69 |
- rdb-03.rdb |
76 |
- command: "--bind all --no-http-admin --server-name rdb_03 --canonical-address rdb-03.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" |
|
70 |
+ command: "--bind all --no-http-admin --server-name rdb_03 --canonical-address rdb-03.rdb --directory /var/data/rethinkdb --join rdb-02 --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" |
|
71 |
+ depends_on: |
|
72 |
+ - rdb-01 |
|
73 |
+ - rdb-02 |
|
77 | 74 |
rdb-proxy: |
78 |
- image: jlhawn/rethinkdb:2.3.0 |
|
75 |
+ image: jlhawn/rethinkdb:2.3.4 |
|
79 | 76 |
ports: |
80 | 77 |
- "8080:8080" |
81 | 78 |
volumes: |
... | ... |
@@ -85,7 +79,7 @@ services: |
85 | 85 |
aliases: |
86 | 86 |
- rdb-proxy |
87 | 87 |
- rdb-proxy.rdp |
88 |
- command: "proxy --bind all --join rdb.rdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" |
|
88 |
+ command: "proxy --bind all --join rdb-03 --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" |
|
89 | 89 |
depends_on: |
90 | 90 |
- rdb-01 |
91 | 91 |
- rdb-02 |
... | ... |
@@ -1,34 +1,49 @@ |
1 |
-server: |
|
2 |
- build: . |
|
3 |
- dockerfile: server.Dockerfile |
|
4 |
- links: |
|
5 |
- - mysql |
|
6 |
- - signer |
|
7 |
- - signer:notarysigner |
|
8 |
- environment: |
|
9 |
- - SERVICE_NAME=notary_server |
|
10 |
- ports: |
|
11 |
- - "8080" |
|
12 |
- - "4443:4443" |
|
13 |
- entrypoint: /usr/bin/env sh |
|
14 |
- command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.json" |
|
15 |
-signer: |
|
16 |
- build: . |
|
17 |
- dockerfile: signer.Dockerfile |
|
18 |
- links: |
|
19 |
- - mysql |
|
20 |
- environment: |
|
21 |
- - SERVICE_NAME=notary_signer |
|
22 |
- entrypoint: /usr/bin/env sh |
|
23 |
- command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.json" |
|
24 |
-mysql: |
|
25 |
- volumes: |
|
26 |
- - ./notarymysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d |
|
27 |
- - notary_data:/var/lib/mysql |
|
28 |
- image: mariadb:10.1.10 |
|
29 |
- ports: |
|
30 |
- - "3306:3306" |
|
31 |
- environment: |
|
32 |
- - TERM=dumb |
|
33 |
- - MYSQL_ALLOW_EMPTY_PASSWORD="true" |
|
34 |
- command: mysqld --innodb_file_per_table |
|
1 |
+version: "2" |
|
2 |
+services: |
|
3 |
+ server: |
|
4 |
+ build: |
|
5 |
+ context: . |
|
6 |
+ dockerfile: server.Dockerfile |
|
7 |
+ networks: |
|
8 |
+ - mdb |
|
9 |
+ - sig |
|
10 |
+ ports: |
|
11 |
+ - "8080" |
|
12 |
+ - "4443:4443" |
|
13 |
+ entrypoint: /usr/bin/env sh |
|
14 |
+ command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.json" |
|
15 |
+ depends_on: |
|
16 |
+ - mysql |
|
17 |
+ - signer |
|
18 |
+ signer: |
|
19 |
+ build: |
|
20 |
+ context: . |
|
21 |
+ dockerfile: signer.Dockerfile |
|
22 |
+ networks: |
|
23 |
+ mdb: |
|
24 |
+ sig: |
|
25 |
+ aliases: |
|
26 |
+ - notarysigner |
|
27 |
+ entrypoint: /usr/bin/env sh |
|
28 |
+ command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.json" |
|
29 |
+ depends_on: |
|
30 |
+ - mysql |
|
31 |
+ mysql: |
|
32 |
+ networks: |
|
33 |
+ - mdb |
|
34 |
+ volumes: |
|
35 |
+ - ./notarymysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d |
|
36 |
+ - notary_data:/var/lib/mysql |
|
37 |
+ image: mariadb:10.1.10 |
|
38 |
+ environment: |
|
39 |
+ - TERM=dumb |
|
40 |
+ - MYSQL_ALLOW_EMPTY_PASSWORD="true" |
|
41 |
+ command: mysqld --innodb_file_per_table |
|
42 |
+volumes: |
|
43 |
+ notary_data: |
|
44 |
+ external: false |
|
45 |
+networks: |
|
46 |
+ mdb: |
|
47 |
+ external: false |
|
48 |
+ sig: |
|
49 |
+ external: false |
35 | 50 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,7 @@ |
0 |
+package notary |
|
1 |
+ |
|
2 |
+// PassRetriever is a callback function that should retrieve a passphrase |
|
3 |
+// for a given named key. If it should be treated as new passphrase (e.g. with |
|
4 |
+// confirmation), createNew will be true. Attempts is passed in so that implementers |
|
5 |
+// decide how many chances to give to a human, for example. |
|
6 |
+type PassRetriever func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) |
... | ... |
@@ -8,19 +8,13 @@ import ( |
8 | 8 |
"fmt" |
9 | 9 |
"io" |
10 | 10 |
"os" |
11 |
- "strings" |
|
12 |
- |
|
13 | 11 |
"path/filepath" |
12 |
+ "strings" |
|
14 | 13 |
|
15 | 14 |
"github.com/docker/docker/pkg/term" |
15 |
+ "github.com/docker/notary" |
|
16 | 16 |
) |
17 | 17 |
|
18 |
-// Retriever is a callback function that should retrieve a passphrase |
|
19 |
-// for a given named key. If it should be treated as new passphrase (e.g. with |
|
20 |
-// confirmation), createNew will be true. Attempts is passed in so that implementers |
|
21 |
-// decide how many chances to give to a human, for example. |
|
22 |
-type Retriever func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) |
|
23 |
- |
|
24 | 18 |
const ( |
25 | 19 |
idBytesToDisplay = 7 |
26 | 20 |
tufRootAlias = "root" |
... | ... |
@@ -46,155 +40,165 @@ var ( |
46 | 46 |
// ErrTooManyAttempts is returned if the maximum number of passphrase |
47 | 47 |
// entry attempts is reached. |
48 | 48 |
ErrTooManyAttempts = errors.New("Too many attempts") |
49 |
+ |
|
50 |
+ // ErrNoInput is returned if we do not have a valid input method for passphrases |
|
51 |
+ ErrNoInput = errors.New("Please either use environment variables or STDIN with a terminal to provide key passphrases") |
|
49 | 52 |
) |
50 | 53 |
|
51 | 54 |
// PromptRetriever returns a new Retriever which will provide a prompt on stdin |
52 |
-// and stdout to retrieve a passphrase. The passphrase will be cached such that |
|
55 |
+// and stdout to retrieve a passphrase. stdin will be checked if it is a terminal, |
|
56 |
+// else the PromptRetriever will error when attempting to retrieve a passphrase. |
|
57 |
+// Upon successful passphrase retrievals, the passphrase will be cached such that |
|
53 | 58 |
// subsequent prompts will produce the same passphrase. |
54 |
-func PromptRetriever() Retriever { |
|
59 |
+func PromptRetriever() notary.PassRetriever { |
|
60 |
+ if !term.IsTerminal(os.Stdin.Fd()) { |
|
61 |
+ return func(string, string, bool, int) (string, bool, error) { |
|
62 |
+ return "", false, ErrNoInput |
|
63 |
+ } |
|
64 |
+ } |
|
55 | 65 |
return PromptRetrieverWithInOut(os.Stdin, os.Stdout, nil) |
56 | 66 |
} |
57 | 67 |
|
58 |
-// PromptRetrieverWithInOut returns a new Retriever which will provide a |
|
59 |
-// prompt using the given in and out readers. The passphrase will be cached |
|
60 |
-// such that subsequent prompts will produce the same passphrase. |
|
61 |
-// aliasMap can be used to specify display names for TUF key aliases. If aliasMap |
|
62 |
-// is nil, a sensible default will be used. |
|
63 |
-func PromptRetrieverWithInOut(in io.Reader, out io.Writer, aliasMap map[string]string) Retriever { |
|
64 |
- userEnteredTargetsSnapshotsPass := false |
|
65 |
- targetsSnapshotsPass := "" |
|
66 |
- userEnteredRootsPass := false |
|
67 |
- rootsPass := "" |
|
68 |
- |
|
69 |
- return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { |
|
70 |
- if alias == tufRootAlias && createNew && numAttempts == 0 { |
|
71 |
- fmt.Fprintln(out, tufRootKeyGenerationWarning) |
|
72 |
- } |
|
73 |
- if numAttempts > 0 { |
|
74 |
- if !createNew { |
|
75 |
- fmt.Fprintln(out, "Passphrase incorrect. Please retry.") |
|
76 |
- } |
|
77 |
- } |
|
78 |
- |
|
79 |
- // Figure out if we should display a different string for this alias |
|
80 |
- displayAlias := alias |
|
81 |
- if aliasMap != nil { |
|
82 |
- if val, ok := aliasMap[alias]; ok { |
|
83 |
- displayAlias = val |
|
84 |
- } |
|
68 |
+type boundRetriever struct { |
|
69 |
+ in io.Reader |
|
70 |
+ out io.Writer |
|
71 |
+ aliasMap map[string]string |
|
72 |
+ passphraseCache map[string]string |
|
73 |
+} |
|
85 | 74 |
|
75 |
+func (br *boundRetriever) getPassphrase(keyName, alias string, createNew bool, numAttempts int) (string, bool, error) { |
|
76 |
+ if numAttempts == 0 { |
|
77 |
+ if alias == tufRootAlias && createNew { |
|
78 |
+ fmt.Fprintln(br.out, tufRootKeyGenerationWarning) |
|
86 | 79 |
} |
87 | 80 |
|
88 |
- // First, check if we have a password cached for this alias. |
|
89 |
- if numAttempts == 0 { |
|
90 |
- if userEnteredTargetsSnapshotsPass && (alias == tufSnapshotAlias || alias == tufTargetsAlias) { |
|
91 |
- return targetsSnapshotsPass, false, nil |
|
92 |
- } |
|
93 |
- if userEnteredRootsPass && (alias == "root") { |
|
94 |
- return rootsPass, false, nil |
|
95 |
- } |
|
81 |
+ if pass, ok := br.passphraseCache[alias]; ok { |
|
82 |
+ return pass, false, nil |
|
96 | 83 |
} |
97 |
- |
|
98 |
- if numAttempts > 3 && !createNew { |
|
84 |
+ } else if !createNew { // per `if`, numAttempts > 0 if we're at this `else` |
|
85 |
+ if numAttempts > 3 { |
|
99 | 86 |
return "", true, ErrTooManyAttempts |
100 | 87 |
} |
88 |
+ fmt.Fprintln(br.out, "Passphrase incorrect. Please retry.") |
|
89 |
+ } |
|
101 | 90 |
|
102 |
- // If typing on the terminal, we do not want the terminal to echo the |
|
103 |
- // password that is typed (so it doesn't display) |
|
104 |
- if term.IsTerminal(0) { |
|
105 |
- state, err := term.SaveState(0) |
|
106 |
- if err != nil { |
|
107 |
- return "", false, err |
|
108 |
- } |
|
109 |
- term.DisableEcho(0, state) |
|
110 |
- defer term.RestoreTerminal(0, state) |
|
111 |
- } |
|
112 |
- |
|
113 |
- stdin := bufio.NewReader(in) |
|
91 |
+ // passphrase not cached and we're not aborting, get passphrase from user! |
|
92 |
+ return br.requestPassphrase(keyName, alias, createNew, numAttempts) |
|
93 |
+} |
|
114 | 94 |
|
115 |
- indexOfLastSeparator := strings.LastIndex(keyName, string(filepath.Separator)) |
|
116 |
- if indexOfLastSeparator == -1 { |
|
117 |
- indexOfLastSeparator = 0 |
|
118 |
- } |
|
95 |
+func (br *boundRetriever) requestPassphrase(keyName, alias string, createNew bool, numAttempts int) (string, bool, error) { |
|
96 |
+ // Figure out if we should display a different string for this alias |
|
97 |
+ displayAlias := alias |
|
98 |
+ if val, ok := br.aliasMap[alias]; ok { |
|
99 |
+ displayAlias = val |
|
100 |
+ } |
|
119 | 101 |
|
120 |
- var shortName string |
|
121 |
- if len(keyName) > indexOfLastSeparator+idBytesToDisplay { |
|
122 |
- if indexOfLastSeparator > 0 { |
|
123 |
- keyNamePrefix := keyName[:indexOfLastSeparator] |
|
124 |
- keyNameID := keyName[indexOfLastSeparator+1 : indexOfLastSeparator+idBytesToDisplay+1] |
|
125 |
- shortName = keyNameID + " (" + keyNamePrefix + ")" |
|
126 |
- } else { |
|
127 |
- shortName = keyName[indexOfLastSeparator : indexOfLastSeparator+idBytesToDisplay] |
|
128 |
- } |
|
102 |
+ // If typing on the terminal, we do not want the terminal to echo the |
|
103 |
+ // password that is typed (so it doesn't display) |
|
104 |
+ if term.IsTerminal(os.Stdin.Fd()) { |
|
105 |
+ state, err := term.SaveState(os.Stdin.Fd()) |
|
106 |
+ if err != nil { |
|
107 |
+ return "", false, err |
|
129 | 108 |
} |
109 |
+ term.DisableEcho(os.Stdin.Fd(), state) |
|
110 |
+ defer term.RestoreTerminal(os.Stdin.Fd(), state) |
|
111 |
+ } |
|
130 | 112 |
|
131 |
- withID := fmt.Sprintf(" with ID %s", shortName) |
|
132 |
- if shortName == "" { |
|
133 |
- withID = "" |
|
134 |
- } |
|
113 |
+ indexOfLastSeparator := strings.LastIndex(keyName, string(filepath.Separator)) |
|
114 |
+ if indexOfLastSeparator == -1 { |
|
115 |
+ indexOfLastSeparator = 0 |
|
116 |
+ } |
|
135 | 117 |
|
136 |
- if createNew { |
|
137 |
- fmt.Fprintf(out, "Enter passphrase for new %s key%s: ", displayAlias, withID) |
|
138 |
- } else if displayAlias == "yubikey" { |
|
139 |
- fmt.Fprintf(out, "Enter the %s for the attached Yubikey: ", keyName) |
|
118 |
+ var shortName string |
|
119 |
+ if len(keyName) > indexOfLastSeparator+idBytesToDisplay { |
|
120 |
+ if indexOfLastSeparator > 0 { |
|
121 |
+ keyNamePrefix := keyName[:indexOfLastSeparator] |
|
122 |
+ keyNameID := keyName[indexOfLastSeparator+1 : indexOfLastSeparator+idBytesToDisplay+1] |
|
123 |
+ shortName = keyNameID + " (" + keyNamePrefix + ")" |
|
140 | 124 |
} else { |
141 |
- fmt.Fprintf(out, "Enter passphrase for %s key%s: ", displayAlias, withID) |
|
125 |
+ shortName = keyName[indexOfLastSeparator : indexOfLastSeparator+idBytesToDisplay] |
|
142 | 126 |
} |
127 |
+ } |
|
143 | 128 |
|
144 |
- passphrase, err := stdin.ReadBytes('\n') |
|
145 |
- fmt.Fprintln(out) |
|
146 |
- if err != nil { |
|
147 |
- return "", false, err |
|
148 |
- } |
|
129 |
+ withID := fmt.Sprintf(" with ID %s", shortName) |
|
130 |
+ if shortName == "" { |
|
131 |
+ withID = "" |
|
132 |
+ } |
|
149 | 133 |
|
150 |
- retPass := strings.TrimSpace(string(passphrase)) |
|
151 |
- |
|
152 |
- if !createNew { |
|
153 |
- if alias == tufSnapshotAlias || alias == tufTargetsAlias { |
|
154 |
- userEnteredTargetsSnapshotsPass = true |
|
155 |
- targetsSnapshotsPass = retPass |
|
156 |
- } |
|
157 |
- if alias == tufRootAlias { |
|
158 |
- userEnteredRootsPass = true |
|
159 |
- rootsPass = retPass |
|
160 |
- } |
|
161 |
- return retPass, false, nil |
|
162 |
- } |
|
134 |
+ switch { |
|
135 |
+ case createNew: |
|
136 |
+ fmt.Fprintf(br.out, "Enter passphrase for new %s key%s: ", displayAlias, withID) |
|
137 |
+ case displayAlias == "yubikey": |
|
138 |
+ fmt.Fprintf(br.out, "Enter the %s for the attached Yubikey: ", keyName) |
|
139 |
+ default: |
|
140 |
+ fmt.Fprintf(br.out, "Enter passphrase for %s key%s: ", displayAlias, withID) |
|
141 |
+ } |
|
163 | 142 |
|
164 |
- if len(retPass) < 8 { |
|
165 |
- fmt.Fprintln(out, "Passphrase is too short. Please use a password manager to generate and store a good random passphrase.") |
|
166 |
- return "", false, ErrTooShort |
|
167 |
- } |
|
143 |
+ stdin := bufio.NewReader(br.in) |
|
144 |
+ passphrase, err := stdin.ReadBytes('\n') |
|
145 |
+ fmt.Fprintln(br.out) |
|
146 |
+ if err != nil { |
|
147 |
+ return "", false, err |
|
148 |
+ } |
|
168 | 149 |
|
169 |
- fmt.Fprintf(out, "Repeat passphrase for new %s key%s: ", displayAlias, withID) |
|
170 |
- confirmation, err := stdin.ReadBytes('\n') |
|
171 |
- fmt.Fprintln(out) |
|
150 |
+ retPass := strings.TrimSpace(string(passphrase)) |
|
151 |
+ |
|
152 |
+ if createNew { |
|
153 |
+ err = br.verifyAndConfirmPassword(stdin, retPass, displayAlias, withID) |
|
172 | 154 |
if err != nil { |
173 | 155 |
return "", false, err |
174 | 156 |
} |
175 |
- confirmationStr := strings.TrimSpace(string(confirmation)) |
|
157 |
+ } |
|
176 | 158 |
|
177 |
- if retPass != confirmationStr { |
|
178 |
- fmt.Fprintln(out, "Passphrases do not match. Please retry.") |
|
179 |
- return "", false, ErrDontMatch |
|
180 |
- } |
|
159 |
+ br.cachePassword(alias, retPass) |
|
181 | 160 |
|
182 |
- if alias == tufSnapshotAlias || alias == tufTargetsAlias { |
|
183 |
- userEnteredTargetsSnapshotsPass = true |
|
184 |
- targetsSnapshotsPass = retPass |
|
185 |
- } |
|
186 |
- if alias == tufRootAlias { |
|
187 |
- userEnteredRootsPass = true |
|
188 |
- rootsPass = retPass |
|
189 |
- } |
|
161 |
+ return retPass, false, nil |
|
162 |
+} |
|
190 | 163 |
|
191 |
- return retPass, false, nil |
|
164 |
+func (br *boundRetriever) verifyAndConfirmPassword(stdin *bufio.Reader, retPass, displayAlias, withID string) error { |
|
165 |
+ if len(retPass) < 8 { |
|
166 |
+ fmt.Fprintln(br.out, "Passphrase is too short. Please use a password manager to generate and store a good random passphrase.") |
|
167 |
+ return ErrTooShort |
|
192 | 168 |
} |
169 |
+ |
|
170 |
+ fmt.Fprintf(br.out, "Repeat passphrase for new %s key%s: ", displayAlias, withID) |
|
171 |
+ confirmation, err := stdin.ReadBytes('\n') |
|
172 |
+ fmt.Fprintln(br.out) |
|
173 |
+ if err != nil { |
|
174 |
+ return err |
|
175 |
+ } |
|
176 |
+ confirmationStr := strings.TrimSpace(string(confirmation)) |
|
177 |
+ |
|
178 |
+ if retPass != confirmationStr { |
|
179 |
+ fmt.Fprintln(br.out, "Passphrases do not match. Please retry.") |
|
180 |
+ return ErrDontMatch |
|
181 |
+ } |
|
182 |
+ return nil |
|
183 |
+} |
|
184 |
+ |
|
185 |
+func (br *boundRetriever) cachePassword(alias, retPass string) { |
|
186 |
+ br.passphraseCache[alias] = retPass |
|
187 |
+} |
|
188 |
+ |
|
189 |
+// PromptRetrieverWithInOut returns a new Retriever which will provide a |
|
190 |
+// prompt using the given in and out readers. The passphrase will be cached |
|
191 |
+// such that subsequent prompts will produce the same passphrase. |
|
192 |
+// aliasMap can be used to specify display names for TUF key aliases. If aliasMap |
|
193 |
+// is nil, a sensible default will be used. |
|
194 |
+func PromptRetrieverWithInOut(in io.Reader, out io.Writer, aliasMap map[string]string) notary.PassRetriever { |
|
195 |
+ bound := &boundRetriever{ |
|
196 |
+ in: in, |
|
197 |
+ out: out, |
|
198 |
+ aliasMap: aliasMap, |
|
199 |
+ passphraseCache: make(map[string]string), |
|
200 |
+ } |
|
201 |
+ |
|
202 |
+ return bound.getPassphrase |
|
193 | 203 |
} |
194 | 204 |
|
195 | 205 |
// ConstantRetriever returns a new Retriever which will return a constant string |
196 | 206 |
// as a passphrase. |
197 |
-func ConstantRetriever(constantPassphrase string) Retriever { |
|
207 |
+func ConstantRetriever(constantPassphrase string) notary.PassRetriever { |
|
198 | 208 |
return func(k, a string, c bool, n int) (string, bool, error) { |
199 | 209 |
return constantPassphrase, false, nil |
200 | 210 |
} |
... | ... |
@@ -1,4 +1,4 @@ |
1 |
-FROM golang:1.6.1-alpine |
|
1 |
+FROM golang:1.7.1-alpine |
|
2 | 2 |
MAINTAINER David Lawrence "david.lawrence@docker.com" |
3 | 3 |
|
4 | 4 |
RUN apk add --update git gcc libc-dev && rm -rf /var/cache/apk/* |
... | ... |
@@ -13,6 +13,7 @@ COPY . /go/src/${NOTARYPKG} |
13 | 13 |
|
14 | 14 |
WORKDIR /go/src/${NOTARYPKG} |
15 | 15 |
|
16 |
+ENV SERVICE_NAME=notary_server |
|
16 | 17 |
EXPOSE 4443 |
17 | 18 |
|
18 | 19 |
# Install notary-server |
... | ... |
@@ -1,4 +1,4 @@ |
1 |
-FROM golang:1.6.1-alpine |
|
1 |
+FROM golang:1.7.1-alpine |
|
2 | 2 |
MAINTAINER David Lawrence "david.lawrence@docker.com" |
3 | 3 |
|
4 | 4 |
RUN apk add --update git gcc libc-dev && rm -rf /var/cache/apk/* |
... | ... |
@@ -13,11 +13,10 @@ COPY . /go/src/${NOTARYPKG} |
13 | 13 |
|
14 | 14 |
WORKDIR /go/src/${NOTARYPKG} |
15 | 15 |
|
16 |
+ENV SERVICE_NAME=notary_signer |
|
16 | 17 |
ENV NOTARY_SIGNER_DEFAULT_ALIAS="timestamp_1" |
17 | 18 |
ENV NOTARY_SIGNER_TIMESTAMP_1="testpassword" |
18 | 19 |
|
19 |
-EXPOSE 4444 |
|
20 |
- |
|
21 | 20 |
# Install notary-signer |
22 | 21 |
RUN go install \ |
23 | 22 |
-tags pkcs11 \ |
24 | 23 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,22 @@ |
0 |
+package storage |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "errors" |
|
4 |
+ "fmt" |
|
5 |
+) |
|
6 |
+ |
|
7 |
+var ( |
|
8 |
+ // ErrPathOutsideStore indicates that the returned path would be |
|
9 |
+ // outside the store |
|
10 |
+ ErrPathOutsideStore = errors.New("path outside file store") |
|
11 |
+) |
|
12 |
+ |
|
13 |
+// ErrMetaNotFound indicates we did not find a particular piece |
|
14 |
+// of metadata in the store |
|
15 |
+type ErrMetaNotFound struct { |
|
16 |
+ Resource string |
|
17 |
+} |
|
18 |
+ |
|
19 |
+func (err ErrMetaNotFound) Error() string { |
|
20 |
+ return fmt.Sprintf("%s trust data unavailable. Has a notary repository been initialized?", err.Resource) |
|
21 |
+} |
0 | 22 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,222 @@ |
0 |
+package storage |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "fmt" |
|
4 |
+ "io" |
|
5 |
+ "io/ioutil" |
|
6 |
+ "os" |
|
7 |
+ "path/filepath" |
|
8 |
+ "strings" |
|
9 |
+ |
|
10 |
+ "github.com/docker/notary" |
|
11 |
+) |
|
12 |
+ |
|
13 |
+// NewFilesystemStore creates a new store in a directory tree |
|
14 |
+func NewFilesystemStore(baseDir, subDir, extension string) (*FilesystemStore, error) { |
|
15 |
+ baseDir = filepath.Join(baseDir, subDir) |
|
16 |
+ |
|
17 |
+ return NewFileStore(baseDir, extension, notary.PrivKeyPerms) |
|
18 |
+} |
|
19 |
+ |
|
20 |
+// NewFileStore creates a fully configurable file store |
|
21 |
+func NewFileStore(baseDir, fileExt string, perms os.FileMode) (*FilesystemStore, error) { |
|
22 |
+ baseDir = filepath.Clean(baseDir) |
|
23 |
+ if err := createDirectory(baseDir, perms); err != nil { |
|
24 |
+ return nil, err |
|
25 |
+ } |
|
26 |
+ if !strings.HasPrefix(fileExt, ".") { |
|
27 |
+ fileExt = "." + fileExt |
|
28 |
+ } |
|
29 |
+ |
|
30 |
+ return &FilesystemStore{ |
|
31 |
+ baseDir: baseDir, |
|
32 |
+ ext: fileExt, |
|
33 |
+ perms: perms, |
|
34 |
+ }, nil |
|
35 |
+} |
|
36 |
+ |
|
37 |
+// NewSimpleFileStore is a convenience wrapper to create a world readable, |
|
38 |
+// owner writeable filestore |
|
39 |
+func NewSimpleFileStore(baseDir, fileExt string) (*FilesystemStore, error) { |
|
40 |
+ return NewFileStore(baseDir, fileExt, notary.PubCertPerms) |
|
41 |
+} |
|
42 |
+ |
|
43 |
+// NewPrivateKeyFileStorage initializes a new filestore for private keys, appending |
|
44 |
+// the notary.PrivDir to the baseDir. |
|
45 |
+func NewPrivateKeyFileStorage(baseDir, fileExt string) (*FilesystemStore, error) { |
|
46 |
+ baseDir = filepath.Join(baseDir, notary.PrivDir) |
|
47 |
+ return NewFileStore(baseDir, fileExt, notary.PrivKeyPerms) |
|
48 |
+} |
|
49 |
+ |
|
50 |
+// NewPrivateSimpleFileStore is a wrapper to create an owner readable/writeable |
|
51 |
+// _only_ filestore |
|
52 |
+func NewPrivateSimpleFileStore(baseDir, fileExt string) (*FilesystemStore, error) { |
|
53 |
+ return NewFileStore(baseDir, fileExt, notary.PrivKeyPerms) |
|
54 |
+} |
|
55 |
+ |
|
56 |
+// FilesystemStore is a store in a locally accessible directory |
|
57 |
+type FilesystemStore struct { |
|
58 |
+ baseDir string |
|
59 |
+ ext string |
|
60 |
+ perms os.FileMode |
|
61 |
+} |
|
62 |
+ |
|
63 |
+func (f *FilesystemStore) getPath(name string) (string, error) { |
|
64 |
+ fileName := fmt.Sprintf("%s%s", name, f.ext) |
|
65 |
+ fullPath := filepath.Join(f.baseDir, fileName) |
|
66 |
+ |
|
67 |
+ if !strings.HasPrefix(fullPath, f.baseDir) { |
|
68 |
+ return "", ErrPathOutsideStore |
|
69 |
+ } |
|
70 |
+ return fullPath, nil |
|
71 |
+} |
|
72 |
+ |
|
73 |
+// GetSized returns the meta for the given name (a role) up to size bytes |
|
74 |
+// If size is "NoSizeLimit", this corresponds to "infinite," but we cut off at a |
|
75 |
+// predefined threshold "notary.MaxDownloadSize". If the file is larger than size |
|
76 |
+// we return ErrMaliciousServer for consistency with the HTTPStore |
|
77 |
+func (f *FilesystemStore) GetSized(name string, size int64) ([]byte, error) { |
|
78 |
+ p, err := f.getPath(name) |
|
79 |
+ if err != nil { |
|
80 |
+ return nil, err |
|
81 |
+ } |
|
82 |
+ file, err := os.OpenFile(p, os.O_RDONLY, f.perms) |
|
83 |
+ if err != nil { |
|
84 |
+ if os.IsNotExist(err) { |
|
85 |
+ err = ErrMetaNotFound{Resource: name} |
|
86 |
+ } |
|
87 |
+ return nil, err |
|
88 |
+ } |
|
89 |
+ defer file.Close() |
|
90 |
+ |
|
91 |
+ if size == NoSizeLimit { |
|
92 |
+ size = notary.MaxDownloadSize |
|
93 |
+ } |
|
94 |
+ |
|
95 |
+ stat, err := file.Stat() |
|
96 |
+ if err != nil { |
|
97 |
+ return nil, err |
|
98 |
+ } |
|
99 |
+ if stat.Size() > size { |
|
100 |
+ return nil, ErrMaliciousServer{} |
|
101 |
+ } |
|
102 |
+ |
|
103 |
+ l := io.LimitReader(file, size) |
|
104 |
+ return ioutil.ReadAll(l) |
|
105 |
+} |
|
106 |
+ |
|
107 |
+// Get returns the meta for the given name. |
|
108 |
+func (f *FilesystemStore) Get(name string) ([]byte, error) { |
|
109 |
+ p, err := f.getPath(name) |
|
110 |
+ if err != nil { |
|
111 |
+ return nil, err |
|
112 |
+ } |
|
113 |
+ meta, err := ioutil.ReadFile(p) |
|
114 |
+ if err != nil { |
|
115 |
+ if os.IsNotExist(err) { |
|
116 |
+ err = ErrMetaNotFound{Resource: name} |
|
117 |
+ } |
|
118 |
+ return nil, err |
|
119 |
+ } |
|
120 |
+ return meta, nil |
|
121 |
+} |
|
122 |
+ |
|
123 |
+// SetMulti sets the metadata for multiple roles in one operation |
|
124 |
+func (f *FilesystemStore) SetMulti(metas map[string][]byte) error { |
|
125 |
+ for role, blob := range metas { |
|
126 |
+ err := f.Set(role, blob) |
|
127 |
+ if err != nil { |
|
128 |
+ return err |
|
129 |
+ } |
|
130 |
+ } |
|
131 |
+ return nil |
|
132 |
+} |
|
133 |
+ |
|
134 |
+// Set sets the meta for a single role |
|
135 |
+func (f *FilesystemStore) Set(name string, meta []byte) error { |
|
136 |
+ fp, err := f.getPath(name) |
|
137 |
+ if err != nil { |
|
138 |
+ return err |
|
139 |
+ } |
|
140 |
+ |
|
141 |
+ // Ensures the parent directories of the file we are about to write exist |
|
142 |
+ err = os.MkdirAll(filepath.Dir(fp), f.perms) |
|
143 |
+ if err != nil { |
|
144 |
+ return err |
|
145 |
+ } |
|
146 |
+ |
|
147 |
+ // if something already exists, just delete it and re-write it |
|
148 |
+ os.RemoveAll(fp) |
|
149 |
+ |
|
150 |
+ // Write the file to disk |
|
151 |
+ if err = ioutil.WriteFile(fp, meta, f.perms); err != nil { |
|
152 |
+ return err |
|
153 |
+ } |
|
154 |
+ return nil |
|
155 |
+} |
|
156 |
+ |
|
157 |
+// RemoveAll clears the existing filestore by removing its base directory |
|
158 |
+func (f *FilesystemStore) RemoveAll() error { |
|
159 |
+ return os.RemoveAll(f.baseDir) |
|
160 |
+} |
|
161 |
+ |
|
162 |
+// Remove removes the metadata for a single role - if the metadata doesn't |
|
163 |
+// exist, no error is returned |
|
164 |
+func (f *FilesystemStore) Remove(name string) error { |
|
165 |
+ p, err := f.getPath(name) |
|
166 |
+ if err != nil { |
|
167 |
+ return err |
|
168 |
+ } |
|
169 |
+ return os.RemoveAll(p) // RemoveAll succeeds if path doesn't exist |
|
170 |
+} |
|
171 |
+ |
|
172 |
+// Location returns a human readable name for the storage location |
|
173 |
+func (f FilesystemStore) Location() string { |
|
174 |
+ return f.baseDir |
|
175 |
+} |
|
176 |
+ |
|
177 |
+// ListFiles returns a list of all the filenames that can be used with Get* |
|
178 |
+// to retrieve content from this filestore |
|
179 |
+func (f FilesystemStore) ListFiles() []string { |
|
180 |
+ files := make([]string, 0, 0) |
|
181 |
+ filepath.Walk(f.baseDir, func(fp string, fi os.FileInfo, err error) error { |
|
182 |
+ // If there are errors, ignore this particular file |
|
183 |
+ if err != nil { |
|
184 |
+ return nil |
|
185 |
+ } |
|
186 |
+ // Ignore if it is a directory |
|
187 |
+ if fi.IsDir() { |
|
188 |
+ return nil |
|
189 |
+ } |
|
190 |
+ |
|
191 |
+ // If this is a symlink, ignore it |
|
192 |
+ if fi.Mode()&os.ModeSymlink == os.ModeSymlink { |
|
193 |
+ return nil |
|
194 |
+ } |
|
195 |
+ |
|
196 |
+ // Only allow matches that end with our certificate extension (e.g. *.crt) |
|
197 |
+ matched, _ := filepath.Match("*"+f.ext, fi.Name()) |
|
198 |
+ |
|
199 |
+ if matched { |
|
200 |
+ // Find the relative path for this file relative to the base path. |
|
201 |
+ fp, err = filepath.Rel(f.baseDir, fp) |
|
202 |
+ if err != nil { |
|
203 |
+ return err |
|
204 |
+ } |
|
205 |
+ trimmed := strings.TrimSuffix(fp, f.ext) |
|
206 |
+ files = append(files, trimmed) |
|
207 |
+ } |
|
208 |
+ return nil |
|
209 |
+ }) |
|
210 |
+ return files |
|
211 |
+} |
|
212 |
+ |
|
213 |
+// createDirectory receives a string of the path to a directory. |
|
214 |
+// It does not support passing files, so the caller has to remove |
|
215 |
+// the filename by doing filepath.Dir(full_path_to_file) |
|
216 |
+func createDirectory(dir string, perms os.FileMode) error { |
|
217 |
+ // This prevents someone passing /path/to/dir and 'dir' not being created |
|
218 |
+ // If two '//' exist, MkdirAll deals it with correctly |
|
219 |
+ dir = dir + "/" |
|
220 |
+ return os.MkdirAll(dir, perms) |
|
221 |
+} |
0 | 222 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,339 @@ |
0 |
+// A Store that can fetch and set metadata on a remote server. |
|
1 |
+// Some API constraints: |
|
2 |
+// - Response bodies for error codes should be unmarshallable as: |
|
3 |
+// {"errors": [{..., "detail": <serialized validation error>}]} |
|
4 |
+// else validation error details, etc. will be unparsable. The errors |
|
5 |
+// should have a github.com/docker/notary/tuf/validation/SerializableError |
|
6 |
+// in the Details field. |
|
7 |
+// If writing your own server, please have a look at |
|
8 |
+// github.com/docker/distribution/registry/api/errcode |
|
9 |
+ |
|
10 |
+package storage |
|
11 |
+ |
|
12 |
+import ( |
|
13 |
+ "bytes" |
|
14 |
+ "encoding/json" |
|
15 |
+ "errors" |
|
16 |
+ "fmt" |
|
17 |
+ "io" |
|
18 |
+ "io/ioutil" |
|
19 |
+ "mime/multipart" |
|
20 |
+ "net/http" |
|
21 |
+ "net/url" |
|
22 |
+ "path" |
|
23 |
+ |
|
24 |
+ "github.com/Sirupsen/logrus" |
|
25 |
+ "github.com/docker/notary" |
|
26 |
+ "github.com/docker/notary/tuf/validation" |
|
27 |
+) |
|
28 |
+ |
|
29 |
+// ErrServerUnavailable indicates an error from the server. code allows us to |
|
30 |
+// populate the http error we received |
|
31 |
+type ErrServerUnavailable struct { |
|
32 |
+ code int |
|
33 |
+} |
|
34 |
+ |
|
35 |
+// NetworkError represents any kind of network error when attempting to make a request |
|
36 |
+type NetworkError struct { |
|
37 |
+ Wrapped error |
|
38 |
+} |
|
39 |
+ |
|
40 |
+func (n NetworkError) Error() string { |
|
41 |
+ return n.Wrapped.Error() |
|
42 |
+} |
|
43 |
+ |
|
44 |
+func (err ErrServerUnavailable) Error() string { |
|
45 |
+ if err.code == 401 { |
|
46 |
+ return fmt.Sprintf("you are not authorized to perform this operation: server returned 401.") |
|
47 |
+ } |
|
48 |
+ return fmt.Sprintf("unable to reach trust server at this time: %d.", err.code) |
|
49 |
+} |
|
50 |
+ |
|
51 |
+// ErrMaliciousServer indicates the server returned a response that is highly suspected |
|
52 |
+// of being malicious. i.e. it attempted to send us more data than the known size of a |
|
53 |
+// particular role metadata. |
|
54 |
+type ErrMaliciousServer struct{} |
|
55 |
+ |
|
56 |
+func (err ErrMaliciousServer) Error() string { |
|
57 |
+ return "trust server returned a bad response." |
|
58 |
+} |
|
59 |
+ |
|
60 |
+// ErrInvalidOperation indicates that the server returned a 400 response and |
|
61 |
+// propagate any body we received. |
|
62 |
+type ErrInvalidOperation struct { |
|
63 |
+ msg string |
|
64 |
+} |
|
65 |
+ |
|
66 |
+func (err ErrInvalidOperation) Error() string { |
|
67 |
+ if err.msg != "" { |
|
68 |
+ return fmt.Sprintf("trust server rejected operation: %s", err.msg) |
|
69 |
+ } |
|
70 |
+ return "trust server rejected operation." |
|
71 |
+} |
|
72 |
+ |
|
73 |
+// HTTPStore manages pulling and pushing metadata from and to a remote |
|
74 |
+// service over HTTP. It assumes the URL structure of the remote service |
|
75 |
+// maps identically to the structure of the TUF repo: |
|
76 |
+// <baseURL>/<metaPrefix>/(root|targets|snapshot|timestamp).json |
|
77 |
+// <baseURL>/<targetsPrefix>/foo.sh |
|
78 |
+// |
|
79 |
+// If consistent snapshots are disabled, it is advised that caching is not |
|
80 |
+// enabled. Simple set a cachePath (and ensure it's writeable) to enable |
|
81 |
+// caching. |
|
82 |
+type HTTPStore struct { |
|
83 |
+ baseURL url.URL |
|
84 |
+ metaPrefix string |
|
85 |
+ metaExtension string |
|
86 |
+ keyExtension string |
|
87 |
+ roundTrip http.RoundTripper |
|
88 |
+} |
|
89 |
+ |
|
90 |
+// NewHTTPStore initializes a new store against a URL and a number of configuration options |
|
91 |
+func NewHTTPStore(baseURL, metaPrefix, metaExtension, keyExtension string, roundTrip http.RoundTripper) (RemoteStore, error) { |
|
92 |
+ base, err := url.Parse(baseURL) |
|
93 |
+ if err != nil { |
|
94 |
+ return nil, err |
|
95 |
+ } |
|
96 |
+ if !base.IsAbs() { |
|
97 |
+ return nil, errors.New("HTTPStore requires an absolute baseURL") |
|
98 |
+ } |
|
99 |
+ if roundTrip == nil { |
|
100 |
+ return &OfflineStore{}, nil |
|
101 |
+ } |
|
102 |
+ return &HTTPStore{ |
|
103 |
+ baseURL: *base, |
|
104 |
+ metaPrefix: metaPrefix, |
|
105 |
+ metaExtension: metaExtension, |
|
106 |
+ keyExtension: keyExtension, |
|
107 |
+ roundTrip: roundTrip, |
|
108 |
+ }, nil |
|
109 |
+} |
|
110 |
+ |
|
111 |
+func tryUnmarshalError(resp *http.Response, defaultError error) error { |
|
112 |
+ bodyBytes, err := ioutil.ReadAll(resp.Body) |
|
113 |
+ if err != nil { |
|
114 |
+ return defaultError |
|
115 |
+ } |
|
116 |
+ var parsedErrors struct { |
|
117 |
+ Errors []struct { |
|
118 |
+ Detail validation.SerializableError `json:"detail"` |
|
119 |
+ } `json:"errors"` |
|
120 |
+ } |
|
121 |
+ if err := json.Unmarshal(bodyBytes, &parsedErrors); err != nil { |
|
122 |
+ return defaultError |
|
123 |
+ } |
|
124 |
+ if len(parsedErrors.Errors) != 1 { |
|
125 |
+ return defaultError |
|
126 |
+ } |
|
127 |
+ err = parsedErrors.Errors[0].Detail.Error |
|
128 |
+ if err == nil { |
|
129 |
+ return defaultError |
|
130 |
+ } |
|
131 |
+ return err |
|
132 |
+} |
|
133 |
+ |
|
134 |
+func translateStatusToError(resp *http.Response, resource string) error { |
|
135 |
+ switch resp.StatusCode { |
|
136 |
+ case http.StatusOK: |
|
137 |
+ return nil |
|
138 |
+ case http.StatusNotFound: |
|
139 |
+ return ErrMetaNotFound{Resource: resource} |
|
140 |
+ case http.StatusBadRequest: |
|
141 |
+ return tryUnmarshalError(resp, ErrInvalidOperation{}) |
|
142 |
+ default: |
|
143 |
+ return ErrServerUnavailable{code: resp.StatusCode} |
|
144 |
+ } |
|
145 |
+} |
|
146 |
+ |
|
147 |
+// GetSized downloads the named meta file with the given size. A short body |
|
148 |
+// is acceptable because in the case of timestamp.json, the size is a cap, |
|
149 |
+// not an exact length. |
|
150 |
+// If size is "NoSizeLimit", this corresponds to "infinite," but we cut off at a |
|
151 |
+// predefined threshold "notary.MaxDownloadSize". |
|
152 |
+func (s HTTPStore) GetSized(name string, size int64) ([]byte, error) { |
|
153 |
+ url, err := s.buildMetaURL(name) |
|
154 |
+ if err != nil { |
|
155 |
+ return nil, err |
|
156 |
+ } |
|
157 |
+ req, err := http.NewRequest("GET", url.String(), nil) |
|
158 |
+ if err != nil { |
|
159 |
+ return nil, err |
|
160 |
+ } |
|
161 |
+ resp, err := s.roundTrip.RoundTrip(req) |
|
162 |
+ if err != nil { |
|
163 |
+ return nil, NetworkError{Wrapped: err} |
|
164 |
+ } |
|
165 |
+ defer resp.Body.Close() |
|
166 |
+ if err := translateStatusToError(resp, name); err != nil { |
|
167 |
+ logrus.Debugf("received HTTP status %d when requesting %s.", resp.StatusCode, name) |
|
168 |
+ return nil, err |
|
169 |
+ } |
|
170 |
+ if size == NoSizeLimit { |
|
171 |
+ size = notary.MaxDownloadSize |
|
172 |
+ } |
|
173 |
+ if resp.ContentLength > size { |
|
174 |
+ return nil, ErrMaliciousServer{} |
|
175 |
+ } |
|
176 |
+ logrus.Debugf("%d when retrieving metadata for %s", resp.StatusCode, name) |
|
177 |
+ b := io.LimitReader(resp.Body, size) |
|
178 |
+ body, err := ioutil.ReadAll(b) |
|
179 |
+ if err != nil { |
|
180 |
+ return nil, err |
|
181 |
+ } |
|
182 |
+ return body, nil |
|
183 |
+} |
|
184 |
+ |
|
185 |
+// Set sends a single piece of metadata to the TUF server |
|
186 |
+func (s HTTPStore) Set(name string, blob []byte) error { |
|
187 |
+ return s.SetMulti(map[string][]byte{name: blob}) |
|
188 |
+} |
|
189 |
+ |
|
190 |
+// Remove always fails, because we should never be able to delete metadata |
|
191 |
+// remotely |
|
192 |
+func (s HTTPStore) Remove(name string) error { |
|
193 |
+ return ErrInvalidOperation{msg: "cannot delete individual metadata files"} |
|
194 |
+} |
|
195 |
+ |
|
196 |
+// NewMultiPartMetaRequest builds a request with the provided metadata updates |
|
197 |
+// in multipart form |
|
198 |
+func NewMultiPartMetaRequest(url string, metas map[string][]byte) (*http.Request, error) { |
|
199 |
+ body := &bytes.Buffer{} |
|
200 |
+ writer := multipart.NewWriter(body) |
|
201 |
+ for role, blob := range metas { |
|
202 |
+ part, err := writer.CreateFormFile("files", role) |
|
203 |
+ if err != nil { |
|
204 |
+ return nil, err |
|
205 |
+ } |
|
206 |
+ _, err = io.Copy(part, bytes.NewBuffer(blob)) |
|
207 |
+ if err != nil { |
|
208 |
+ return nil, err |
|
209 |
+ } |
|
210 |
+ } |
|
211 |
+ err := writer.Close() |
|
212 |
+ if err != nil { |
|
213 |
+ return nil, err |
|
214 |
+ } |
|
215 |
+ req, err := http.NewRequest("POST", url, body) |
|
216 |
+ if err != nil { |
|
217 |
+ return nil, err |
|
218 |
+ } |
|
219 |
+ req.Header.Set("Content-Type", writer.FormDataContentType()) |
|
220 |
+ return req, nil |
|
221 |
+} |
|
222 |
+ |
|
223 |
+// SetMulti does a single batch upload of multiple pieces of TUF metadata. |
|
224 |
+// This should be preferred for updating a remote server as it enable the server |
|
225 |
+// to remain consistent, either accepting or rejecting the complete update. |
|
226 |
+func (s HTTPStore) SetMulti(metas map[string][]byte) error { |
|
227 |
+ url, err := s.buildMetaURL("") |
|
228 |
+ if err != nil { |
|
229 |
+ return err |
|
230 |
+ } |
|
231 |
+ req, err := NewMultiPartMetaRequest(url.String(), metas) |
|
232 |
+ if err != nil { |
|
233 |
+ return err |
|
234 |
+ } |
|
235 |
+ resp, err := s.roundTrip.RoundTrip(req) |
|
236 |
+ if err != nil { |
|
237 |
+ return NetworkError{Wrapped: err} |
|
238 |
+ } |
|
239 |
+ defer resp.Body.Close() |
|
240 |
+ // if this 404's something is pretty wrong |
|
241 |
+ return translateStatusToError(resp, "POST metadata endpoint") |
|
242 |
+} |
|
243 |
+ |
|
244 |
+// RemoveAll will attempt to delete all TUF metadata for a GUN |
|
245 |
+func (s HTTPStore) RemoveAll() error { |
|
246 |
+ url, err := s.buildMetaURL("") |
|
247 |
+ if err != nil { |
|
248 |
+ return err |
|
249 |
+ } |
|
250 |
+ req, err := http.NewRequest("DELETE", url.String(), nil) |
|
251 |
+ if err != nil { |
|
252 |
+ return err |
|
253 |
+ } |
|
254 |
+ resp, err := s.roundTrip.RoundTrip(req) |
|
255 |
+ if err != nil { |
|
256 |
+ return NetworkError{Wrapped: err} |
|
257 |
+ } |
|
258 |
+ defer resp.Body.Close() |
|
259 |
+ return translateStatusToError(resp, "DELETE metadata for GUN endpoint") |
|
260 |
+} |
|
261 |
+ |
|
262 |
+func (s HTTPStore) buildMetaURL(name string) (*url.URL, error) { |
|
263 |
+ var filename string |
|
264 |
+ if name != "" { |
|
265 |
+ filename = fmt.Sprintf("%s.%s", name, s.metaExtension) |
|
266 |
+ } |
|
267 |
+ uri := path.Join(s.metaPrefix, filename) |
|
268 |
+ return s.buildURL(uri) |
|
269 |
+} |
|
270 |
+ |
|
271 |
+func (s HTTPStore) buildKeyURL(name string) (*url.URL, error) { |
|
272 |
+ filename := fmt.Sprintf("%s.%s", name, s.keyExtension) |
|
273 |
+ uri := path.Join(s.metaPrefix, filename) |
|
274 |
+ return s.buildURL(uri) |
|
275 |
+} |
|
276 |
+ |
|
277 |
+func (s HTTPStore) buildURL(uri string) (*url.URL, error) { |
|
278 |
+ sub, err := url.Parse(uri) |
|
279 |
+ if err != nil { |
|
280 |
+ return nil, err |
|
281 |
+ } |
|
282 |
+ return s.baseURL.ResolveReference(sub), nil |
|
283 |
+} |
|
284 |
+ |
|
285 |
+// GetKey retrieves a public key from the remote server |
|
286 |
+func (s HTTPStore) GetKey(role string) ([]byte, error) { |
|
287 |
+ url, err := s.buildKeyURL(role) |
|
288 |
+ if err != nil { |
|
289 |
+ return nil, err |
|
290 |
+ } |
|
291 |
+ req, err := http.NewRequest("GET", url.String(), nil) |
|
292 |
+ if err != nil { |
|
293 |
+ return nil, err |
|
294 |
+ } |
|
295 |
+ resp, err := s.roundTrip.RoundTrip(req) |
|
296 |
+ if err != nil { |
|
297 |
+ return nil, NetworkError{Wrapped: err} |
|
298 |
+ } |
|
299 |
+ defer resp.Body.Close() |
|
300 |
+ if err := translateStatusToError(resp, role+" key"); err != nil { |
|
301 |
+ return nil, err |
|
302 |
+ } |
|
303 |
+ body, err := ioutil.ReadAll(resp.Body) |
|
304 |
+ if err != nil { |
|
305 |
+ return nil, err |
|
306 |
+ } |
|
307 |
+ return body, nil |
|
308 |
+} |
|
309 |
+ |
|
310 |
+// RotateKey rotates a private key and returns the public component from the remote server |
|
311 |
+func (s HTTPStore) RotateKey(role string) ([]byte, error) { |
|
312 |
+ url, err := s.buildKeyURL(role) |
|
313 |
+ if err != nil { |
|
314 |
+ return nil, err |
|
315 |
+ } |
|
316 |
+ req, err := http.NewRequest("POST", url.String(), nil) |
|
317 |
+ if err != nil { |
|
318 |
+ return nil, err |
|
319 |
+ } |
|
320 |
+ resp, err := s.roundTrip.RoundTrip(req) |
|
321 |
+ if err != nil { |
|
322 |
+ return nil, NetworkError{Wrapped: err} |
|
323 |
+ } |
|
324 |
+ defer resp.Body.Close() |
|
325 |
+ if err := translateStatusToError(resp, role+" key"); err != nil { |
|
326 |
+ return nil, err |
|
327 |
+ } |
|
328 |
+ body, err := ioutil.ReadAll(resp.Body) |
|
329 |
+ if err != nil { |
|
330 |
+ return nil, err |
|
331 |
+ } |
|
332 |
+ return body, nil |
|
333 |
+} |
|
334 |
+ |
|
335 |
+// Location returns a human readable name for the storage location |
|
336 |
+func (s HTTPStore) Location() string { |
|
337 |
+ return s.baseURL.String() |
|
338 |
+} |
0 | 339 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,34 @@ |
0 |
+package storage |
|
1 |
+ |
|
2 |
+// NoSizeLimit is represented as -1 for arguments to GetMeta |
|
3 |
+const NoSizeLimit int64 = -1 |
|
4 |
+ |
|
5 |
+// MetadataStore must be implemented by anything that intends to interact |
|
6 |
+// with a store of TUF files |
|
7 |
+type MetadataStore interface { |
|
8 |
+ GetSized(name string, size int64) ([]byte, error) |
|
9 |
+ Set(name string, blob []byte) error |
|
10 |
+ SetMulti(map[string][]byte) error |
|
11 |
+ RemoveAll() error |
|
12 |
+ Remove(name string) error |
|
13 |
+} |
|
14 |
+ |
|
15 |
+// PublicKeyStore must be implemented by a key service |
|
16 |
+type PublicKeyStore interface { |
|
17 |
+ GetKey(role string) ([]byte, error) |
|
18 |
+ RotateKey(role string) ([]byte, error) |
|
19 |
+} |
|
20 |
+ |
|
21 |
+// RemoteStore is similar to LocalStore with the added expectation that it should |
|
22 |
+// provide a way to download targets once located |
|
23 |
+type RemoteStore interface { |
|
24 |
+ MetadataStore |
|
25 |
+ PublicKeyStore |
|
26 |
+} |
|
27 |
+ |
|
28 |
+// Bootstrapper is a thing that can set itself up |
|
29 |
+type Bootstrapper interface { |
|
30 |
+ // Bootstrap instructs a configured Bootstrapper to perform |
|
31 |
+ // its setup operations. |
|
32 |
+ Bootstrap() error |
|
33 |
+} |
0 | 34 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,124 @@ |
0 |
+package storage |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "crypto/sha256" |
|
4 |
+ |
|
5 |
+ "github.com/docker/notary" |
|
6 |
+ "github.com/docker/notary/tuf/utils" |
|
7 |
+) |
|
8 |
+ |
|
9 |
+// NewMemoryStore returns a MetadataStore that operates entirely in memory. |
|
10 |
+// Very useful for testing |
|
11 |
+func NewMemoryStore(initial map[string][]byte) *MemoryStore { |
|
12 |
+ var consistent = make(map[string][]byte) |
|
13 |
+ if initial == nil { |
|
14 |
+ initial = make(map[string][]byte) |
|
15 |
+ } else { |
|
16 |
+ // add all seed meta to consistent |
|
17 |
+ for name, data := range initial { |
|
18 |
+ checksum := sha256.Sum256(data) |
|
19 |
+ path := utils.ConsistentName(name, checksum[:]) |
|
20 |
+ consistent[path] = data |
|
21 |
+ } |
|
22 |
+ } |
|
23 |
+ return &MemoryStore{ |
|
24 |
+ data: initial, |
|
25 |
+ consistent: consistent, |
|
26 |
+ } |
|
27 |
+} |
|
28 |
+ |
|
29 |
+// MemoryStore implements a mock RemoteStore entirely in memory. |
|
30 |
+// For testing purposes only. |
|
31 |
+type MemoryStore struct { |
|
32 |
+ data map[string][]byte |
|
33 |
+ consistent map[string][]byte |
|
34 |
+} |
|
35 |
+ |
|
36 |
+// GetSized returns up to size bytes of data references by name. |
|
37 |
+// If size is "NoSizeLimit", this corresponds to "infinite," but we cut off at a |
|
38 |
+// predefined threshold "notary.MaxDownloadSize", as we will always know the |
|
39 |
+// size for everything but a timestamp and sometimes a root, |
|
40 |
+// neither of which should be exceptionally large |
|
41 |
+func (m MemoryStore) GetSized(name string, size int64) ([]byte, error) { |
|
42 |
+ d, ok := m.data[name] |
|
43 |
+ if ok { |
|
44 |
+ if size == NoSizeLimit { |
|
45 |
+ size = notary.MaxDownloadSize |
|
46 |
+ } |
|
47 |
+ if int64(len(d)) < size { |
|
48 |
+ return d, nil |
|
49 |
+ } |
|
50 |
+ return d[:size], nil |
|
51 |
+ } |
|
52 |
+ d, ok = m.consistent[name] |
|
53 |
+ if ok { |
|
54 |
+ if int64(len(d)) < size { |
|
55 |
+ return d, nil |
|
56 |
+ } |
|
57 |
+ return d[:size], nil |
|
58 |
+ } |
|
59 |
+ return nil, ErrMetaNotFound{Resource: name} |
|
60 |
+} |
|
61 |
+ |
|
62 |
+// Get returns the data associated with name |
|
63 |
+func (m MemoryStore) Get(name string) ([]byte, error) { |
|
64 |
+ if d, ok := m.data[name]; ok { |
|
65 |
+ return d, nil |
|
66 |
+ } |
|
67 |
+ if d, ok := m.consistent[name]; ok { |
|
68 |
+ return d, nil |
|
69 |
+ } |
|
70 |
+ return nil, ErrMetaNotFound{Resource: name} |
|
71 |
+} |
|
72 |
+ |
|
73 |
+// Set sets the metadata value for the given name |
|
74 |
+func (m *MemoryStore) Set(name string, meta []byte) error { |
|
75 |
+ m.data[name] = meta |
|
76 |
+ |
|
77 |
+ checksum := sha256.Sum256(meta) |
|
78 |
+ path := utils.ConsistentName(name, checksum[:]) |
|
79 |
+ m.consistent[path] = meta |
|
80 |
+ return nil |
|
81 |
+} |
|
82 |
+ |
|
83 |
+// SetMulti sets multiple pieces of metadata for multiple names |
|
84 |
+// in a single operation. |
|
85 |
+func (m *MemoryStore) SetMulti(metas map[string][]byte) error { |
|
86 |
+ for role, blob := range metas { |
|
87 |
+ m.Set(role, blob) |
|
88 |
+ } |
|
89 |
+ return nil |
|
90 |
+} |
|
91 |
+ |
|
92 |
+// Remove removes the metadata for a single role - if the metadata doesn't |
|
93 |
+// exist, no error is returned |
|
94 |
+func (m *MemoryStore) Remove(name string) error { |
|
95 |
+ if meta, ok := m.data[name]; ok { |
|
96 |
+ checksum := sha256.Sum256(meta) |
|
97 |
+ path := utils.ConsistentName(name, checksum[:]) |
|
98 |
+ delete(m.data, name) |
|
99 |
+ delete(m.consistent, path) |
|
100 |
+ } |
|
101 |
+ return nil |
|
102 |
+} |
|
103 |
+ |
|
104 |
+// RemoveAll clears the existing memory store by setting this store as new empty one |
|
105 |
+func (m *MemoryStore) RemoveAll() error { |
|
106 |
+ *m = *NewMemoryStore(nil) |
|
107 |
+ return nil |
|
108 |
+} |
|
109 |
+ |
|
110 |
+// Location provides a human readable name for the storage location |
|
111 |
+func (m MemoryStore) Location() string { |
|
112 |
+ return "memory" |
|
113 |
+} |
|
114 |
+ |
|
115 |
+// ListFiles returns a list of all files. The names returned should be |
|
116 |
+// usable with Get directly, with no modification. |
|
117 |
+func (m *MemoryStore) ListFiles() []string { |
|
118 |
+ names := make([]string, 0, len(m.data)) |
|
119 |
+ for n := range m.data { |
|
120 |
+ names = append(names, n) |
|
121 |
+ } |
|
122 |
+ return names |
|
123 |
+} |
0 | 124 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,54 @@ |
0 |
+package storage |
|
1 |
+ |
|
2 |
+// ErrOffline is used to indicate we are operating offline |
|
3 |
+type ErrOffline struct{} |
|
4 |
+ |
|
5 |
+func (e ErrOffline) Error() string { |
|
6 |
+ return "client is offline" |
|
7 |
+} |
|
8 |
+ |
|
9 |
+var err = ErrOffline{} |
|
10 |
+ |
|
11 |
+// OfflineStore is to be used as a placeholder for a nil store. It simply |
|
12 |
+// returns ErrOffline for every operation |
|
13 |
+type OfflineStore struct{} |
|
14 |
+ |
|
15 |
+// GetSized returns ErrOffline |
|
16 |
+func (es OfflineStore) GetSized(name string, size int64) ([]byte, error) { |
|
17 |
+ return nil, err |
|
18 |
+} |
|
19 |
+ |
|
20 |
+// Set returns ErrOffline |
|
21 |
+func (es OfflineStore) Set(name string, blob []byte) error { |
|
22 |
+ return err |
|
23 |
+} |
|
24 |
+ |
|
25 |
+// SetMulti returns ErrOffline |
|
26 |
+func (es OfflineStore) SetMulti(map[string][]byte) error { |
|
27 |
+ return err |
|
28 |
+} |
|
29 |
+ |
|
30 |
+// Remove returns ErrOffline |
|
31 |
+func (es OfflineStore) Remove(name string) error { |
|
32 |
+ return err |
|
33 |
+} |
|
34 |
+ |
|
35 |
+// GetKey returns ErrOffline |
|
36 |
+func (es OfflineStore) GetKey(role string) ([]byte, error) { |
|
37 |
+ return nil, err |
|
38 |
+} |
|
39 |
+ |
|
40 |
+// RotateKey returns ErrOffline |
|
41 |
+func (es OfflineStore) RotateKey(role string) ([]byte, error) { |
|
42 |
+ return nil, err |
|
43 |
+} |
|
44 |
+ |
|
45 |
+// RemoveAll return ErrOffline |
|
46 |
+func (es OfflineStore) RemoveAll() error { |
|
47 |
+ return err |
|
48 |
+} |
|
49 |
+ |
|
50 |
+// Location returns a human readable name for the storage location |
|
51 |
+func (es OfflineStore) Location() string { |
|
52 |
+ return "offline" |
|
53 |
+} |
0 | 54 |
deleted file mode 100644 |
... | ... |
@@ -1,150 +0,0 @@ |
1 |
-package trustmanager |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "fmt" |
|
5 |
- "io/ioutil" |
|
6 |
- "os" |
|
7 |
- "path/filepath" |
|
8 |
- "strings" |
|
9 |
-) |
|
10 |
- |
|
11 |
-// SimpleFileStore implements FileStore |
|
12 |
-type SimpleFileStore struct { |
|
13 |
- baseDir string |
|
14 |
- fileExt string |
|
15 |
- perms os.FileMode |
|
16 |
-} |
|
17 |
- |
|
18 |
-// NewFileStore creates a fully configurable file store |
|
19 |
-func NewFileStore(baseDir, fileExt string, perms os.FileMode) (*SimpleFileStore, error) { |
|
20 |
- baseDir = filepath.Clean(baseDir) |
|
21 |
- if err := createDirectory(baseDir, perms); err != nil { |
|
22 |
- return nil, err |
|
23 |
- } |
|
24 |
- if !strings.HasPrefix(fileExt, ".") { |
|
25 |
- fileExt = "." + fileExt |
|
26 |
- } |
|
27 |
- |
|
28 |
- return &SimpleFileStore{ |
|
29 |
- baseDir: baseDir, |
|
30 |
- fileExt: fileExt, |
|
31 |
- perms: perms, |
|
32 |
- }, nil |
|
33 |
-} |
|
34 |
- |
|
35 |
-// NewSimpleFileStore is a convenience wrapper to create a world readable, |
|
36 |
-// owner writeable filestore |
|
37 |
-func NewSimpleFileStore(baseDir, fileExt string) (*SimpleFileStore, error) { |
|
38 |
- return NewFileStore(baseDir, fileExt, visible) |
|
39 |
-} |
|
40 |
- |
|
41 |
-// NewPrivateSimpleFileStore is a wrapper to create an owner readable/writeable |
|
42 |
-// _only_ filestore |
|
43 |
-func NewPrivateSimpleFileStore(baseDir, fileExt string) (*SimpleFileStore, error) { |
|
44 |
- return NewFileStore(baseDir, fileExt, private) |
|
45 |
-} |
|
46 |
- |
|
47 |
-// Add writes data to a file with a given name |
|
48 |
-func (f *SimpleFileStore) Add(name string, data []byte) error { |
|
49 |
- filePath, err := f.GetPath(name) |
|
50 |
- if err != nil { |
|
51 |
- return err |
|
52 |
- } |
|
53 |
- createDirectory(filepath.Dir(filePath), f.perms) |
|
54 |
- return ioutil.WriteFile(filePath, data, f.perms) |
|
55 |
-} |
|
56 |
- |
|
57 |
-// Remove removes a file identified by name |
|
58 |
-func (f *SimpleFileStore) Remove(name string) error { |
|
59 |
- // Attempt to remove |
|
60 |
- filePath, err := f.GetPath(name) |
|
61 |
- if err != nil { |
|
62 |
- return err |
|
63 |
- } |
|
64 |
- return os.Remove(filePath) |
|
65 |
-} |
|
66 |
- |
|
67 |
-// Get returns the data given a file name |
|
68 |
-func (f *SimpleFileStore) Get(name string) ([]byte, error) { |
|
69 |
- filePath, err := f.GetPath(name) |
|
70 |
- if err != nil { |
|
71 |
- return nil, err |
|
72 |
- } |
|
73 |
- data, err := ioutil.ReadFile(filePath) |
|
74 |
- if err != nil { |
|
75 |
- return nil, err |
|
76 |
- } |
|
77 |
- |
|
78 |
- return data, nil |
|
79 |
-} |
|
80 |
- |
|
81 |
-// GetPath returns the full final path of a file with a given name |
|
82 |
-func (f *SimpleFileStore) GetPath(name string) (string, error) { |
|
83 |
- fileName := f.genFileName(name) |
|
84 |
- fullPath := filepath.Clean(filepath.Join(f.baseDir, fileName)) |
|
85 |
- |
|
86 |
- if !strings.HasPrefix(fullPath, f.baseDir) { |
|
87 |
- return "", ErrPathOutsideStore |
|
88 |
- } |
|
89 |
- return fullPath, nil |
|
90 |
-} |
|
91 |
- |
|
92 |
-// ListFiles lists all the files inside of a store |
|
93 |
-func (f *SimpleFileStore) ListFiles() []string { |
|
94 |
- return f.list(f.baseDir) |
|
95 |
-} |
|
96 |
- |
|
97 |
-// list lists all the files in a directory given a full path. Ignores symlinks. |
|
98 |
-func (f *SimpleFileStore) list(path string) []string { |
|
99 |
- files := make([]string, 0, 0) |
|
100 |
- filepath.Walk(path, func(fp string, fi os.FileInfo, err error) error { |
|
101 |
- // If there are errors, ignore this particular file |
|
102 |
- if err != nil { |
|
103 |
- return nil |
|
104 |
- } |
|
105 |
- // Ignore if it is a directory |
|
106 |
- if fi.IsDir() { |
|
107 |
- return nil |
|
108 |
- } |
|
109 |
- |
|
110 |
- // If this is a symlink, ignore it |
|
111 |
- if fi.Mode()&os.ModeSymlink == os.ModeSymlink { |
|
112 |
- return nil |
|
113 |
- } |
|
114 |
- |
|
115 |
- // Only allow matches that end with our certificate extension (e.g. *.crt) |
|
116 |
- matched, _ := filepath.Match("*"+f.fileExt, fi.Name()) |
|
117 |
- |
|
118 |
- if matched { |
|
119 |
- // Find the relative path for this file relative to the base path. |
|
120 |
- fp, err = filepath.Rel(path, fp) |
|
121 |
- if err != nil { |
|
122 |
- return err |
|
123 |
- } |
|
124 |
- trimmed := strings.TrimSuffix(fp, f.fileExt) |
|
125 |
- files = append(files, trimmed) |
|
126 |
- } |
|
127 |
- return nil |
|
128 |
- }) |
|
129 |
- return files |
|
130 |
-} |
|
131 |
- |
|
132 |
-// genFileName returns the name using the right extension |
|
133 |
-func (f *SimpleFileStore) genFileName(name string) string { |
|
134 |
- return fmt.Sprintf("%s%s", name, f.fileExt) |
|
135 |
-} |
|
136 |
- |
|
137 |
-// BaseDir returns the base directory of the filestore |
|
138 |
-func (f *SimpleFileStore) BaseDir() string { |
|
139 |
- return f.baseDir |
|
140 |
-} |
|
141 |
- |
|
142 |
-// createDirectory receives a string of the path to a directory. |
|
143 |
-// It does not support passing files, so the caller has to remove |
|
144 |
-// the filename by doing filepath.Dir(full_path_to_file) |
|
145 |
-func createDirectory(dir string, perms os.FileMode) error { |
|
146 |
- // This prevents someone passing /path/to/dir and 'dir' not being created |
|
147 |
- // If two '//' exist, MkdirAll deals it with correctly |
|
148 |
- dir = dir + "/" |
|
149 |
- return os.MkdirAll(dir, perms) |
|
150 |
-} |
151 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,82 @@ |
0 |
+package trustmanager |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "fmt" |
|
4 |
+ |
|
5 |
+ "github.com/docker/notary/tuf/data" |
|
6 |
+) |
|
7 |
+ |
|
8 |
+// Storage implements the bare bones primitives (no hierarchy) |
|
9 |
+type Storage interface { |
|
10 |
+ // Add writes a file to the specified location, returning an error if this |
|
11 |
+ // is not possible (reasons may include permissions errors). The path is cleaned |
|
12 |
+ // before being made absolute against the store's base dir. |
|
13 |
+ Set(fileName string, data []byte) error |
|
14 |
+ |
|
15 |
+ // Remove deletes a file from the store relative to the store's base directory. |
|
16 |
+ // The path is cleaned before being made absolute to ensure no path traversal |
|
17 |
+ // outside the base directory is possible. |
|
18 |
+ Remove(fileName string) error |
|
19 |
+ |
|
20 |
+ // Get returns the file content found at fileName relative to the base directory |
|
21 |
+ // of the file store. The path is cleaned before being made absolute to ensure |
|
22 |
+ // path traversal outside the store is not possible. If the file is not found |
|
23 |
+ // an error to that effect is returned. |
|
24 |
+ Get(fileName string) ([]byte, error) |
|
25 |
+ |
|
26 |
+ // ListFiles returns a list of paths relative to the base directory of the |
|
27 |
+ // filestore. Any of these paths must be retrievable via the |
|
28 |
+ // Storage.Get method. |
|
29 |
+ ListFiles() []string |
|
30 |
+ |
|
31 |
+ // Location returns a human readable name indicating where the implementer |
|
32 |
+ // is storing keys |
|
33 |
+ Location() string |
|
34 |
+} |
|
35 |
+ |
|
36 |
+// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key |
|
37 |
+type ErrAttemptsExceeded struct{} |
|
38 |
+ |
|
39 |
+// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key |
|
40 |
+func (err ErrAttemptsExceeded) Error() string { |
|
41 |
+ return "maximum number of passphrase attempts exceeded" |
|
42 |
+} |
|
43 |
+ |
|
44 |
+// ErrPasswordInvalid is returned when signing fails. It could also mean the signing |
|
45 |
+// key file was corrupted, but we have no way to distinguish. |
|
46 |
+type ErrPasswordInvalid struct{} |
|
47 |
+ |
|
48 |
+// ErrPasswordInvalid is returned when signing fails. It could also mean the signing |
|
49 |
+// key file was corrupted, but we have no way to distinguish. |
|
50 |
+func (err ErrPasswordInvalid) Error() string { |
|
51 |
+ return "password invalid, operation has failed." |
|
52 |
+} |
|
53 |
+ |
|
54 |
+// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key. |
|
55 |
+type ErrKeyNotFound struct { |
|
56 |
+ KeyID string |
|
57 |
+} |
|
58 |
+ |
|
59 |
+// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key. |
|
60 |
+func (err ErrKeyNotFound) Error() string { |
|
61 |
+ return fmt.Sprintf("signing key not found: %s", err.KeyID) |
|
62 |
+} |
|
63 |
+ |
|
64 |
+// KeyStore is a generic interface for private key storage |
|
65 |
+type KeyStore interface { |
|
66 |
+ // AddKey adds a key to the KeyStore, and if the key already exists, |
|
67 |
+ // succeeds. Otherwise, returns an error if it cannot add. |
|
68 |
+ AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error |
|
69 |
+ // Should fail with ErrKeyNotFound if the keystore is operating normally |
|
70 |
+ // and knows that it does not store the requested key. |
|
71 |
+ GetKey(keyID string) (data.PrivateKey, string, error) |
|
72 |
+ GetKeyInfo(keyID string) (KeyInfo, error) |
|
73 |
+ ListKeys() map[string]KeyInfo |
|
74 |
+ RemoveKey(keyID string) error |
|
75 |
+ Name() string |
|
76 |
+} |
|
77 |
+ |
|
78 |
+type cachedKey struct { |
|
79 |
+ alias string |
|
80 |
+ key data.PrivateKey |
|
81 |
+} |
0 | 82 |
deleted file mode 100644 |
... | ... |
@@ -1,497 +0,0 @@ |
1 |
-package trustmanager |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "encoding/pem" |
|
5 |
- "fmt" |
|
6 |
- "path/filepath" |
|
7 |
- "strings" |
|
8 |
- "sync" |
|
9 |
- |
|
10 |
- "github.com/Sirupsen/logrus" |
|
11 |
- "github.com/docker/notary" |
|
12 |
- "github.com/docker/notary/passphrase" |
|
13 |
- "github.com/docker/notary/tuf/data" |
|
14 |
-) |
|
15 |
- |
|
16 |
-type keyInfoMap map[string]KeyInfo |
|
17 |
- |
|
18 |
-// KeyFileStore persists and manages private keys on disk |
|
19 |
-type KeyFileStore struct { |
|
20 |
- sync.Mutex |
|
21 |
- SimpleFileStore |
|
22 |
- passphrase.Retriever |
|
23 |
- cachedKeys map[string]*cachedKey |
|
24 |
- keyInfoMap |
|
25 |
-} |
|
26 |
- |
|
27 |
-// KeyMemoryStore manages private keys in memory |
|
28 |
-type KeyMemoryStore struct { |
|
29 |
- sync.Mutex |
|
30 |
- MemoryFileStore |
|
31 |
- passphrase.Retriever |
|
32 |
- cachedKeys map[string]*cachedKey |
|
33 |
- keyInfoMap |
|
34 |
-} |
|
35 |
- |
|
36 |
-// KeyInfo stores the role, path, and gun for a corresponding private key ID |
|
37 |
-// It is assumed that each private key ID is unique |
|
38 |
-type KeyInfo struct { |
|
39 |
- Gun string |
|
40 |
- Role string |
|
41 |
-} |
|
42 |
- |
|
43 |
-// NewKeyFileStore returns a new KeyFileStore creating a private directory to |
|
44 |
-// hold the keys. |
|
45 |
-func NewKeyFileStore(baseDir string, passphraseRetriever passphrase.Retriever) (*KeyFileStore, error) { |
|
46 |
- baseDir = filepath.Join(baseDir, notary.PrivDir) |
|
47 |
- fileStore, err := NewPrivateSimpleFileStore(baseDir, keyExtension) |
|
48 |
- if err != nil { |
|
49 |
- return nil, err |
|
50 |
- } |
|
51 |
- cachedKeys := make(map[string]*cachedKey) |
|
52 |
- keyInfoMap := make(keyInfoMap) |
|
53 |
- |
|
54 |
- keyStore := &KeyFileStore{SimpleFileStore: *fileStore, |
|
55 |
- Retriever: passphraseRetriever, |
|
56 |
- cachedKeys: cachedKeys, |
|
57 |
- keyInfoMap: keyInfoMap, |
|
58 |
- } |
|
59 |
- |
|
60 |
- // Load this keystore's ID --> gun/role map |
|
61 |
- keyStore.loadKeyInfo() |
|
62 |
- return keyStore, nil |
|
63 |
-} |
|
64 |
- |
|
65 |
-func generateKeyInfoMap(s Storage) map[string]KeyInfo { |
|
66 |
- keyInfoMap := make(map[string]KeyInfo) |
|
67 |
- for _, keyPath := range s.ListFiles() { |
|
68 |
- d, err := s.Get(keyPath) |
|
69 |
- if err != nil { |
|
70 |
- logrus.Error(err) |
|
71 |
- continue |
|
72 |
- } |
|
73 |
- keyID, keyInfo, err := KeyInfoFromPEM(d, keyPath) |
|
74 |
- if err != nil { |
|
75 |
- logrus.Error(err) |
|
76 |
- continue |
|
77 |
- } |
|
78 |
- keyInfoMap[keyID] = keyInfo |
|
79 |
- } |
|
80 |
- return keyInfoMap |
|
81 |
-} |
|
82 |
- |
|
83 |
-// Attempts to infer the keyID, role, and GUN from the specified key path. |
|
84 |
-// Note that non-root roles can only be inferred if this is a legacy style filename: KEYID_ROLE.key |
|
85 |
-func inferKeyInfoFromKeyPath(keyPath string) (string, string, string) { |
|
86 |
- var keyID, role, gun string |
|
87 |
- keyID = filepath.Base(keyPath) |
|
88 |
- underscoreIndex := strings.LastIndex(keyID, "_") |
|
89 |
- |
|
90 |
- // This is the legacy KEYID_ROLE filename |
|
91 |
- // The keyID is the first part of the keyname |
|
92 |
- // The keyRole is the second part of the keyname |
|
93 |
- // in a key named abcde_root, abcde is the keyID and root is the KeyAlias |
|
94 |
- if underscoreIndex != -1 { |
|
95 |
- role = keyID[underscoreIndex+1:] |
|
96 |
- keyID = keyID[:underscoreIndex] |
|
97 |
- } |
|
98 |
- |
|
99 |
- if filepath.HasPrefix(keyPath, notary.RootKeysSubdir+"/") { |
|
100 |
- return keyID, data.CanonicalRootRole, "" |
|
101 |
- } |
|
102 |
- |
|
103 |
- keyPath = strings.TrimPrefix(keyPath, notary.NonRootKeysSubdir+"/") |
|
104 |
- gun = getGunFromFullID(keyPath) |
|
105 |
- return keyID, role, gun |
|
106 |
-} |
|
107 |
- |
|
108 |
-func getGunFromFullID(fullKeyID string) string { |
|
109 |
- keyGun := filepath.Dir(fullKeyID) |
|
110 |
- // If the gun is empty, Dir will return . |
|
111 |
- if keyGun == "." { |
|
112 |
- keyGun = "" |
|
113 |
- } |
|
114 |
- return keyGun |
|
115 |
-} |
|
116 |
- |
|
117 |
-func (s *KeyFileStore) loadKeyInfo() { |
|
118 |
- s.keyInfoMap = generateKeyInfoMap(s) |
|
119 |
-} |
|
120 |
- |
|
121 |
-func (s *KeyMemoryStore) loadKeyInfo() { |
|
122 |
- s.keyInfoMap = generateKeyInfoMap(s) |
|
123 |
-} |
|
124 |
- |
|
125 |
-// GetKeyInfo returns the corresponding gun and role key info for a keyID |
|
126 |
-func (s *KeyFileStore) GetKeyInfo(keyID string) (KeyInfo, error) { |
|
127 |
- if info, ok := s.keyInfoMap[keyID]; ok { |
|
128 |
- return info, nil |
|
129 |
- } |
|
130 |
- return KeyInfo{}, fmt.Errorf("Could not find info for keyID %s", keyID) |
|
131 |
-} |
|
132 |
- |
|
133 |
-// GetKeyInfo returns the corresponding gun and role key info for a keyID |
|
134 |
-func (s *KeyMemoryStore) GetKeyInfo(keyID string) (KeyInfo, error) { |
|
135 |
- if info, ok := s.keyInfoMap[keyID]; ok { |
|
136 |
- return info, nil |
|
137 |
- } |
|
138 |
- return KeyInfo{}, fmt.Errorf("Could not find info for keyID %s", keyID) |
|
139 |
-} |
|
140 |
- |
|
141 |
-// Name returns a user friendly name for the location this store |
|
142 |
-// keeps its data |
|
143 |
-func (s *KeyFileStore) Name() string { |
|
144 |
- return fmt.Sprintf("file (%s)", s.SimpleFileStore.BaseDir()) |
|
145 |
-} |
|
146 |
- |
|
147 |
-// AddKey stores the contents of a PEM-encoded private key as a PEM block |
|
148 |
-func (s *KeyFileStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error { |
|
149 |
- s.Lock() |
|
150 |
- defer s.Unlock() |
|
151 |
- if keyInfo.Role == data.CanonicalRootRole || data.IsDelegation(keyInfo.Role) || !data.ValidRole(keyInfo.Role) { |
|
152 |
- keyInfo.Gun = "" |
|
153 |
- } |
|
154 |
- err := addKey(s, s.Retriever, s.cachedKeys, filepath.Join(keyInfo.Gun, privKey.ID()), keyInfo.Role, privKey) |
|
155 |
- if err != nil { |
|
156 |
- return err |
|
157 |
- } |
|
158 |
- s.keyInfoMap[privKey.ID()] = keyInfo |
|
159 |
- return nil |
|
160 |
-} |
|
161 |
- |
|
162 |
-// GetKey returns the PrivateKey given a KeyID |
|
163 |
-func (s *KeyFileStore) GetKey(name string) (data.PrivateKey, string, error) { |
|
164 |
- s.Lock() |
|
165 |
- defer s.Unlock() |
|
166 |
- // If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds |
|
167 |
- if keyInfo, ok := s.keyInfoMap[name]; ok { |
|
168 |
- name = filepath.Join(keyInfo.Gun, name) |
|
169 |
- } |
|
170 |
- return getKey(s, s.Retriever, s.cachedKeys, name) |
|
171 |
-} |
|
172 |
- |
|
173 |
-// ListKeys returns a list of unique PublicKeys present on the KeyFileStore, by returning a copy of the keyInfoMap |
|
174 |
-func (s *KeyFileStore) ListKeys() map[string]KeyInfo { |
|
175 |
- return copyKeyInfoMap(s.keyInfoMap) |
|
176 |
-} |
|
177 |
- |
|
178 |
-// RemoveKey removes the key from the keyfilestore |
|
179 |
-func (s *KeyFileStore) RemoveKey(keyID string) error { |
|
180 |
- s.Lock() |
|
181 |
- defer s.Unlock() |
|
182 |
- // If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds |
|
183 |
- if keyInfo, ok := s.keyInfoMap[keyID]; ok { |
|
184 |
- keyID = filepath.Join(keyInfo.Gun, keyID) |
|
185 |
- } |
|
186 |
- err := removeKey(s, s.cachedKeys, keyID) |
|
187 |
- if err != nil { |
|
188 |
- return err |
|
189 |
- } |
|
190 |
- // Remove this key from our keyInfo map if we removed from our filesystem |
|
191 |
- delete(s.keyInfoMap, filepath.Base(keyID)) |
|
192 |
- return nil |
|
193 |
-} |
|
194 |
- |
|
195 |
-// ExportKey exports the encrypted bytes from the keystore |
|
196 |
-func (s *KeyFileStore) ExportKey(keyID string) ([]byte, error) { |
|
197 |
- if keyInfo, ok := s.keyInfoMap[keyID]; ok { |
|
198 |
- keyID = filepath.Join(keyInfo.Gun, keyID) |
|
199 |
- } |
|
200 |
- keyBytes, _, err := getRawKey(s, keyID) |
|
201 |
- if err != nil { |
|
202 |
- return nil, err |
|
203 |
- } |
|
204 |
- return keyBytes, nil |
|
205 |
-} |
|
206 |
- |
|
207 |
-// NewKeyMemoryStore returns a new KeyMemoryStore which holds keys in memory |
|
208 |
-func NewKeyMemoryStore(passphraseRetriever passphrase.Retriever) *KeyMemoryStore { |
|
209 |
- memStore := NewMemoryFileStore() |
|
210 |
- cachedKeys := make(map[string]*cachedKey) |
|
211 |
- |
|
212 |
- keyInfoMap := make(keyInfoMap) |
|
213 |
- |
|
214 |
- keyStore := &KeyMemoryStore{MemoryFileStore: *memStore, |
|
215 |
- Retriever: passphraseRetriever, |
|
216 |
- cachedKeys: cachedKeys, |
|
217 |
- keyInfoMap: keyInfoMap, |
|
218 |
- } |
|
219 |
- |
|
220 |
- // Load this keystore's ID --> gun/role map |
|
221 |
- keyStore.loadKeyInfo() |
|
222 |
- return keyStore |
|
223 |
-} |
|
224 |
- |
|
225 |
-// Name returns a user friendly name for the location this store |
|
226 |
-// keeps its data |
|
227 |
-func (s *KeyMemoryStore) Name() string { |
|
228 |
- return "memory" |
|
229 |
-} |
|
230 |
- |
|
231 |
-// AddKey stores the contents of a PEM-encoded private key as a PEM block |
|
232 |
-func (s *KeyMemoryStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error { |
|
233 |
- s.Lock() |
|
234 |
- defer s.Unlock() |
|
235 |
- if keyInfo.Role == data.CanonicalRootRole || data.IsDelegation(keyInfo.Role) || !data.ValidRole(keyInfo.Role) { |
|
236 |
- keyInfo.Gun = "" |
|
237 |
- } |
|
238 |
- err := addKey(s, s.Retriever, s.cachedKeys, filepath.Join(keyInfo.Gun, privKey.ID()), keyInfo.Role, privKey) |
|
239 |
- if err != nil { |
|
240 |
- return err |
|
241 |
- } |
|
242 |
- s.keyInfoMap[privKey.ID()] = keyInfo |
|
243 |
- return nil |
|
244 |
-} |
|
245 |
- |
|
246 |
-// GetKey returns the PrivateKey given a KeyID |
|
247 |
-func (s *KeyMemoryStore) GetKey(name string) (data.PrivateKey, string, error) { |
|
248 |
- s.Lock() |
|
249 |
- defer s.Unlock() |
|
250 |
- // If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds |
|
251 |
- if keyInfo, ok := s.keyInfoMap[name]; ok { |
|
252 |
- name = filepath.Join(keyInfo.Gun, name) |
|
253 |
- } |
|
254 |
- return getKey(s, s.Retriever, s.cachedKeys, name) |
|
255 |
-} |
|
256 |
- |
|
257 |
-// ListKeys returns a list of unique PublicKeys present on the KeyFileStore, by returning a copy of the keyInfoMap |
|
258 |
-func (s *KeyMemoryStore) ListKeys() map[string]KeyInfo { |
|
259 |
- return copyKeyInfoMap(s.keyInfoMap) |
|
260 |
-} |
|
261 |
- |
|
262 |
-// copyKeyInfoMap returns a deep copy of the passed-in keyInfoMap |
|
263 |
-func copyKeyInfoMap(keyInfoMap map[string]KeyInfo) map[string]KeyInfo { |
|
264 |
- copyMap := make(map[string]KeyInfo) |
|
265 |
- for keyID, keyInfo := range keyInfoMap { |
|
266 |
- copyMap[keyID] = KeyInfo{Role: keyInfo.Role, Gun: keyInfo.Gun} |
|
267 |
- } |
|
268 |
- return copyMap |
|
269 |
-} |
|
270 |
- |
|
271 |
-// RemoveKey removes the key from the keystore |
|
272 |
-func (s *KeyMemoryStore) RemoveKey(keyID string) error { |
|
273 |
- s.Lock() |
|
274 |
- defer s.Unlock() |
|
275 |
- // If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds |
|
276 |
- if keyInfo, ok := s.keyInfoMap[keyID]; ok { |
|
277 |
- keyID = filepath.Join(keyInfo.Gun, keyID) |
|
278 |
- } |
|
279 |
- err := removeKey(s, s.cachedKeys, keyID) |
|
280 |
- if err != nil { |
|
281 |
- return err |
|
282 |
- } |
|
283 |
- // Remove this key from our keyInfo map if we removed from our filesystem |
|
284 |
- delete(s.keyInfoMap, filepath.Base(keyID)) |
|
285 |
- return nil |
|
286 |
-} |
|
287 |
- |
|
288 |
-// ExportKey exports the encrypted bytes from the keystore |
|
289 |
-func (s *KeyMemoryStore) ExportKey(keyID string) ([]byte, error) { |
|
290 |
- keyBytes, _, err := getRawKey(s, keyID) |
|
291 |
- if err != nil { |
|
292 |
- return nil, err |
|
293 |
- } |
|
294 |
- return keyBytes, nil |
|
295 |
-} |
|
296 |
- |
|
297 |
-// KeyInfoFromPEM attempts to get a keyID and KeyInfo from the filename and PEM bytes of a key |
|
298 |
-func KeyInfoFromPEM(pemBytes []byte, filename string) (string, KeyInfo, error) { |
|
299 |
- keyID, role, gun := inferKeyInfoFromKeyPath(filename) |
|
300 |
- if role == "" { |
|
301 |
- block, _ := pem.Decode(pemBytes) |
|
302 |
- if block == nil { |
|
303 |
- return "", KeyInfo{}, fmt.Errorf("could not decode PEM block for key %s", filename) |
|
304 |
- } |
|
305 |
- if keyRole, ok := block.Headers["role"]; ok { |
|
306 |
- role = keyRole |
|
307 |
- } |
|
308 |
- } |
|
309 |
- return keyID, KeyInfo{Gun: gun, Role: role}, nil |
|
310 |
-} |
|
311 |
- |
|
312 |
-func addKey(s Storage, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error { |
|
313 |
- |
|
314 |
- var ( |
|
315 |
- chosenPassphrase string |
|
316 |
- giveup bool |
|
317 |
- err error |
|
318 |
- ) |
|
319 |
- |
|
320 |
- for attempts := 0; ; attempts++ { |
|
321 |
- chosenPassphrase, giveup, err = passphraseRetriever(name, role, true, attempts) |
|
322 |
- if err != nil { |
|
323 |
- continue |
|
324 |
- } |
|
325 |
- if giveup { |
|
326 |
- return ErrAttemptsExceeded{} |
|
327 |
- } |
|
328 |
- if attempts > 10 { |
|
329 |
- return ErrAttemptsExceeded{} |
|
330 |
- } |
|
331 |
- break |
|
332 |
- } |
|
333 |
- |
|
334 |
- return encryptAndAddKey(s, chosenPassphrase, cachedKeys, name, role, privKey) |
|
335 |
-} |
|
336 |
- |
|
337 |
-// getKeyRole finds the role for the given keyID. It attempts to look |
|
338 |
-// both in the newer format PEM headers, and also in the legacy filename |
|
339 |
-// format. It returns: the role, whether it was found in the legacy format |
|
340 |
-// (true == legacy), and an error |
|
341 |
-func getKeyRole(s Storage, keyID string) (string, bool, error) { |
|
342 |
- name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(keyID), filepath.Ext(keyID))) |
|
343 |
- |
|
344 |
- for _, file := range s.ListFiles() { |
|
345 |
- filename := filepath.Base(file) |
|
346 |
- |
|
347 |
- if strings.HasPrefix(filename, name) { |
|
348 |
- d, err := s.Get(file) |
|
349 |
- if err != nil { |
|
350 |
- return "", false, err |
|
351 |
- } |
|
352 |
- block, _ := pem.Decode(d) |
|
353 |
- if block != nil { |
|
354 |
- if role, ok := block.Headers["role"]; ok { |
|
355 |
- return role, false, nil |
|
356 |
- } |
|
357 |
- } |
|
358 |
- |
|
359 |
- role := strings.TrimPrefix(filename, name+"_") |
|
360 |
- return role, true, nil |
|
361 |
- } |
|
362 |
- } |
|
363 |
- |
|
364 |
- return "", false, ErrKeyNotFound{KeyID: keyID} |
|
365 |
-} |
|
366 |
- |
|
367 |
-// GetKey returns the PrivateKey given a KeyID |
|
368 |
-func getKey(s Storage, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name string) (data.PrivateKey, string, error) { |
|
369 |
- cachedKeyEntry, ok := cachedKeys[name] |
|
370 |
- if ok { |
|
371 |
- return cachedKeyEntry.key, cachedKeyEntry.alias, nil |
|
372 |
- } |
|
373 |
- |
|
374 |
- keyBytes, keyAlias, err := getRawKey(s, name) |
|
375 |
- if err != nil { |
|
376 |
- return nil, "", err |
|
377 |
- } |
|
378 |
- |
|
379 |
- // See if the key is encrypted. If its encrypted we'll fail to parse the private key |
|
380 |
- privKey, err := ParsePEMPrivateKey(keyBytes, "") |
|
381 |
- if err != nil { |
|
382 |
- privKey, _, err = GetPasswdDecryptBytes(passphraseRetriever, keyBytes, name, string(keyAlias)) |
|
383 |
- if err != nil { |
|
384 |
- return nil, "", err |
|
385 |
- } |
|
386 |
- } |
|
387 |
- cachedKeys[name] = &cachedKey{alias: keyAlias, key: privKey} |
|
388 |
- return privKey, keyAlias, nil |
|
389 |
-} |
|
390 |
- |
|
391 |
-// RemoveKey removes the key from the keyfilestore |
|
392 |
-func removeKey(s Storage, cachedKeys map[string]*cachedKey, name string) error { |
|
393 |
- role, legacy, err := getKeyRole(s, name) |
|
394 |
- if err != nil { |
|
395 |
- return err |
|
396 |
- } |
|
397 |
- |
|
398 |
- delete(cachedKeys, name) |
|
399 |
- |
|
400 |
- if legacy { |
|
401 |
- name = name + "_" + role |
|
402 |
- } |
|
403 |
- |
|
404 |
- // being in a subdirectory is for backwards compatibliity |
|
405 |
- err = s.Remove(filepath.Join(getSubdir(role), name)) |
|
406 |
- if err != nil { |
|
407 |
- return err |
|
408 |
- } |
|
409 |
- return nil |
|
410 |
-} |
|
411 |
- |
|
412 |
-// Assumes 2 subdirectories, 1 containing root keys and 1 containing tuf keys |
|
413 |
-func getSubdir(alias string) string { |
|
414 |
- if alias == data.CanonicalRootRole { |
|
415 |
- return notary.RootKeysSubdir |
|
416 |
- } |
|
417 |
- return notary.NonRootKeysSubdir |
|
418 |
-} |
|
419 |
- |
|
420 |
-// Given a key ID, gets the bytes and alias belonging to that key if the key |
|
421 |
-// exists |
|
422 |
-func getRawKey(s Storage, name string) ([]byte, string, error) { |
|
423 |
- role, legacy, err := getKeyRole(s, name) |
|
424 |
- if err != nil { |
|
425 |
- return nil, "", err |
|
426 |
- } |
|
427 |
- |
|
428 |
- if legacy { |
|
429 |
- name = name + "_" + role |
|
430 |
- } |
|
431 |
- |
|
432 |
- var keyBytes []byte |
|
433 |
- keyBytes, err = s.Get(filepath.Join(getSubdir(role), name)) |
|
434 |
- if err != nil { |
|
435 |
- return nil, "", err |
|
436 |
- } |
|
437 |
- return keyBytes, role, nil |
|
438 |
-} |
|
439 |
- |
|
440 |
-// GetPasswdDecryptBytes gets the password to decrypt the given pem bytes. |
|
441 |
-// Returns the password and private key |
|
442 |
-func GetPasswdDecryptBytes(passphraseRetriever passphrase.Retriever, pemBytes []byte, name, alias string) (data.PrivateKey, string, error) { |
|
443 |
- var ( |
|
444 |
- passwd string |
|
445 |
- retErr error |
|
446 |
- privKey data.PrivateKey |
|
447 |
- ) |
|
448 |
- for attempts := 0; ; attempts++ { |
|
449 |
- var ( |
|
450 |
- giveup bool |
|
451 |
- err error |
|
452 |
- ) |
|
453 |
- passwd, giveup, err = passphraseRetriever(name, alias, false, attempts) |
|
454 |
- // Check if the passphrase retriever got an error or if it is telling us to give up |
|
455 |
- if giveup || err != nil { |
|
456 |
- return nil, "", ErrPasswordInvalid{} |
|
457 |
- } |
|
458 |
- if attempts > 10 { |
|
459 |
- return nil, "", ErrAttemptsExceeded{} |
|
460 |
- } |
|
461 |
- |
|
462 |
- // Try to convert PEM encoded bytes back to a PrivateKey using the passphrase |
|
463 |
- privKey, err = ParsePEMPrivateKey(pemBytes, passwd) |
|
464 |
- if err != nil { |
|
465 |
- retErr = ErrPasswordInvalid{} |
|
466 |
- } else { |
|
467 |
- // We managed to parse the PrivateKey. We've succeeded! |
|
468 |
- retErr = nil |
|
469 |
- break |
|
470 |
- } |
|
471 |
- } |
|
472 |
- if retErr != nil { |
|
473 |
- return nil, "", retErr |
|
474 |
- } |
|
475 |
- return privKey, passwd, nil |
|
476 |
-} |
|
477 |
- |
|
478 |
-func encryptAndAddKey(s Storage, passwd string, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error { |
|
479 |
- |
|
480 |
- var ( |
|
481 |
- pemPrivKey []byte |
|
482 |
- err error |
|
483 |
- ) |
|
484 |
- |
|
485 |
- if passwd != "" { |
|
486 |
- pemPrivKey, err = EncryptPrivateKey(privKey, role, passwd) |
|
487 |
- } else { |
|
488 |
- pemPrivKey, err = KeyToPEM(privKey, role) |
|
489 |
- } |
|
490 |
- |
|
491 |
- if err != nil { |
|
492 |
- return err |
|
493 |
- } |
|
494 |
- |
|
495 |
- cachedKeys[name] = &cachedKey{alias: role, key: privKey} |
|
496 |
- return s.Add(filepath.Join(getSubdir(role), name), pemPrivKey) |
|
497 |
-} |
... | ... |
@@ -1,59 +1,325 @@ |
1 | 1 |
package trustmanager |
2 | 2 |
|
3 | 3 |
import ( |
4 |
+ "encoding/pem" |
|
4 | 5 |
"fmt" |
6 |
+ "path/filepath" |
|
7 |
+ "strings" |
|
8 |
+ "sync" |
|
5 | 9 |
|
10 |
+ "github.com/Sirupsen/logrus" |
|
11 |
+ "github.com/docker/notary" |
|
12 |
+ store "github.com/docker/notary/storage" |
|
6 | 13 |
"github.com/docker/notary/tuf/data" |
14 |
+ "github.com/docker/notary/tuf/utils" |
|
7 | 15 |
) |
8 | 16 |
|
9 |
-// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key |
|
10 |
-type ErrAttemptsExceeded struct{} |
|
17 |
+type keyInfoMap map[string]KeyInfo |
|
11 | 18 |
|
12 |
-// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key |
|
13 |
-func (err ErrAttemptsExceeded) Error() string { |
|
14 |
- return "maximum number of passphrase attempts exceeded" |
|
19 |
+// KeyInfo stores the role, path, and gun for a corresponding private key ID |
|
20 |
+// It is assumed that each private key ID is unique |
|
21 |
+type KeyInfo struct { |
|
22 |
+ Gun string |
|
23 |
+ Role string |
|
15 | 24 |
} |
16 | 25 |
|
17 |
-// ErrPasswordInvalid is returned when signing fails. It could also mean the signing |
|
18 |
-// key file was corrupted, but we have no way to distinguish. |
|
19 |
-type ErrPasswordInvalid struct{} |
|
26 |
+// GenericKeyStore is a wrapper for Storage instances that provides |
|
27 |
+// translation between the []byte form and Public/PrivateKey objects |
|
28 |
+type GenericKeyStore struct { |
|
29 |
+ store Storage |
|
30 |
+ sync.Mutex |
|
31 |
+ notary.PassRetriever |
|
32 |
+ cachedKeys map[string]*cachedKey |
|
33 |
+ keyInfoMap |
|
34 |
+} |
|
20 | 35 |
|
21 |
-// ErrPasswordInvalid is returned when signing fails. It could also mean the signing |
|
22 |
-// key file was corrupted, but we have no way to distinguish. |
|
23 |
-func (err ErrPasswordInvalid) Error() string { |
|
24 |
- return "password invalid, operation has failed." |
|
36 |
+// NewKeyFileStore returns a new KeyFileStore creating a private directory to |
|
37 |
+// hold the keys. |
|
38 |
+func NewKeyFileStore(baseDir string, p notary.PassRetriever) (*GenericKeyStore, error) { |
|
39 |
+ fileStore, err := store.NewPrivateKeyFileStorage(baseDir, notary.KeyExtension) |
|
40 |
+ if err != nil { |
|
41 |
+ return nil, err |
|
42 |
+ } |
|
43 |
+ return NewGenericKeyStore(fileStore, p), nil |
|
25 | 44 |
} |
26 | 45 |
|
27 |
-// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key. |
|
28 |
-type ErrKeyNotFound struct { |
|
29 |
- KeyID string |
|
46 |
+// NewKeyMemoryStore returns a new KeyMemoryStore which holds keys in memory |
|
47 |
+func NewKeyMemoryStore(p notary.PassRetriever) *GenericKeyStore { |
|
48 |
+ memStore := store.NewMemoryStore(nil) |
|
49 |
+ return NewGenericKeyStore(memStore, p) |
|
30 | 50 |
} |
31 | 51 |
|
32 |
-// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key. |
|
33 |
-func (err ErrKeyNotFound) Error() string { |
|
34 |
- return fmt.Sprintf("signing key not found: %s", err.KeyID) |
|
52 |
+// NewGenericKeyStore creates a GenericKeyStore wrapping the provided |
|
53 |
+// Storage instance, using the PassRetriever to enc/decrypt keys |
|
54 |
+func NewGenericKeyStore(s Storage, p notary.PassRetriever) *GenericKeyStore { |
|
55 |
+ ks := GenericKeyStore{ |
|
56 |
+ store: s, |
|
57 |
+ PassRetriever: p, |
|
58 |
+ cachedKeys: make(map[string]*cachedKey), |
|
59 |
+ keyInfoMap: make(keyInfoMap), |
|
60 |
+ } |
|
61 |
+ ks.loadKeyInfo() |
|
62 |
+ return &ks |
|
35 | 63 |
} |
36 | 64 |
|
37 |
-const ( |
|
38 |
- keyExtension = "key" |
|
39 |
-) |
|
65 |
+func generateKeyInfoMap(s Storage) map[string]KeyInfo { |
|
66 |
+ keyInfoMap := make(map[string]KeyInfo) |
|
67 |
+ for _, keyPath := range s.ListFiles() { |
|
68 |
+ d, err := s.Get(keyPath) |
|
69 |
+ if err != nil { |
|
70 |
+ logrus.Error(err) |
|
71 |
+ continue |
|
72 |
+ } |
|
73 |
+ keyID, keyInfo, err := KeyInfoFromPEM(d, keyPath) |
|
74 |
+ if err != nil { |
|
75 |
+ logrus.Error(err) |
|
76 |
+ continue |
|
77 |
+ } |
|
78 |
+ keyInfoMap[keyID] = keyInfo |
|
79 |
+ } |
|
80 |
+ return keyInfoMap |
|
81 |
+} |
|
82 |
+ |
|
83 |
+// Attempts to infer the keyID, role, and GUN from the specified key path. |
|
84 |
+// Note that non-root roles can only be inferred if this is a legacy style filename: KEYID_ROLE.key |
|
85 |
+func inferKeyInfoFromKeyPath(keyPath string) (string, string, string) { |
|
86 |
+ var keyID, role, gun string |
|
87 |
+ keyID = filepath.Base(keyPath) |
|
88 |
+ underscoreIndex := strings.LastIndex(keyID, "_") |
|
89 |
+ |
|
90 |
+ // This is the legacy KEYID_ROLE filename |
|
91 |
+ // The keyID is the first part of the keyname |
|
92 |
+ // The keyRole is the second part of the keyname |
|
93 |
+ // in a key named abcde_root, abcde is the keyID and root is the KeyAlias |
|
94 |
+ if underscoreIndex != -1 { |
|
95 |
+ role = keyID[underscoreIndex+1:] |
|
96 |
+ keyID = keyID[:underscoreIndex] |
|
97 |
+ } |
|
98 |
+ |
|
99 |
+ if filepath.HasPrefix(keyPath, notary.RootKeysSubdir+"/") { |
|
100 |
+ return keyID, data.CanonicalRootRole, "" |
|
101 |
+ } |
|
102 |
+ |
|
103 |
+ keyPath = strings.TrimPrefix(keyPath, notary.NonRootKeysSubdir+"/") |
|
104 |
+ gun = getGunFromFullID(keyPath) |
|
105 |
+ return keyID, role, gun |
|
106 |
+} |
|
107 |
+ |
|
108 |
+func getGunFromFullID(fullKeyID string) string { |
|
109 |
+ keyGun := filepath.Dir(fullKeyID) |
|
110 |
+ // If the gun is empty, Dir will return . |
|
111 |
+ if keyGun == "." { |
|
112 |
+ keyGun = "" |
|
113 |
+ } |
|
114 |
+ return keyGun |
|
115 |
+} |
|
116 |
+ |
|
117 |
+func (s *GenericKeyStore) loadKeyInfo() { |
|
118 |
+ s.keyInfoMap = generateKeyInfoMap(s.store) |
|
119 |
+} |
|
120 |
+ |
|
121 |
+// GetKeyInfo returns the corresponding gun and role key info for a keyID |
|
122 |
+func (s *GenericKeyStore) GetKeyInfo(keyID string) (KeyInfo, error) { |
|
123 |
+ if info, ok := s.keyInfoMap[keyID]; ok { |
|
124 |
+ return info, nil |
|
125 |
+ } |
|
126 |
+ return KeyInfo{}, fmt.Errorf("Could not find info for keyID %s", keyID) |
|
127 |
+} |
|
128 |
+ |
|
129 |
+// AddKey stores the contents of a PEM-encoded private key as a PEM block |
|
130 |
+func (s *GenericKeyStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error { |
|
131 |
+ var ( |
|
132 |
+ chosenPassphrase string |
|
133 |
+ giveup bool |
|
134 |
+ err error |
|
135 |
+ pemPrivKey []byte |
|
136 |
+ ) |
|
137 |
+ s.Lock() |
|
138 |
+ defer s.Unlock() |
|
139 |
+ if keyInfo.Role == data.CanonicalRootRole || data.IsDelegation(keyInfo.Role) || !data.ValidRole(keyInfo.Role) { |
|
140 |
+ keyInfo.Gun = "" |
|
141 |
+ } |
|
142 |
+ name := filepath.Join(keyInfo.Gun, privKey.ID()) |
|
143 |
+ for attempts := 0; ; attempts++ { |
|
144 |
+ chosenPassphrase, giveup, err = s.PassRetriever(name, keyInfo.Role, true, attempts) |
|
145 |
+ if err == nil { |
|
146 |
+ break |
|
147 |
+ } |
|
148 |
+ if giveup || attempts > 10 { |
|
149 |
+ return ErrAttemptsExceeded{} |
|
150 |
+ } |
|
151 |
+ } |
|
152 |
+ |
|
153 |
+ if chosenPassphrase != "" { |
|
154 |
+ pemPrivKey, err = utils.EncryptPrivateKey(privKey, keyInfo.Role, keyInfo.Gun, chosenPassphrase) |
|
155 |
+ } else { |
|
156 |
+ pemPrivKey, err = utils.KeyToPEM(privKey, keyInfo.Role) |
|
157 |
+ } |
|
158 |
+ |
|
159 |
+ if err != nil { |
|
160 |
+ return err |
|
161 |
+ } |
|
162 |
+ |
|
163 |
+ s.cachedKeys[name] = &cachedKey{alias: keyInfo.Role, key: privKey} |
|
164 |
+ err = s.store.Set(filepath.Join(getSubdir(keyInfo.Role), name), pemPrivKey) |
|
165 |
+ if err != nil { |
|
166 |
+ return err |
|
167 |
+ } |
|
168 |
+ s.keyInfoMap[privKey.ID()] = keyInfo |
|
169 |
+ return nil |
|
170 |
+} |
|
171 |
+ |
|
172 |
+// GetKey returns the PrivateKey given a KeyID |
|
173 |
+func (s *GenericKeyStore) GetKey(name string) (data.PrivateKey, string, error) { |
|
174 |
+ s.Lock() |
|
175 |
+ defer s.Unlock() |
|
176 |
+ cachedKeyEntry, ok := s.cachedKeys[name] |
|
177 |
+ if ok { |
|
178 |
+ return cachedKeyEntry.key, cachedKeyEntry.alias, nil |
|
179 |
+ } |
|
180 |
+ |
|
181 |
+ keyBytes, _, keyAlias, err := getKey(s.store, name) |
|
182 |
+ if err != nil { |
|
183 |
+ return nil, "", err |
|
184 |
+ } |
|
185 |
+ |
|
186 |
+ // See if the key is encrypted. If its encrypted we'll fail to parse the private key |
|
187 |
+ privKey, err := utils.ParsePEMPrivateKey(keyBytes, "") |
|
188 |
+ if err != nil { |
|
189 |
+ privKey, _, err = GetPasswdDecryptBytes(s.PassRetriever, keyBytes, name, string(keyAlias)) |
|
190 |
+ if err != nil { |
|
191 |
+ return nil, "", err |
|
192 |
+ } |
|
193 |
+ } |
|
194 |
+ s.cachedKeys[name] = &cachedKey{alias: keyAlias, key: privKey} |
|
195 |
+ return privKey, keyAlias, nil |
|
196 |
+} |
|
197 |
+ |
|
198 |
+// ListKeys returns a list of unique PublicKeys present on the KeyFileStore, by returning a copy of the keyInfoMap |
|
199 |
+func (s *GenericKeyStore) ListKeys() map[string]KeyInfo { |
|
200 |
+ return copyKeyInfoMap(s.keyInfoMap) |
|
201 |
+} |
|
202 |
+ |
|
203 |
+// RemoveKey removes the key from the keyfilestore |
|
204 |
+func (s *GenericKeyStore) RemoveKey(keyID string) error { |
|
205 |
+ s.Lock() |
|
206 |
+ defer s.Unlock() |
|
207 |
+ |
|
208 |
+ _, filename, _, err := getKey(s.store, keyID) |
|
209 |
+ switch err.(type) { |
|
210 |
+ case ErrKeyNotFound, nil: |
|
211 |
+ break |
|
212 |
+ default: |
|
213 |
+ return err |
|
214 |
+ } |
|
215 |
+ |
|
216 |
+ delete(s.cachedKeys, keyID) |
|
217 |
+ |
|
218 |
+ err = s.store.Remove(filename) // removing a file that doesn't exist doesn't fail |
|
219 |
+ if err != nil { |
|
220 |
+ return err |
|
221 |
+ } |
|
222 |
+ |
|
223 |
+ // Remove this key from our keyInfo map if we removed from our filesystem |
|
224 |
+ delete(s.keyInfoMap, filepath.Base(keyID)) |
|
225 |
+ return nil |
|
226 |
+} |
|
227 |
+ |
|
228 |
+// Name returns a user friendly name for the location this store |
|
229 |
+// keeps its data |
|
230 |
+func (s *GenericKeyStore) Name() string { |
|
231 |
+ return s.store.Location() |
|
232 |
+} |
|
233 |
+ |
|
234 |
+// copyKeyInfoMap returns a deep copy of the passed-in keyInfoMap |
|
235 |
+func copyKeyInfoMap(keyInfoMap map[string]KeyInfo) map[string]KeyInfo { |
|
236 |
+ copyMap := make(map[string]KeyInfo) |
|
237 |
+ for keyID, keyInfo := range keyInfoMap { |
|
238 |
+ copyMap[keyID] = KeyInfo{Role: keyInfo.Role, Gun: keyInfo.Gun} |
|
239 |
+ } |
|
240 |
+ return copyMap |
|
241 |
+} |
|
242 |
+ |
|
243 |
+// KeyInfoFromPEM attempts to get a keyID and KeyInfo from the filename and PEM bytes of a key |
|
244 |
+func KeyInfoFromPEM(pemBytes []byte, filename string) (string, KeyInfo, error) { |
|
245 |
+ keyID, role, gun := inferKeyInfoFromKeyPath(filename) |
|
246 |
+ if role == "" { |
|
247 |
+ block, _ := pem.Decode(pemBytes) |
|
248 |
+ if block == nil { |
|
249 |
+ return "", KeyInfo{}, fmt.Errorf("could not decode PEM block for key %s", filename) |
|
250 |
+ } |
|
251 |
+ if keyRole, ok := block.Headers["role"]; ok { |
|
252 |
+ role = keyRole |
|
253 |
+ } |
|
254 |
+ } |
|
255 |
+ return keyID, KeyInfo{Gun: gun, Role: role}, nil |
|
256 |
+} |
|
257 |
+ |
|
258 |
+// getKey finds the key and role for the given keyID. It attempts to |
|
259 |
+// look both in the newer format PEM headers, and also in the legacy filename |
|
260 |
+// format. It returns: the key bytes, the filename it was found under, the role, |
|
261 |
+// and an error |
|
262 |
+func getKey(s Storage, keyID string) ([]byte, string, string, error) { |
|
263 |
+ name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(keyID), filepath.Ext(keyID))) |
|
264 |
+ |
|
265 |
+ for _, file := range s.ListFiles() { |
|
266 |
+ filename := filepath.Base(file) |
|
267 |
+ |
|
268 |
+ if strings.HasPrefix(filename, name) { |
|
269 |
+ d, err := s.Get(file) |
|
270 |
+ if err != nil { |
|
271 |
+ return nil, "", "", err |
|
272 |
+ } |
|
273 |
+ block, _ := pem.Decode(d) |
|
274 |
+ if block != nil { |
|
275 |
+ if role, ok := block.Headers["role"]; ok { |
|
276 |
+ return d, file, role, nil |
|
277 |
+ } |
|
278 |
+ } |
|
279 |
+ |
|
280 |
+ role := strings.TrimPrefix(filename, name+"_") |
|
281 |
+ return d, file, role, nil |
|
282 |
+ } |
|
283 |
+ } |
|
284 |
+ |
|
285 |
+ return nil, "", "", ErrKeyNotFound{KeyID: keyID} |
|
286 |
+} |
|
287 |
+ |
|
288 |
+// Assumes 2 subdirectories, 1 containing root keys and 1 containing TUF keys |
|
289 |
+func getSubdir(alias string) string { |
|
290 |
+ if alias == data.CanonicalRootRole { |
|
291 |
+ return notary.RootKeysSubdir |
|
292 |
+ } |
|
293 |
+ return notary.NonRootKeysSubdir |
|
294 |
+} |
|
295 |
+ |
|
296 |
+// GetPasswdDecryptBytes gets the password to decrypt the given pem bytes. |
|
297 |
+// Returns the password and private key |
|
298 |
+func GetPasswdDecryptBytes(passphraseRetriever notary.PassRetriever, pemBytes []byte, name, alias string) (data.PrivateKey, string, error) { |
|
299 |
+ var ( |
|
300 |
+ passwd string |
|
301 |
+ privKey data.PrivateKey |
|
302 |
+ ) |
|
303 |
+ for attempts := 0; ; attempts++ { |
|
304 |
+ var ( |
|
305 |
+ giveup bool |
|
306 |
+ err error |
|
307 |
+ ) |
|
308 |
+ if attempts > 10 { |
|
309 |
+ return nil, "", ErrAttemptsExceeded{} |
|
310 |
+ } |
|
311 |
+ passwd, giveup, err = passphraseRetriever(name, alias, false, attempts) |
|
312 |
+ // Check if the passphrase retriever got an error or if it is telling us to give up |
|
313 |
+ if giveup || err != nil { |
|
314 |
+ return nil, "", ErrPasswordInvalid{} |
|
315 |
+ } |
|
40 | 316 |
|
41 |
-// KeyStore is a generic interface for private key storage |
|
42 |
-type KeyStore interface { |
|
43 |
- // AddKey adds a key to the KeyStore, and if the key already exists, |
|
44 |
- // succeeds. Otherwise, returns an error if it cannot add. |
|
45 |
- AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error |
|
46 |
- // Should fail with ErrKeyNotFound if the keystore is operating normally |
|
47 |
- // and knows that it does not store the requested key. |
|
48 |
- GetKey(keyID string) (data.PrivateKey, string, error) |
|
49 |
- GetKeyInfo(keyID string) (KeyInfo, error) |
|
50 |
- ListKeys() map[string]KeyInfo |
|
51 |
- RemoveKey(keyID string) error |
|
52 |
- ExportKey(keyID string) ([]byte, error) |
|
53 |
- Name() string |
|
54 |
-} |
|
55 |
- |
|
56 |
-type cachedKey struct { |
|
57 |
- alias string |
|
58 |
- key data.PrivateKey |
|
317 |
+ // Try to convert PEM encoded bytes back to a PrivateKey using the passphrase |
|
318 |
+ privKey, err = utils.ParsePEMPrivateKey(pemBytes, passwd) |
|
319 |
+ if err == nil { |
|
320 |
+ // We managed to parse the PrivateKey. We've succeeded! |
|
321 |
+ break |
|
322 |
+ } |
|
323 |
+ } |
|
324 |
+ return privKey, passwd, nil |
|
59 | 325 |
} |
60 | 326 |
deleted file mode 100644 |
... | ... |
@@ -1,67 +0,0 @@ |
1 |
-package trustmanager |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "os" |
|
5 |
- "sync" |
|
6 |
-) |
|
7 |
- |
|
8 |
-// MemoryFileStore is an implementation of Storage that keeps |
|
9 |
-// the contents in memory. |
|
10 |
-type MemoryFileStore struct { |
|
11 |
- sync.Mutex |
|
12 |
- |
|
13 |
- files map[string][]byte |
|
14 |
-} |
|
15 |
- |
|
16 |
-// NewMemoryFileStore creates a MemoryFileStore |
|
17 |
-func NewMemoryFileStore() *MemoryFileStore { |
|
18 |
- return &MemoryFileStore{ |
|
19 |
- files: make(map[string][]byte), |
|
20 |
- } |
|
21 |
-} |
|
22 |
- |
|
23 |
-// Add writes data to a file with a given name |
|
24 |
-func (f *MemoryFileStore) Add(name string, data []byte) error { |
|
25 |
- f.Lock() |
|
26 |
- defer f.Unlock() |
|
27 |
- |
|
28 |
- f.files[name] = data |
|
29 |
- return nil |
|
30 |
-} |
|
31 |
- |
|
32 |
-// Remove removes a file identified by name |
|
33 |
-func (f *MemoryFileStore) Remove(name string) error { |
|
34 |
- f.Lock() |
|
35 |
- defer f.Unlock() |
|
36 |
- |
|
37 |
- if _, present := f.files[name]; !present { |
|
38 |
- return os.ErrNotExist |
|
39 |
- } |
|
40 |
- delete(f.files, name) |
|
41 |
- |
|
42 |
- return nil |
|
43 |
-} |
|
44 |
- |
|
45 |
-// Get returns the data given a file name |
|
46 |
-func (f *MemoryFileStore) Get(name string) ([]byte, error) { |
|
47 |
- f.Lock() |
|
48 |
- defer f.Unlock() |
|
49 |
- |
|
50 |
- fileData, present := f.files[name] |
|
51 |
- if !present { |
|
52 |
- return nil, os.ErrNotExist |
|
53 |
- } |
|
54 |
- |
|
55 |
- return fileData, nil |
|
56 |
-} |
|
57 |
- |
|
58 |
-// ListFiles lists all the files inside of a store |
|
59 |
-func (f *MemoryFileStore) ListFiles() []string { |
|
60 |
- var list []string |
|
61 |
- |
|
62 |
- for name := range f.files { |
|
63 |
- list = append(list, name) |
|
64 |
- } |
|
65 |
- |
|
66 |
- return list |
|
67 |
-} |
68 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,42 +0,0 @@ |
1 |
-package trustmanager |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "errors" |
|
5 |
- |
|
6 |
- "github.com/docker/notary" |
|
7 |
-) |
|
8 |
- |
|
9 |
-const ( |
|
10 |
- visible = notary.PubCertPerms |
|
11 |
- private = notary.PrivKeyPerms |
|
12 |
-) |
|
13 |
- |
|
14 |
-var ( |
|
15 |
- // ErrPathOutsideStore indicates that the returned path would be |
|
16 |
- // outside the store |
|
17 |
- ErrPathOutsideStore = errors.New("path outside file store") |
|
18 |
-) |
|
19 |
- |
|
20 |
-// Storage implements the bare bones primitives (no hierarchy) |
|
21 |
-type Storage interface { |
|
22 |
- // Add writes a file to the specified location, returning an error if this |
|
23 |
- // is not possible (reasons may include permissions errors). The path is cleaned |
|
24 |
- // before being made absolute against the store's base dir. |
|
25 |
- Add(fileName string, data []byte) error |
|
26 |
- |
|
27 |
- // Remove deletes a file from the store relative to the store's base directory. |
|
28 |
- // The path is cleaned before being made absolute to ensure no path traversal |
|
29 |
- // outside the base directory is possible. |
|
30 |
- Remove(fileName string) error |
|
31 |
- |
|
32 |
- // Get returns the file content found at fileName relative to the base directory |
|
33 |
- // of the file store. The path is cleaned before being made absolute to ensure |
|
34 |
- // path traversal outside the store is not possible. If the file is not found |
|
35 |
- // an error to that effect is returned. |
|
36 |
- Get(fileName string) ([]byte, error) |
|
37 |
- |
|
38 |
- // ListFiles returns a list of paths relative to the base directory of the |
|
39 |
- // filestore. Any of these paths must be retrievable via the |
|
40 |
- // Storage.Get method. |
|
41 |
- ListFiles() []string |
|
42 |
-} |
43 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,524 +0,0 @@ |
1 |
-package trustmanager |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "bytes" |
|
5 |
- "crypto/ecdsa" |
|
6 |
- "crypto/elliptic" |
|
7 |
- "crypto/rand" |
|
8 |
- "crypto/rsa" |
|
9 |
- "crypto/x509" |
|
10 |
- "crypto/x509/pkix" |
|
11 |
- "encoding/pem" |
|
12 |
- "errors" |
|
13 |
- "fmt" |
|
14 |
- "io" |
|
15 |
- "io/ioutil" |
|
16 |
- "math/big" |
|
17 |
- "time" |
|
18 |
- |
|
19 |
- "github.com/Sirupsen/logrus" |
|
20 |
- "github.com/agl/ed25519" |
|
21 |
- "github.com/docker/notary" |
|
22 |
- "github.com/docker/notary/tuf/data" |
|
23 |
-) |
|
24 |
- |
|
25 |
-// CertToPEM is a utility function returns a PEM encoded x509 Certificate |
|
26 |
-func CertToPEM(cert *x509.Certificate) []byte { |
|
27 |
- pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) |
|
28 |
- |
|
29 |
- return pemCert |
|
30 |
-} |
|
31 |
- |
|
32 |
-// CertChainToPEM is a utility function returns a PEM encoded chain of x509 Certificates, in the order they are passed |
|
33 |
-func CertChainToPEM(certChain []*x509.Certificate) ([]byte, error) { |
|
34 |
- var pemBytes bytes.Buffer |
|
35 |
- for _, cert := range certChain { |
|
36 |
- if err := pem.Encode(&pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil { |
|
37 |
- return nil, err |
|
38 |
- } |
|
39 |
- } |
|
40 |
- return pemBytes.Bytes(), nil |
|
41 |
-} |
|
42 |
- |
|
43 |
-// LoadCertFromPEM returns the first certificate found in a bunch of bytes or error |
|
44 |
-// if nothing is found. Taken from https://golang.org/src/crypto/x509/cert_pool.go#L85. |
|
45 |
-func LoadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) { |
|
46 |
- for len(pemBytes) > 0 { |
|
47 |
- var block *pem.Block |
|
48 |
- block, pemBytes = pem.Decode(pemBytes) |
|
49 |
- if block == nil { |
|
50 |
- return nil, errors.New("no certificates found in PEM data") |
|
51 |
- } |
|
52 |
- if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { |
|
53 |
- continue |
|
54 |
- } |
|
55 |
- |
|
56 |
- cert, err := x509.ParseCertificate(block.Bytes) |
|
57 |
- if err != nil { |
|
58 |
- continue |
|
59 |
- } |
|
60 |
- |
|
61 |
- return cert, nil |
|
62 |
- } |
|
63 |
- |
|
64 |
- return nil, errors.New("no certificates found in PEM data") |
|
65 |
-} |
|
66 |
- |
|
67 |
-// LoadCertFromFile loads the first certificate from the file provided. The |
|
68 |
-// data is expected to be PEM Encoded and contain one of more certificates |
|
69 |
-// with PEM type "CERTIFICATE" |
|
70 |
-func LoadCertFromFile(filename string) (*x509.Certificate, error) { |
|
71 |
- certs, err := LoadCertBundleFromFile(filename) |
|
72 |
- if err != nil { |
|
73 |
- return nil, err |
|
74 |
- } |
|
75 |
- return certs[0], nil |
|
76 |
-} |
|
77 |
- |
|
78 |
-// LoadCertBundleFromFile loads certificates from the []byte provided. The |
|
79 |
-// data is expected to be PEM Encoded and contain one of more certificates |
|
80 |
-// with PEM type "CERTIFICATE" |
|
81 |
-func LoadCertBundleFromFile(filename string) ([]*x509.Certificate, error) { |
|
82 |
- b, err := ioutil.ReadFile(filename) |
|
83 |
- if err != nil { |
|
84 |
- return nil, err |
|
85 |
- } |
|
86 |
- |
|
87 |
- return LoadCertBundleFromPEM(b) |
|
88 |
-} |
|
89 |
- |
|
90 |
-// LoadCertBundleFromPEM loads certificates from the []byte provided. The |
|
91 |
-// data is expected to be PEM Encoded and contain one of more certificates |
|
92 |
-// with PEM type "CERTIFICATE" |
|
93 |
-func LoadCertBundleFromPEM(pemBytes []byte) ([]*x509.Certificate, error) { |
|
94 |
- certificates := []*x509.Certificate{} |
|
95 |
- var block *pem.Block |
|
96 |
- block, pemBytes = pem.Decode(pemBytes) |
|
97 |
- for ; block != nil; block, pemBytes = pem.Decode(pemBytes) { |
|
98 |
- if block.Type == "CERTIFICATE" { |
|
99 |
- cert, err := x509.ParseCertificate(block.Bytes) |
|
100 |
- if err != nil { |
|
101 |
- return nil, err |
|
102 |
- } |
|
103 |
- certificates = append(certificates, cert) |
|
104 |
- } else { |
|
105 |
- return nil, fmt.Errorf("invalid pem block type: %s", block.Type) |
|
106 |
- } |
|
107 |
- } |
|
108 |
- |
|
109 |
- if len(certificates) == 0 { |
|
110 |
- return nil, fmt.Errorf("no valid certificates found") |
|
111 |
- } |
|
112 |
- |
|
113 |
- return certificates, nil |
|
114 |
-} |
|
115 |
- |
|
116 |
-// GetLeafCerts parses a list of x509 Certificates and returns all of them |
|
117 |
-// that aren't CA |
|
118 |
-func GetLeafCerts(certs []*x509.Certificate) []*x509.Certificate { |
|
119 |
- var leafCerts []*x509.Certificate |
|
120 |
- for _, cert := range certs { |
|
121 |
- if cert.IsCA { |
|
122 |
- continue |
|
123 |
- } |
|
124 |
- leafCerts = append(leafCerts, cert) |
|
125 |
- } |
|
126 |
- return leafCerts |
|
127 |
-} |
|
128 |
- |
|
129 |
-// GetIntermediateCerts parses a list of x509 Certificates and returns all of the |
|
130 |
-// ones marked as a CA, to be used as intermediates |
|
131 |
-func GetIntermediateCerts(certs []*x509.Certificate) []*x509.Certificate { |
|
132 |
- var intCerts []*x509.Certificate |
|
133 |
- for _, cert := range certs { |
|
134 |
- if cert.IsCA { |
|
135 |
- intCerts = append(intCerts, cert) |
|
136 |
- } |
|
137 |
- } |
|
138 |
- return intCerts |
|
139 |
-} |
|
140 |
- |
|
141 |
-// ParsePEMPrivateKey returns a data.PrivateKey from a PEM encoded private key. It |
|
142 |
-// only supports RSA (PKCS#1) and attempts to decrypt using the passphrase, if encrypted. |
|
143 |
-func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (data.PrivateKey, error) { |
|
144 |
- block, _ := pem.Decode(pemBytes) |
|
145 |
- if block == nil { |
|
146 |
- return nil, errors.New("no valid private key found") |
|
147 |
- } |
|
148 |
- |
|
149 |
- var privKeyBytes []byte |
|
150 |
- var err error |
|
151 |
- if x509.IsEncryptedPEMBlock(block) { |
|
152 |
- privKeyBytes, err = x509.DecryptPEMBlock(block, []byte(passphrase)) |
|
153 |
- if err != nil { |
|
154 |
- return nil, errors.New("could not decrypt private key") |
|
155 |
- } |
|
156 |
- } else { |
|
157 |
- privKeyBytes = block.Bytes |
|
158 |
- } |
|
159 |
- |
|
160 |
- switch block.Type { |
|
161 |
- case "RSA PRIVATE KEY": |
|
162 |
- rsaPrivKey, err := x509.ParsePKCS1PrivateKey(privKeyBytes) |
|
163 |
- if err != nil { |
|
164 |
- return nil, fmt.Errorf("could not parse DER encoded key: %v", err) |
|
165 |
- } |
|
166 |
- |
|
167 |
- tufRSAPrivateKey, err := RSAToPrivateKey(rsaPrivKey) |
|
168 |
- if err != nil { |
|
169 |
- return nil, fmt.Errorf("could not convert rsa.PrivateKey to data.PrivateKey: %v", err) |
|
170 |
- } |
|
171 |
- |
|
172 |
- return tufRSAPrivateKey, nil |
|
173 |
- case "EC PRIVATE KEY": |
|
174 |
- ecdsaPrivKey, err := x509.ParseECPrivateKey(privKeyBytes) |
|
175 |
- if err != nil { |
|
176 |
- return nil, fmt.Errorf("could not parse DER encoded private key: %v", err) |
|
177 |
- } |
|
178 |
- |
|
179 |
- tufECDSAPrivateKey, err := ECDSAToPrivateKey(ecdsaPrivKey) |
|
180 |
- if err != nil { |
|
181 |
- return nil, fmt.Errorf("could not convert ecdsa.PrivateKey to data.PrivateKey: %v", err) |
|
182 |
- } |
|
183 |
- |
|
184 |
- return tufECDSAPrivateKey, nil |
|
185 |
- case "ED25519 PRIVATE KEY": |
|
186 |
- // We serialize ED25519 keys by concatenating the private key |
|
187 |
- // to the public key and encoding with PEM. See the |
|
188 |
- // ED25519ToPrivateKey function. |
|
189 |
- tufECDSAPrivateKey, err := ED25519ToPrivateKey(privKeyBytes) |
|
190 |
- if err != nil { |
|
191 |
- return nil, fmt.Errorf("could not convert ecdsa.PrivateKey to data.PrivateKey: %v", err) |
|
192 |
- } |
|
193 |
- |
|
194 |
- return tufECDSAPrivateKey, nil |
|
195 |
- |
|
196 |
- default: |
|
197 |
- return nil, fmt.Errorf("unsupported key type %q", block.Type) |
|
198 |
- } |
|
199 |
-} |
|
200 |
- |
|
201 |
-// ParsePEMPublicKey returns a data.PublicKey from a PEM encoded public key or certificate. |
|
202 |
-func ParsePEMPublicKey(pubKeyBytes []byte) (data.PublicKey, error) { |
|
203 |
- pemBlock, _ := pem.Decode(pubKeyBytes) |
|
204 |
- if pemBlock == nil { |
|
205 |
- return nil, errors.New("no valid public key found") |
|
206 |
- } |
|
207 |
- |
|
208 |
- switch pemBlock.Type { |
|
209 |
- case "CERTIFICATE": |
|
210 |
- cert, err := x509.ParseCertificate(pemBlock.Bytes) |
|
211 |
- if err != nil { |
|
212 |
- return nil, fmt.Errorf("could not parse provided certificate: %v", err) |
|
213 |
- } |
|
214 |
- err = ValidateCertificate(cert) |
|
215 |
- if err != nil { |
|
216 |
- return nil, fmt.Errorf("invalid certificate: %v", err) |
|
217 |
- } |
|
218 |
- return CertToKey(cert), nil |
|
219 |
- default: |
|
220 |
- return nil, fmt.Errorf("unsupported PEM block type %q, expected certificate", pemBlock.Type) |
|
221 |
- } |
|
222 |
-} |
|
223 |
- |
|
224 |
-// ValidateCertificate returns an error if the certificate is not valid for notary |
|
225 |
-// Currently this is only a time expiry check, and ensuring the public key has a large enough modulus if RSA |
|
226 |
-func ValidateCertificate(c *x509.Certificate) error { |
|
227 |
- if (c.NotBefore).After(c.NotAfter) { |
|
228 |
- return fmt.Errorf("certificate validity window is invalid") |
|
229 |
- } |
|
230 |
- now := time.Now() |
|
231 |
- tomorrow := now.AddDate(0, 0, 1) |
|
232 |
- // Give one day leeway on creation "before" time, check "after" against today |
|
233 |
- if (tomorrow).Before(c.NotBefore) || now.After(c.NotAfter) { |
|
234 |
- return fmt.Errorf("certificate is expired") |
|
235 |
- } |
|
236 |
- // If we have an RSA key, make sure it's long enough |
|
237 |
- if c.PublicKeyAlgorithm == x509.RSA { |
|
238 |
- rsaKey, ok := c.PublicKey.(*rsa.PublicKey) |
|
239 |
- if !ok { |
|
240 |
- return fmt.Errorf("unable to parse RSA public key") |
|
241 |
- } |
|
242 |
- if rsaKey.N.BitLen() < notary.MinRSABitSize { |
|
243 |
- return fmt.Errorf("RSA bit length is too short") |
|
244 |
- } |
|
245 |
- } |
|
246 |
- return nil |
|
247 |
-} |
|
248 |
- |
|
249 |
-// GenerateRSAKey generates an RSA private key and returns a TUF PrivateKey |
|
250 |
-func GenerateRSAKey(random io.Reader, bits int) (data.PrivateKey, error) { |
|
251 |
- rsaPrivKey, err := rsa.GenerateKey(random, bits) |
|
252 |
- if err != nil { |
|
253 |
- return nil, fmt.Errorf("could not generate private key: %v", err) |
|
254 |
- } |
|
255 |
- |
|
256 |
- tufPrivKey, err := RSAToPrivateKey(rsaPrivKey) |
|
257 |
- if err != nil { |
|
258 |
- return nil, err |
|
259 |
- } |
|
260 |
- |
|
261 |
- logrus.Debugf("generated RSA key with keyID: %s", tufPrivKey.ID()) |
|
262 |
- |
|
263 |
- return tufPrivKey, nil |
|
264 |
-} |
|
265 |
- |
|
266 |
-// RSAToPrivateKey converts an rsa.Private key to a TUF data.PrivateKey type |
|
267 |
-func RSAToPrivateKey(rsaPrivKey *rsa.PrivateKey) (data.PrivateKey, error) { |
|
268 |
- // Get a DER-encoded representation of the PublicKey |
|
269 |
- rsaPubBytes, err := x509.MarshalPKIXPublicKey(&rsaPrivKey.PublicKey) |
|
270 |
- if err != nil { |
|
271 |
- return nil, fmt.Errorf("failed to marshal public key: %v", err) |
|
272 |
- } |
|
273 |
- |
|
274 |
- // Get a DER-encoded representation of the PrivateKey |
|
275 |
- rsaPrivBytes := x509.MarshalPKCS1PrivateKey(rsaPrivKey) |
|
276 |
- |
|
277 |
- pubKey := data.NewRSAPublicKey(rsaPubBytes) |
|
278 |
- return data.NewRSAPrivateKey(pubKey, rsaPrivBytes) |
|
279 |
-} |
|
280 |
- |
|
281 |
-// GenerateECDSAKey generates an ECDSA Private key and returns a TUF PrivateKey |
|
282 |
-func GenerateECDSAKey(random io.Reader) (data.PrivateKey, error) { |
|
283 |
- ecdsaPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), random) |
|
284 |
- if err != nil { |
|
285 |
- return nil, err |
|
286 |
- } |
|
287 |
- |
|
288 |
- tufPrivKey, err := ECDSAToPrivateKey(ecdsaPrivKey) |
|
289 |
- if err != nil { |
|
290 |
- return nil, err |
|
291 |
- } |
|
292 |
- |
|
293 |
- logrus.Debugf("generated ECDSA key with keyID: %s", tufPrivKey.ID()) |
|
294 |
- |
|
295 |
- return tufPrivKey, nil |
|
296 |
-} |
|
297 |
- |
|
298 |
-// GenerateED25519Key generates an ED25519 private key and returns a TUF |
|
299 |
-// PrivateKey. The serialization format we use is just the public key bytes |
|
300 |
-// followed by the private key bytes |
|
301 |
-func GenerateED25519Key(random io.Reader) (data.PrivateKey, error) { |
|
302 |
- pub, priv, err := ed25519.GenerateKey(random) |
|
303 |
- if err != nil { |
|
304 |
- return nil, err |
|
305 |
- } |
|
306 |
- |
|
307 |
- var serialized [ed25519.PublicKeySize + ed25519.PrivateKeySize]byte |
|
308 |
- copy(serialized[:], pub[:]) |
|
309 |
- copy(serialized[ed25519.PublicKeySize:], priv[:]) |
|
310 |
- |
|
311 |
- tufPrivKey, err := ED25519ToPrivateKey(serialized[:]) |
|
312 |
- if err != nil { |
|
313 |
- return nil, err |
|
314 |
- } |
|
315 |
- |
|
316 |
- logrus.Debugf("generated ED25519 key with keyID: %s", tufPrivKey.ID()) |
|
317 |
- |
|
318 |
- return tufPrivKey, nil |
|
319 |
-} |
|
320 |
- |
|
321 |
-// ECDSAToPrivateKey converts an ecdsa.Private key to a TUF data.PrivateKey type |
|
322 |
-func ECDSAToPrivateKey(ecdsaPrivKey *ecdsa.PrivateKey) (data.PrivateKey, error) { |
|
323 |
- // Get a DER-encoded representation of the PublicKey |
|
324 |
- ecdsaPubBytes, err := x509.MarshalPKIXPublicKey(&ecdsaPrivKey.PublicKey) |
|
325 |
- if err != nil { |
|
326 |
- return nil, fmt.Errorf("failed to marshal public key: %v", err) |
|
327 |
- } |
|
328 |
- |
|
329 |
- // Get a DER-encoded representation of the PrivateKey |
|
330 |
- ecdsaPrivKeyBytes, err := x509.MarshalECPrivateKey(ecdsaPrivKey) |
|
331 |
- if err != nil { |
|
332 |
- return nil, fmt.Errorf("failed to marshal private key: %v", err) |
|
333 |
- } |
|
334 |
- |
|
335 |
- pubKey := data.NewECDSAPublicKey(ecdsaPubBytes) |
|
336 |
- return data.NewECDSAPrivateKey(pubKey, ecdsaPrivKeyBytes) |
|
337 |
-} |
|
338 |
- |
|
339 |
-// ED25519ToPrivateKey converts a serialized ED25519 key to a TUF |
|
340 |
-// data.PrivateKey type |
|
341 |
-func ED25519ToPrivateKey(privKeyBytes []byte) (data.PrivateKey, error) { |
|
342 |
- if len(privKeyBytes) != ed25519.PublicKeySize+ed25519.PrivateKeySize { |
|
343 |
- return nil, errors.New("malformed ed25519 private key") |
|
344 |
- } |
|
345 |
- |
|
346 |
- pubKey := data.NewED25519PublicKey(privKeyBytes[:ed25519.PublicKeySize]) |
|
347 |
- return data.NewED25519PrivateKey(*pubKey, privKeyBytes) |
|
348 |
-} |
|
349 |
- |
|
350 |
-func blockType(k data.PrivateKey) (string, error) { |
|
351 |
- switch k.Algorithm() { |
|
352 |
- case data.RSAKey, data.RSAx509Key: |
|
353 |
- return "RSA PRIVATE KEY", nil |
|
354 |
- case data.ECDSAKey, data.ECDSAx509Key: |
|
355 |
- return "EC PRIVATE KEY", nil |
|
356 |
- case data.ED25519Key: |
|
357 |
- return "ED25519 PRIVATE KEY", nil |
|
358 |
- default: |
|
359 |
- return "", fmt.Errorf("algorithm %s not supported", k.Algorithm()) |
|
360 |
- } |
|
361 |
-} |
|
362 |
- |
|
363 |
-// KeyToPEM returns a PEM encoded key from a Private Key |
|
364 |
-func KeyToPEM(privKey data.PrivateKey, role string) ([]byte, error) { |
|
365 |
- bt, err := blockType(privKey) |
|
366 |
- if err != nil { |
|
367 |
- return nil, err |
|
368 |
- } |
|
369 |
- |
|
370 |
- headers := map[string]string{} |
|
371 |
- if role != "" { |
|
372 |
- headers = map[string]string{ |
|
373 |
- "role": role, |
|
374 |
- } |
|
375 |
- } |
|
376 |
- |
|
377 |
- block := &pem.Block{ |
|
378 |
- Type: bt, |
|
379 |
- Headers: headers, |
|
380 |
- Bytes: privKey.Private(), |
|
381 |
- } |
|
382 |
- |
|
383 |
- return pem.EncodeToMemory(block), nil |
|
384 |
-} |
|
385 |
- |
|
386 |
-// EncryptPrivateKey returns an encrypted PEM key given a Privatekey |
|
387 |
-// and a passphrase |
|
388 |
-func EncryptPrivateKey(key data.PrivateKey, role, passphrase string) ([]byte, error) { |
|
389 |
- bt, err := blockType(key) |
|
390 |
- if err != nil { |
|
391 |
- return nil, err |
|
392 |
- } |
|
393 |
- |
|
394 |
- password := []byte(passphrase) |
|
395 |
- cipherType := x509.PEMCipherAES256 |
|
396 |
- |
|
397 |
- encryptedPEMBlock, err := x509.EncryptPEMBlock(rand.Reader, |
|
398 |
- bt, |
|
399 |
- key.Private(), |
|
400 |
- password, |
|
401 |
- cipherType) |
|
402 |
- if err != nil { |
|
403 |
- return nil, err |
|
404 |
- } |
|
405 |
- |
|
406 |
- if encryptedPEMBlock.Headers == nil { |
|
407 |
- return nil, fmt.Errorf("unable to encrypt key - invalid PEM file produced") |
|
408 |
- } |
|
409 |
- encryptedPEMBlock.Headers["role"] = role |
|
410 |
- |
|
411 |
- return pem.EncodeToMemory(encryptedPEMBlock), nil |
|
412 |
-} |
|
413 |
- |
|
414 |
-// ReadRoleFromPEM returns the value from the role PEM header, if it exists |
|
415 |
-func ReadRoleFromPEM(pemBytes []byte) string { |
|
416 |
- pemBlock, _ := pem.Decode(pemBytes) |
|
417 |
- if pemBlock == nil || pemBlock.Headers == nil { |
|
418 |
- return "" |
|
419 |
- } |
|
420 |
- role, ok := pemBlock.Headers["role"] |
|
421 |
- if !ok { |
|
422 |
- return "" |
|
423 |
- } |
|
424 |
- return role |
|
425 |
-} |
|
426 |
- |
|
427 |
-// CertToKey transforms a single input certificate into its corresponding |
|
428 |
-// PublicKey |
|
429 |
-func CertToKey(cert *x509.Certificate) data.PublicKey { |
|
430 |
- block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw} |
|
431 |
- pemdata := pem.EncodeToMemory(&block) |
|
432 |
- |
|
433 |
- switch cert.PublicKeyAlgorithm { |
|
434 |
- case x509.RSA: |
|
435 |
- return data.NewRSAx509PublicKey(pemdata) |
|
436 |
- case x509.ECDSA: |
|
437 |
- return data.NewECDSAx509PublicKey(pemdata) |
|
438 |
- default: |
|
439 |
- logrus.Debugf("Unknown key type parsed from certificate: %v", cert.PublicKeyAlgorithm) |
|
440 |
- return nil |
|
441 |
- } |
|
442 |
-} |
|
443 |
- |
|
444 |
-// CertsToKeys transforms each of the input certificate chains into its corresponding |
|
445 |
-// PublicKey |
|
446 |
-func CertsToKeys(leafCerts map[string]*x509.Certificate, intCerts map[string][]*x509.Certificate) map[string]data.PublicKey { |
|
447 |
- keys := make(map[string]data.PublicKey) |
|
448 |
- for id, leafCert := range leafCerts { |
|
449 |
- if key, err := CertBundleToKey(leafCert, intCerts[id]); err == nil { |
|
450 |
- keys[key.ID()] = key |
|
451 |
- } |
|
452 |
- } |
|
453 |
- return keys |
|
454 |
-} |
|
455 |
- |
|
456 |
-// CertBundleToKey creates a TUF key from a leaf certs and a list of |
|
457 |
-// intermediates |
|
458 |
-func CertBundleToKey(leafCert *x509.Certificate, intCerts []*x509.Certificate) (data.PublicKey, error) { |
|
459 |
- certBundle := []*x509.Certificate{leafCert} |
|
460 |
- certBundle = append(certBundle, intCerts...) |
|
461 |
- certChainPEM, err := CertChainToPEM(certBundle) |
|
462 |
- if err != nil { |
|
463 |
- return nil, err |
|
464 |
- } |
|
465 |
- var newKey data.PublicKey |
|
466 |
- // Use the leaf cert's public key algorithm for typing |
|
467 |
- switch leafCert.PublicKeyAlgorithm { |
|
468 |
- case x509.RSA: |
|
469 |
- newKey = data.NewRSAx509PublicKey(certChainPEM) |
|
470 |
- case x509.ECDSA: |
|
471 |
- newKey = data.NewECDSAx509PublicKey(certChainPEM) |
|
472 |
- default: |
|
473 |
- logrus.Debugf("Unknown key type parsed from certificate: %v", leafCert.PublicKeyAlgorithm) |
|
474 |
- return nil, x509.ErrUnsupportedAlgorithm |
|
475 |
- } |
|
476 |
- return newKey, nil |
|
477 |
-} |
|
478 |
- |
|
479 |
-// NewCertificate returns an X509 Certificate following a template, given a GUN and validity interval. |
|
480 |
-func NewCertificate(gun string, startTime, endTime time.Time) (*x509.Certificate, error) { |
|
481 |
- serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) |
|
482 |
- |
|
483 |
- serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) |
|
484 |
- if err != nil { |
|
485 |
- return nil, fmt.Errorf("failed to generate new certificate: %v", err) |
|
486 |
- } |
|
487 |
- |
|
488 |
- return &x509.Certificate{ |
|
489 |
- SerialNumber: serialNumber, |
|
490 |
- Subject: pkix.Name{ |
|
491 |
- CommonName: gun, |
|
492 |
- }, |
|
493 |
- NotBefore: startTime, |
|
494 |
- NotAfter: endTime, |
|
495 |
- |
|
496 |
- KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, |
|
497 |
- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, |
|
498 |
- BasicConstraintsValid: true, |
|
499 |
- }, nil |
|
500 |
-} |
|
501 |
- |
|
502 |
-// X509PublicKeyID returns a public key ID as a string, given a |
|
503 |
-// data.PublicKey that contains an X509 Certificate |
|
504 |
-func X509PublicKeyID(certPubKey data.PublicKey) (string, error) { |
|
505 |
- // Note that this only loads the first certificate from the public key |
|
506 |
- cert, err := LoadCertFromPEM(certPubKey.Public()) |
|
507 |
- if err != nil { |
|
508 |
- return "", err |
|
509 |
- } |
|
510 |
- pubKeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey) |
|
511 |
- if err != nil { |
|
512 |
- return "", err |
|
513 |
- } |
|
514 |
- |
|
515 |
- var key data.PublicKey |
|
516 |
- switch certPubKey.Algorithm() { |
|
517 |
- case data.ECDSAx509Key: |
|
518 |
- key = data.NewECDSAPublicKey(pubKeyBytes) |
|
519 |
- case data.RSAx509Key: |
|
520 |
- key = data.NewRSAPublicKey(pubKeyBytes) |
|
521 |
- } |
|
522 |
- |
|
523 |
- return key.ID(), nil |
|
524 |
-} |
525 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,57 @@ |
0 |
+// +build pkcs11 |
|
1 |
+ |
|
2 |
+package yubikey |
|
3 |
+ |
|
4 |
+import ( |
|
5 |
+ "encoding/pem" |
|
6 |
+ "errors" |
|
7 |
+ "github.com/docker/notary" |
|
8 |
+ "github.com/docker/notary/trustmanager" |
|
9 |
+ "github.com/docker/notary/tuf/utils" |
|
10 |
+) |
|
11 |
+ |
|
12 |
+// YubiImport is a wrapper around the YubiStore that allows us to import private |
|
13 |
+// keys to the yubikey |
|
14 |
+type YubiImport struct { |
|
15 |
+ dest *YubiStore |
|
16 |
+ passRetriever notary.PassRetriever |
|
17 |
+} |
|
18 |
+ |
|
19 |
+// NewImporter returns a wrapper for the YubiStore provided that enables importing |
|
20 |
+// keys via the simple Set(string, []byte) interface |
|
21 |
+func NewImporter(ys *YubiStore, ret notary.PassRetriever) *YubiImport { |
|
22 |
+ return &YubiImport{ |
|
23 |
+ dest: ys, |
|
24 |
+ passRetriever: ret, |
|
25 |
+ } |
|
26 |
+} |
|
27 |
+ |
|
28 |
+// Set determines if we are allowed to set the given key on the Yubikey and |
|
29 |
+// calls through to YubiStore.AddKey if it's valid |
|
30 |
+func (s *YubiImport) Set(name string, bytes []byte) error { |
|
31 |
+ block, _ := pem.Decode(bytes) |
|
32 |
+ if block == nil { |
|
33 |
+ return errors.New("invalid PEM data, could not parse") |
|
34 |
+ } |
|
35 |
+ role, ok := block.Headers["role"] |
|
36 |
+ if !ok { |
|
37 |
+ return errors.New("no role found for key") |
|
38 |
+ } |
|
39 |
+ ki := trustmanager.KeyInfo{ |
|
40 |
+ // GUN is ignored by YubiStore |
|
41 |
+ Role: role, |
|
42 |
+ } |
|
43 |
+ privKey, err := utils.ParsePEMPrivateKey(bytes, "") |
|
44 |
+ if err != nil { |
|
45 |
+ privKey, _, err = trustmanager.GetPasswdDecryptBytes( |
|
46 |
+ s.passRetriever, |
|
47 |
+ bytes, |
|
48 |
+ name, |
|
49 |
+ ki.Role, |
|
50 |
+ ) |
|
51 |
+ if err != nil { |
|
52 |
+ return err |
|
53 |
+ } |
|
54 |
+ } |
|
55 |
+ return s.dest.AddKey(ki, privKey) |
|
56 |
+} |
... | ... |
@@ -17,10 +17,11 @@ import ( |
17 | 17 |
"time" |
18 | 18 |
|
19 | 19 |
"github.com/Sirupsen/logrus" |
20 |
- "github.com/docker/notary/passphrase" |
|
20 |
+ "github.com/docker/notary" |
|
21 | 21 |
"github.com/docker/notary/trustmanager" |
22 | 22 |
"github.com/docker/notary/tuf/data" |
23 | 23 |
"github.com/docker/notary/tuf/signed" |
24 |
+ "github.com/docker/notary/tuf/utils" |
|
24 | 25 |
"github.com/miekg/pkcs11" |
25 | 26 |
) |
26 | 27 |
|
... | ... |
@@ -132,7 +133,7 @@ type yubiSlot struct { |
132 | 132 |
// YubiPrivateKey represents a private key inside of a yubikey |
133 | 133 |
type YubiPrivateKey struct { |
134 | 134 |
data.ECDSAPublicKey |
135 |
- passRetriever passphrase.Retriever |
|
135 |
+ passRetriever notary.PassRetriever |
|
136 | 136 |
slot []byte |
137 | 137 |
libLoader pkcs11LibLoader |
138 | 138 |
} |
... | ... |
@@ -143,9 +144,9 @@ type yubikeySigner struct { |
143 | 143 |
} |
144 | 144 |
|
145 | 145 |
// NewYubiPrivateKey returns a YubiPrivateKey, which implements the data.PrivateKey |
146 |
-// interface except that the private material is inacessible |
|
146 |
+// interface except that the private material is inaccessible |
|
147 | 147 |
func NewYubiPrivateKey(slot []byte, pubKey data.ECDSAPublicKey, |
148 |
- passRetriever passphrase.Retriever) *YubiPrivateKey { |
|
148 |
+ passRetriever notary.PassRetriever) *YubiPrivateKey { |
|
149 | 149 |
|
150 | 150 |
return &YubiPrivateKey{ |
151 | 151 |
ECDSAPublicKey: pubKey, |
... | ... |
@@ -228,7 +229,7 @@ func addECDSAKey( |
228 | 228 |
session pkcs11.SessionHandle, |
229 | 229 |
privKey data.PrivateKey, |
230 | 230 |
pkcs11KeyID []byte, |
231 |
- passRetriever passphrase.Retriever, |
|
231 |
+ passRetriever notary.PassRetriever, |
|
232 | 232 |
role string, |
233 | 233 |
) error { |
234 | 234 |
logrus.Debugf("Attempting to add key to yubikey with ID: %s", privKey.ID()) |
... | ... |
@@ -249,7 +250,7 @@ func addECDSAKey( |
249 | 249 |
|
250 | 250 |
// Hard-coded policy: the generated certificate expires in 10 years. |
251 | 251 |
startTime := time.Now() |
252 |
- template, err := trustmanager.NewCertificate(role, startTime, startTime.AddDate(10, 0, 0)) |
|
252 |
+ template, err := utils.NewCertificate(role, startTime, startTime.AddDate(10, 0, 0)) |
|
253 | 253 |
if err != nil { |
254 | 254 |
return fmt.Errorf("failed to create the certificate template: %v", err) |
255 | 255 |
} |
... | ... |
@@ -345,7 +346,7 @@ func getECDSAKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byt |
345 | 345 |
} |
346 | 346 |
|
347 | 347 |
// sign returns a signature for a given signature request |
348 |
-func sign(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever passphrase.Retriever, payload []byte) ([]byte, error) { |
|
348 |
+func sign(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever notary.PassRetriever, payload []byte) ([]byte, error) { |
|
349 | 349 |
err := login(ctx, session, passRetriever, pkcs11.CKU_USER, UserPin) |
350 | 350 |
if err != nil { |
351 | 351 |
return nil, fmt.Errorf("error logging in: %v", err) |
... | ... |
@@ -404,7 +405,7 @@ func sign(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, pass |
404 | 404 |
return sig[:], nil |
405 | 405 |
} |
406 | 406 |
|
407 |
-func yubiRemoveKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever passphrase.Retriever, keyID string) error { |
|
407 |
+func yubiRemoveKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever notary.PassRetriever, keyID string) error { |
|
408 | 408 |
err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SOUserPin) |
409 | 409 |
if err != nil { |
410 | 410 |
return err |
... | ... |
@@ -615,7 +616,7 @@ func getNextEmptySlot(ctx IPKCS11Ctx, session pkcs11.SessionHandle) ([]byte, err |
615 | 615 |
|
616 | 616 |
// YubiStore is a KeyStore for private keys inside a Yubikey |
617 | 617 |
type YubiStore struct { |
618 |
- passRetriever passphrase.Retriever |
|
618 |
+ passRetriever notary.PassRetriever |
|
619 | 619 |
keys map[string]yubiSlot |
620 | 620 |
backupStore trustmanager.KeyStore |
621 | 621 |
libLoader pkcs11LibLoader |
... | ... |
@@ -623,7 +624,7 @@ type YubiStore struct { |
623 | 623 |
|
624 | 624 |
// NewYubiStore returns a YubiStore, given a backup key store to write any |
625 | 625 |
// generated keys to (usually a KeyFileStore) |
626 |
-func NewYubiStore(backupStore trustmanager.KeyStore, passphraseRetriever passphrase.Retriever) ( |
|
626 |
+func NewYubiStore(backupStore trustmanager.KeyStore, passphraseRetriever notary.PassRetriever) ( |
|
627 | 627 |
*YubiStore, error) { |
628 | 628 |
|
629 | 629 |
s := &YubiStore{ |
... | ... |
@@ -653,7 +654,7 @@ func (s *YubiStore) ListKeys() map[string]trustmanager.KeyInfo { |
653 | 653 |
} |
654 | 654 |
ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) |
655 | 655 |
if err != nil { |
656 |
- logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error()) |
|
656 |
+ logrus.Debugf("No yubikey found, using alternative key storage: %s", err.Error()) |
|
657 | 657 |
return nil |
658 | 658 |
} |
659 | 659 |
defer cleanup(ctx, session) |
... | ... |
@@ -697,7 +698,7 @@ func (s *YubiStore) addKey(keyID, role string, privKey data.PrivateKey) ( |
697 | 697 |
|
698 | 698 |
ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) |
699 | 699 |
if err != nil { |
700 |
- logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error()) |
|
700 |
+ logrus.Debugf("No yubikey found, using alternative key storage: %s", err.Error()) |
|
701 | 701 |
return false, err |
702 | 702 |
} |
703 | 703 |
defer cleanup(ctx, session) |
... | ... |
@@ -735,7 +736,7 @@ func (s *YubiStore) addKey(keyID, role string, privKey data.PrivateKey) ( |
735 | 735 |
func (s *YubiStore) GetKey(keyID string) (data.PrivateKey, string, error) { |
736 | 736 |
ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) |
737 | 737 |
if err != nil { |
738 |
- logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error()) |
|
738 |
+ logrus.Debugf("No yubikey found, using alternative key storage: %s", err.Error()) |
|
739 | 739 |
if _, ok := err.(errHSMNotPresent); ok { |
740 | 740 |
err = trustmanager.ErrKeyNotFound{KeyID: keyID} |
741 | 741 |
} |
... | ... |
@@ -770,7 +771,7 @@ func (s *YubiStore) GetKey(keyID string) (data.PrivateKey, string, error) { |
770 | 770 |
func (s *YubiStore) RemoveKey(keyID string) error { |
771 | 771 |
ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) |
772 | 772 |
if err != nil { |
773 |
- logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error()) |
|
773 |
+ logrus.Debugf("No yubikey found, using alternative key storage: %s", err.Error()) |
|
774 | 774 |
return nil |
775 | 775 |
} |
776 | 776 |
defer cleanup(ctx, session) |
... | ... |
@@ -789,12 +790,6 @@ func (s *YubiStore) RemoveKey(keyID string) error { |
789 | 789 |
return err |
790 | 790 |
} |
791 | 791 |
|
792 |
-// ExportKey doesn't work, because you can't export data from a Yubikey |
|
793 |
-func (s *YubiStore) ExportKey(keyID string) ([]byte, error) { |
|
794 |
- logrus.Debugf("Attempting to export: %s key inside of YubiStore", keyID) |
|
795 |
- return nil, errors.New("Keys cannot be exported from a Yubikey.") |
|
796 |
-} |
|
797 |
- |
|
798 | 792 |
// GetKeyInfo is not yet implemented |
799 | 793 |
func (s *YubiStore) GetKeyInfo(keyID string) (trustmanager.KeyInfo, error) { |
800 | 794 |
return trustmanager.KeyInfo{}, fmt.Errorf("Not yet implemented") |
... | ... |
@@ -874,7 +869,7 @@ func IsAccessible() bool { |
874 | 874 |
return true |
875 | 875 |
} |
876 | 876 |
|
877 |
-func login(ctx IPKCS11Ctx, session pkcs11.SessionHandle, passRetriever passphrase.Retriever, userFlag uint, defaultPassw string) error { |
|
877 |
+func login(ctx IPKCS11Ctx, session pkcs11.SessionHandle, passRetriever notary.PassRetriever, userFlag uint, defaultPassw string) error { |
|
878 | 878 |
// try default password |
879 | 879 |
err := ctx.Login(session, userFlag, defaultPassw) |
880 | 880 |
if err == nil { |
... | ... |
@@ -902,13 +897,12 @@ func login(ctx IPKCS11Ctx, session pkcs11.SessionHandle, passRetriever passphras |
902 | 902 |
return trustmanager.ErrAttemptsExceeded{} |
903 | 903 |
} |
904 | 904 |
|
905 |
- // Try to convert PEM encoded bytes back to a PrivateKey using the passphrase |
|
905 |
+ // attempt to login. Loop if failed |
|
906 | 906 |
err = ctx.Login(session, userFlag, passwd) |
907 | 907 |
if err == nil { |
908 | 908 |
return nil |
909 | 909 |
} |
910 | 910 |
} |
911 |
- return nil |
|
912 | 911 |
} |
913 | 912 |
|
914 | 913 |
func buildKeyMap(keys map[string]yubiSlot) map[string]trustmanager.KeyInfo { |
915 | 914 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,37 @@ |
0 |
+-----BEGIN CERTIFICATE----- |
|
1 |
+MIIGMzCCBBugAwIBAgIBATANBgkqhkiG9w0BAQsFADBfMQswCQYDVQQGEwJVUzEL |
|
2 |
+MAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNVBAoMBkRv |
|
3 |
+Y2tlcjEaMBgGA1UEAwwRTm90YXJ5IFRlc3RpbmcgQ0EwHhcNMTUwNzE2MDQyNTAz |
|
4 |
+WhcNMjUwNzEzMDQyNTAzWjBfMRowGAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTEL |
|
5 |
+MAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNVBAoMBkRv |
|
6 |
+Y2tlcjELMAkGA1UECAwCQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC |
|
7 |
+AQCwVVD4pK7z7pXPpJbaZ1Hg5eRXIcaYtbFPCnN0iqy9HsVEGnEn5BPNSEsuP+m0 |
|
8 |
+5N0qVV7DGb1SjiloLXD1qDDvhXWk+giS9ppqPHPLVPB4bvzsqwDYrtpbqkYvO0YK |
|
9 |
+0SL3kxPXUFdlkFfgu0xjlczm2PhWG3Jd8aAtspL/L+VfPA13JUaWxSLpui1In8rh |
|
10 |
+gAyQTK6Q4Of6GbJYTnAHb59UoLXSzB5AfqiUq6L7nEYYKoPflPbRAIWL/UBm0c+H |
|
11 |
+ocms706PYpmPS2RQv3iOGmnn9hEVp3P6jq7WAevbA4aYGx5EsbVtYABqJBbFWAuw |
|
12 |
+wTGRYmzn0Mj0eTMge9ztYB2/2sxdTe6uhmFgpUXngDqJI5O9N3zPfvlEImCky3HM |
|
13 |
+jJoL7g5smqX9o1P+ESLh0VZzhh7IDPzQTXpcPIS/6z0l22QGkK/1N1PaADaUHdLL |
|
14 |
+vSav3y2BaEmPvf2fkZj8yP5eYgi7Cw5ONhHLDYHFcl9Zm/ywmdxHJETz9nfgXnsW |
|
15 |
+HNxDqrkCVO46r/u6rSrUt6hr3oddJG8s8Jo06earw6XU3MzM+3giwkK0SSM3uRPq |
|
16 |
+4AscR1Tv+E31AuOAmjqYQoT29bMIxoSzeljj/YnedwjW45pWyc3JoHaibDwvW9Uo |
|
17 |
+GSZBVy4hrM/Fa7XCWv1WfHNW1gDwaLYwDnl5jFmRBvcfuQIDAQABo4H5MIH2MIGR |
|
18 |
+BgNVHSMEgYkwgYaAFHUM1U3E4WyL1nvFd+dPY8f4O2hZoWOkYTBfMQswCQYDVQQG |
|
19 |
+EwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNV |
|
20 |
+BAoMBkRvY2tlcjEaMBgGA1UEAwwRTm90YXJ5IFRlc3RpbmcgQ0GCCQDCeDLbemIT |
|
21 |
+SzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF |
|
22 |
+BQcDATAOBgNVHQ8BAf8EBAMCAUYwHQYDVR0OBBYEFHe48hcBcAp0bUVlTxXeRA4o |
|
23 |
+E16pMA0GCSqGSIb3DQEBCwUAA4ICAQAWUtAPdUFpwRq+N1SzGUejSikeMGyPZscZ |
|
24 |
+JBUCmhZoFufgXGbLO5OpcRLaV3Xda0t/5PtdGMSEzczeoZHWknDtw+79OBittPPj |
|
25 |
+Sh1oFDuPo35R7eP624lUCch/InZCphTaLx9oDLGcaK3ailQ9wjBdKdlBl8KNKIZp |
|
26 |
+a13aP5rnSm2Jva+tXy/yi3BSds3dGD8ITKZyI/6AFHxGvObrDIBpo4FF/zcWXVDj |
|
27 |
+paOmxplRtM4Hitm+sXGvfqJe4x5DuOXOnPrT3dHvRT6vSZUoKobxMqmRTOcrOIPa |
|
28 |
+EeMpOobshORuRntMDYvvgO3D6p6iciDW2Vp9N6rdMdfOWEQN8JVWvB7IxRHk9qKJ |
|
29 |
+vYOWVbczAt0qpMvXF3PXLjZbUM0knOdUKIEbqP4YUbgdzx6RtgiiY930Aj6tAtce |
|
30 |
+0fpgNlvjMRpSBuWTlAfNNjG/YhndMz9uI68TMfFpR3PcgVIv30krw/9VzoLi2Dpe |
|
31 |
+ow6DrGO6oi+DhN78P4jY/O9UczZK2roZL1Oi5P0RIxf23UZC7x1DlcN3nBr4sYSv |
|
32 |
+rBx4cFTMNpwU+nzsIi4djcFDKmJdEOyjMnkP2v0Lwe7yvK08pZdEu+0zbrq17kue |
|
33 |
+XpXLc7K68QB15yxzGylU5rRwzmC/YsAVyE4eoGu8PxWxrERvHby4B8YP0vAfOraL |
|
34 |
+lKmXlK4dTg== |
|
35 |
+-----END CERTIFICATE----- |
|
36 |
+ |
... | ... |
@@ -5,12 +5,11 @@ import ( |
5 | 5 |
"errors" |
6 | 6 |
"fmt" |
7 | 7 |
"strings" |
8 |
- "time" |
|
9 | 8 |
|
10 | 9 |
"github.com/Sirupsen/logrus" |
11 |
- "github.com/docker/notary/trustmanager" |
|
12 | 10 |
"github.com/docker/notary/tuf/data" |
13 | 11 |
"github.com/docker/notary/tuf/signed" |
12 |
+ "github.com/docker/notary/tuf/utils" |
|
14 | 13 |
) |
15 | 14 |
|
16 | 15 |
// ErrValidationFail is returned when there is no valid trusted certificates |
... | ... |
@@ -98,18 +97,25 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus |
98 | 98 |
// Retrieve all the leaf and intermediate certificates in root for which the CN matches the GUN |
99 | 99 |
allLeafCerts, allIntCerts := parseAllCerts(signedRoot) |
100 | 100 |
certsFromRoot, err := validRootLeafCerts(allLeafCerts, gun, true) |
101 |
+ validIntCerts := validRootIntCerts(allIntCerts) |
|
101 | 102 |
|
102 | 103 |
if err != nil { |
103 | 104 |
logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", gun, err) |
104 | 105 |
return nil, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"} |
105 | 106 |
} |
106 | 107 |
|
108 |
+ logrus.Debugf("found %d leaf certs, of which %d are valid leaf certs for %s", len(allLeafCerts), len(certsFromRoot), gun) |
|
109 |
+ |
|
107 | 110 |
// If we have a previous root, let's try to use it to validate that this new root is valid. |
108 |
- if prevRoot != nil { |
|
111 |
+ havePrevRoot := prevRoot != nil |
|
112 |
+ if havePrevRoot { |
|
109 | 113 |
// Retrieve all the trusted certificates from our previous root |
110 | 114 |
// Note that we do not validate expiries here since our originally trusted root might have expired certs |
111 | 115 |
allTrustedLeafCerts, allTrustedIntCerts := parseAllCerts(prevRoot) |
112 | 116 |
trustedLeafCerts, err := validRootLeafCerts(allTrustedLeafCerts, gun, false) |
117 |
+ if err != nil { |
|
118 |
+ return nil, &ErrValidationFail{Reason: "could not retrieve trusted certs from previous root role data"} |
|
119 |
+ } |
|
113 | 120 |
|
114 | 121 |
// Use the certificates we found in the previous root for the GUN to verify its signatures |
115 | 122 |
// This could potentially be an empty set, in which case we will fail to verify |
... | ... |
@@ -121,45 +127,52 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus |
121 | 121 |
if !ok { |
122 | 122 |
return nil, &ErrValidationFail{Reason: "could not retrieve previous root role data"} |
123 | 123 |
} |
124 |
- |
|
125 | 124 |
err = signed.VerifySignatures( |
126 |
- root, data.BaseRole{Keys: trustmanager.CertsToKeys(trustedLeafCerts, allTrustedIntCerts), Threshold: prevRootRoleData.Threshold}) |
|
125 |
+ root, data.BaseRole{Keys: utils.CertsToKeys(trustedLeafCerts, allTrustedIntCerts), Threshold: prevRootRoleData.Threshold}) |
|
127 | 126 |
if err != nil { |
128 | 127 |
logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err) |
129 | 128 |
return nil, &ErrRootRotationFail{Reason: "failed to validate data with current trusted certificates"} |
130 | 129 |
} |
131 |
- } else { |
|
132 |
- logrus.Debugf("found no currently valid root certificates for %s, using trust_pinning config to bootstrap trust", gun) |
|
133 |
- trustPinCheckFunc, err := NewTrustPinChecker(trustPinning, gun) |
|
134 |
- if err != nil { |
|
135 |
- return nil, &ErrValidationFail{Reason: err.Error()} |
|
130 |
+ // Clear the IsValid marks we could have received from VerifySignatures |
|
131 |
+ for i := range root.Signatures { |
|
132 |
+ root.Signatures[i].IsValid = false |
|
136 | 133 |
} |
134 |
+ } |
|
137 | 135 |
|
138 |
- validPinnedCerts := map[string]*x509.Certificate{} |
|
139 |
- for id, cert := range certsFromRoot { |
|
140 |
- if ok := trustPinCheckFunc(cert, allIntCerts[id]); !ok { |
|
141 |
- continue |
|
142 |
- } |
|
143 |
- validPinnedCerts[id] = cert |
|
144 |
- } |
|
145 |
- if len(validPinnedCerts) == 0 { |
|
146 |
- return nil, &ErrValidationFail{Reason: "unable to match any certificates to trust_pinning config"} |
|
136 |
+ // Regardless of having a previous root or not, confirm that the new root validates against the trust pinning |
|
137 |
+ logrus.Debugf("checking root against trust_pinning config", gun) |
|
138 |
+ trustPinCheckFunc, err := NewTrustPinChecker(trustPinning, gun, !havePrevRoot) |
|
139 |
+ if err != nil { |
|
140 |
+ return nil, &ErrValidationFail{Reason: err.Error()} |
|
141 |
+ } |
|
142 |
+ |
|
143 |
+ validPinnedCerts := map[string]*x509.Certificate{} |
|
144 |
+ for id, cert := range certsFromRoot { |
|
145 |
+ logrus.Debugf("checking trust-pinning for cert: %s", id) |
|
146 |
+ if ok := trustPinCheckFunc(cert, validIntCerts[id]); !ok { |
|
147 |
+ logrus.Debugf("trust-pinning check failed for cert: %s", id) |
|
148 |
+ continue |
|
147 | 149 |
} |
148 |
- certsFromRoot = validPinnedCerts |
|
150 |
+ validPinnedCerts[id] = cert |
|
149 | 151 |
} |
152 |
+ if len(validPinnedCerts) == 0 { |
|
153 |
+ return nil, &ErrValidationFail{Reason: "unable to match any certificates to trust_pinning config"} |
|
154 |
+ } |
|
155 |
+ certsFromRoot = validPinnedCerts |
|
150 | 156 |
|
151 | 157 |
// Validate the integrity of the new root (does it have valid signatures) |
152 | 158 |
// Note that certsFromRoot is guaranteed to be unchanged only if we had prior cert data for this GUN or enabled TOFUS |
153 | 159 |
// If we attempted to pin a certain certificate or CA, certsFromRoot could have been pruned accordingly |
154 | 160 |
err = signed.VerifySignatures(root, data.BaseRole{ |
155 |
- Keys: trustmanager.CertsToKeys(certsFromRoot, allIntCerts), Threshold: rootRole.Threshold}) |
|
161 |
+ Keys: utils.CertsToKeys(certsFromRoot, validIntCerts), Threshold: rootRole.Threshold}) |
|
156 | 162 |
if err != nil { |
157 | 163 |
logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err) |
158 | 164 |
return nil, &ErrValidationFail{Reason: "failed to validate integrity of roots"} |
159 | 165 |
} |
160 | 166 |
|
161 |
- logrus.Debugf("Root validation succeeded for %s", gun) |
|
162 |
- return signedRoot, nil |
|
167 |
+ logrus.Debugf("root validation succeeded for %s", gun) |
|
168 |
+ // Call RootFromSigned to make sure we pick up on the IsValid markings from VerifySignatures |
|
169 |
+ return data.RootFromSigned(root) |
|
163 | 170 |
} |
164 | 171 |
|
165 | 172 |
// validRootLeafCerts returns a list of possibly (if checkExpiry is true) non-expired, non-sha1 certificates |
... | ... |
@@ -177,17 +190,9 @@ func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string, c |
177 | 177 |
continue |
178 | 178 |
} |
179 | 179 |
// Make sure the certificate is not expired if checkExpiry is true |
180 |
- if checkExpiry && time.Now().After(cert.NotAfter) { |
|
181 |
- logrus.Debugf("error leaf certificate is expired") |
|
182 |
- continue |
|
183 |
- } |
|
184 |
- |
|
185 |
- // We don't allow root certificates that use SHA1 |
|
186 |
- if cert.SignatureAlgorithm == x509.SHA1WithRSA || |
|
187 |
- cert.SignatureAlgorithm == x509.DSAWithSHA1 || |
|
188 |
- cert.SignatureAlgorithm == x509.ECDSAWithSHA1 { |
|
189 |
- |
|
190 |
- logrus.Debugf("error certificate uses deprecated hashing algorithm (SHA1)") |
|
180 |
+ // and warn if it hasn't expired yet but is within 6 months of expiry |
|
181 |
+ if err := utils.ValidateCertificate(cert, checkExpiry); err != nil { |
|
182 |
+ logrus.Debugf("%s is invalid: %s", id, err.Error()) |
|
191 | 183 |
continue |
192 | 184 |
} |
193 | 185 |
|
... | ... |
@@ -204,6 +209,24 @@ func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string, c |
204 | 204 |
return validLeafCerts, nil |
205 | 205 |
} |
206 | 206 |
|
207 |
+// validRootIntCerts filters the passed in structure of intermediate certificates to only include non-expired, non-sha1 certificates |
|
208 |
+// Note that this "validity" alone does not imply any measure of trust. |
|
209 |
+func validRootIntCerts(allIntCerts map[string][]*x509.Certificate) map[string][]*x509.Certificate { |
|
210 |
+ validIntCerts := make(map[string][]*x509.Certificate) |
|
211 |
+ |
|
212 |
+ // Go through every leaf cert ID, and build its valid intermediate certificate list |
|
213 |
+ for leafID, intCertList := range allIntCerts { |
|
214 |
+ for _, intCert := range intCertList { |
|
215 |
+ if err := utils.ValidateCertificate(intCert, true); err != nil { |
|
216 |
+ continue |
|
217 |
+ } |
|
218 |
+ validIntCerts[leafID] = append(validIntCerts[leafID], intCert) |
|
219 |
+ } |
|
220 |
+ |
|
221 |
+ } |
|
222 |
+ return validIntCerts |
|
223 |
+} |
|
224 |
+ |
|
207 | 225 |
// parseAllCerts returns two maps, one with all of the leafCertificates and one |
208 | 226 |
// with all the intermediate certificates found in signedRoot |
209 | 227 |
func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, map[string][]*x509.Certificate) { |
... | ... |
@@ -233,14 +256,14 @@ func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, m |
233 | 233 |
|
234 | 234 |
// Decode all the x509 certificates that were bundled with this |
235 | 235 |
// Specific root key |
236 |
- decodedCerts, err := trustmanager.LoadCertBundleFromPEM(key.Public()) |
|
236 |
+ decodedCerts, err := utils.LoadCertBundleFromPEM(key.Public()) |
|
237 | 237 |
if err != nil { |
238 | 238 |
logrus.Debugf("error while parsing root certificate with keyID: %s, %v", keyID, err) |
239 | 239 |
continue |
240 | 240 |
} |
241 | 241 |
|
242 | 242 |
// Get all non-CA certificates in the decoded certificates |
243 |
- leafCertList := trustmanager.GetLeafCerts(decodedCerts) |
|
243 |
+ leafCertList := utils.GetLeafCerts(decodedCerts) |
|
244 | 244 |
|
245 | 245 |
// If we got no leaf certificates or we got more than one, fail |
246 | 246 |
if len(leafCertList) != 1 { |
... | ... |
@@ -260,7 +283,7 @@ func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, m |
260 | 260 |
leafCerts[key.ID()] = leafCert |
261 | 261 |
|
262 | 262 |
// Get all the remainder certificates marked as a CA to be used as intermediates |
263 |
- intermediateCerts := trustmanager.GetIntermediateCerts(decodedCerts) |
|
263 |
+ intermediateCerts := utils.GetIntermediateCerts(decodedCerts) |
|
264 | 264 |
intCerts[key.ID()] = intermediateCerts |
265 | 265 |
} |
266 | 266 |
|
267 | 267 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,31 @@ |
0 |
+-----BEGIN CERTIFICATE----- |
|
1 |
+MIIFKzCCAxWgAwIBAgIQRyp9QqcJfd3ayqdjiz8xIDALBgkqhkiG9w0BAQswODEa |
|
2 |
+MBgGA1UEChMRZG9ja2VyLmNvbS9ub3RhcnkxGjAYBgNVBAMTEWRvY2tlci5jb20v |
|
3 |
+bm90YXJ5MB4XDTE1MDcxNzA2MzQyM1oXDTE3MDcxNjA2MzQyM1owODEaMBgGA1UE |
|
4 |
+ChMRZG9ja2VyLmNvbS9ub3RhcnkxGjAYBgNVBAMTEWRvY2tlci5jb20vbm90YXJ5 |
|
5 |
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoQffrzsYnsH8vGf4Jh55 |
|
6 |
+Cj5wrjUGzD/sHkaFHptjJ6ToJGJv5yMAPxzyInu5sIoGLJapnYVBoAU0YgI9qlAc |
|
7 |
+YA6SxaSwgm6rpvmnl8Qn0qc6ger3inpGaUJylWHuPwWkvcimQAqHZx2dQtL7g6kp |
|
8 |
+rmKeTWpWoWLw3JoAUZUVhZMd6a22ZL/DvAw+Hrogbz4XeyahFb9IH402zPxN6vga |
|
9 |
+JEFTF0Ji1jtNg0Mo4pb9SHsMsiw+LZK7SffHVKPxvd21m/biNmwsgExA3U8OOG8p |
|
10 |
+uygfacys5c8+ZrX+ZFG/cvwKz0k6/QfJU40s6MhXw5C2WttdVmsG9/7rGFYjHoIJ |
|
11 |
+weDyxgWk7vxKzRJI/un7cagDIaQsKrJQcCHIGFRlpIR5TwX7vl3R7cRncrDRMVvc |
|
12 |
+VSEG2esxbw7jtzIp/ypnVRxcOny7IypyjKqVeqZ6HgxZtTBVrF1O/aHo2kvlwyRS |
|
13 |
+Aus4kvh6z3+jzTm9EzfXiPQzY9BEk5gOLxhW9rc6UhlS+pe5lkaN/Hyqy/lPuq89 |
|
14 |
+fMr2rr7lf5WFdFnze6WNYMAaW7dNA4NE0dyD53428ZLXxNVPL4WU66Gac6lynQ8l |
|
15 |
+r5tPsYIFXzh6FVaRKGQUtW1hz9ecO6Y27Rh2JsyiIxgUqk2ooxE69uN42t+dtqKC |
|
16 |
+1s8G/7VtY8GDALFLYTnzLvsCAwEAAaM1MDMwDgYDVR0PAQH/BAQDAgCgMBMGA1Ud |
|
17 |
+JQQMMAoGCCsGAQUFBwMDMAwGA1UdEwEB/wQCMAAwCwYJKoZIhvcNAQELA4ICAQBM |
|
18 |
+Oll3G/XBz8idiNdNJDWUh+5w3ojmwanrTBdCdqEk1WenaR6DtcflJx6Z3f/mwV4o |
|
19 |
+b1skOAX1yX5RCahJHUMxMicz/Q38pOVelGPrWnc3TJB+VKjGyHXlQDVkZFb+4+ef |
|
20 |
+wtj7HngXhHFFDSgjm3EdMndvgDQ7SQb4skOnCNS9iyX7eXxhFBCZmZL+HALKBj2B |
|
21 |
+yhV4IcBDqmp504t14rx9/Jvty0dG7fY7I51gEQpm4S02JML5xvTm1xfboWIhZODI |
|
22 |
+swEAO+ekBoFHbS1Q9KMPjIAw3TrCHH8x8XZq5zsYtAC1yZHdCKa26aWdy56A9eHj |
|
23 |
+O1VxzwmbNyXRenVuBYP+0wr3HVKFG4JJ4ZZpNZzQW/pqEPghCTJIvIueK652ByUc |
|
24 |
+//sv+nXd5f19LeES9pf0l253NDaFZPb6aegKfquWh8qlQBmUQ2GzaTLbtmNd28M6 |
|
25 |
+W7iL7tkKZe1ZnBz9RKgtPrDjjWGZInjjcOU8EtT4SLq7kCVDmPs5MD8vaAm96JsE |
|
26 |
+jmLC3Uu/4k7HiDYX0i0mOWkFjZQMdVatcIF5FPSppwsSbW8QidnXt54UtwtFDEPz |
|
27 |
+lpjs7ybeQE71JXcMZnVIK4bjRXsEFPI98RpIlEdedbSUdYAncLNJRT7HZBMPGSwZ |
|
28 |
+0PNJuglnlr3srVzdW1dz2xQjdvLwxy6mNUF6rbQBWA== |
|
29 |
+-----END CERTIFICATE----- |
|
30 |
+ |
... | ... |
@@ -4,7 +4,6 @@ import ( |
4 | 4 |
"crypto/x509" |
5 | 5 |
"fmt" |
6 | 6 |
"github.com/Sirupsen/logrus" |
7 |
- "github.com/docker/notary/trustmanager" |
|
8 | 7 |
"github.com/docker/notary/tuf/utils" |
9 | 8 |
"strings" |
10 | 9 |
) |
... | ... |
@@ -28,25 +27,29 @@ type trustPinChecker struct { |
28 | 28 |
type CertChecker func(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool |
29 | 29 |
|
30 | 30 |
// NewTrustPinChecker returns a new certChecker function from a TrustPinConfig for a GUN |
31 |
-func NewTrustPinChecker(trustPinConfig TrustPinConfig, gun string) (CertChecker, error) { |
|
31 |
+func NewTrustPinChecker(trustPinConfig TrustPinConfig, gun string, firstBootstrap bool) (CertChecker, error) { |
|
32 | 32 |
t := trustPinChecker{gun: gun, config: trustPinConfig} |
33 | 33 |
// Determine the mode, and if it's even valid |
34 | 34 |
if pinnedCerts, ok := trustPinConfig.Certs[gun]; ok { |
35 |
+ logrus.Debugf("trust-pinning using Cert IDs") |
|
35 | 36 |
t.pinnedCertIDs = pinnedCerts |
36 | 37 |
return t.certsCheck, nil |
37 | 38 |
} |
38 | 39 |
|
39 | 40 |
if caFilepath, err := getPinnedCAFilepathByPrefix(gun, trustPinConfig); err == nil { |
41 |
+ logrus.Debugf("trust-pinning using root CA bundle at: %s", caFilepath) |
|
42 |
+ |
|
40 | 43 |
// Try to add the CA certs from its bundle file to our certificate store, |
41 | 44 |
// and use it to validate certs in the root.json later |
42 |
- caCerts, err := trustmanager.LoadCertBundleFromFile(caFilepath) |
|
45 |
+ caCerts, err := utils.LoadCertBundleFromFile(caFilepath) |
|
43 | 46 |
if err != nil { |
44 | 47 |
return nil, fmt.Errorf("could not load root cert from CA path") |
45 | 48 |
} |
46 | 49 |
// Now only consider certificates that are direct children from this CA cert chain |
47 | 50 |
caRootPool := x509.NewCertPool() |
48 | 51 |
for _, caCert := range caCerts { |
49 |
- if err = trustmanager.ValidateCertificate(caCert); err != nil { |
|
52 |
+ if err = utils.ValidateCertificate(caCert, true); err != nil { |
|
53 |
+ logrus.Debugf("ignoring root CA certificate with CN %s in bundle: %s", caCert.Subject.CommonName, err) |
|
50 | 54 |
continue |
51 | 55 |
} |
52 | 56 |
caRootPool.AddCert(caCert) |
... | ... |
@@ -59,16 +62,18 @@ func NewTrustPinChecker(trustPinConfig TrustPinConfig, gun string) (CertChecker, |
59 | 59 |
return t.caCheck, nil |
60 | 60 |
} |
61 | 61 |
|
62 |
- if !trustPinConfig.DisableTOFU { |
|
63 |
- return t.tofusCheck, nil |
|
62 |
+ // If TOFUs is disabled and we don't have any previous trusted root data for this GUN, we error out |
|
63 |
+ if trustPinConfig.DisableTOFU && firstBootstrap { |
|
64 |
+ return nil, fmt.Errorf("invalid trust pinning specified") |
|
65 |
+ |
|
64 | 66 |
} |
65 |
- return nil, fmt.Errorf("invalid trust pinning specified") |
|
67 |
+ return t.tofusCheck, nil |
|
66 | 68 |
} |
67 | 69 |
|
68 | 70 |
func (t trustPinChecker) certsCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { |
69 | 71 |
// reconstruct the leaf + intermediate cert chain, which is bundled as {leaf, intermediates...}, |
70 | 72 |
// in order to get the matching id in the root file |
71 |
- key, err := trustmanager.CertBundleToKey(leafCert, intCerts) |
|
73 |
+ key, err := utils.CertBundleToKey(leafCert, intCerts) |
|
72 | 74 |
if err != nil { |
73 | 75 |
logrus.Debug("error creating cert bundle: ", err.Error()) |
74 | 76 |
return false |
... | ... |
@@ -84,9 +89,11 @@ func (t trustPinChecker) caCheck(leafCert *x509.Certificate, intCerts []*x509.Ce |
84 | 84 |
} |
85 | 85 |
// Attempt to find a valid certificate chain from the leaf cert to CA root |
86 | 86 |
// Use this certificate if such a valid chain exists (possibly using intermediates) |
87 |
- if _, err := leafCert.Verify(x509.VerifyOptions{Roots: t.pinnedCAPool, Intermediates: caIntPool}); err == nil { |
|
87 |
+ var err error |
|
88 |
+ if _, err = leafCert.Verify(x509.VerifyOptions{Roots: t.pinnedCAPool, Intermediates: caIntPool}); err == nil { |
|
88 | 89 |
return true |
89 | 90 |
} |
91 |
+ logrus.Debugf("unable to find a valid certificate chain from leaf cert to CA root: %s", err) |
|
90 | 92 |
return false |
91 | 93 |
} |
92 | 94 |
|
... | ... |
@@ -1,36 +1,6 @@ |
1 |
-# GOTUF |
|
2 |
- |
|
3 |
-This is still a work in progress but will shortly be a fully compliant |
|
4 |
-Go implementation of [The Update Framework (TUF)](http://theupdateframework.com/). |
|
5 |
- |
|
6 |
-## Where's the CLI |
|
7 |
- |
|
8 |
-This repository provides a library only. The [Notary project](https://github.com/docker/notary) |
|
9 |
-from Docker should be considered the official CLI to be used with this implementation of TUF. |
|
10 |
- |
|
11 |
-## TODOs: |
|
12 |
- |
|
13 |
-- [X] Add Targets to existing repo |
|
14 |
-- [X] Sign metadata files |
|
15 |
-- [X] Refactor TufRepo to take care of signing ~~and verification~~ |
|
16 |
-- [ ] Ensure consistent capitalization in naming (TUF\_\_\_ vs Tuf\_\_\_) |
|
17 |
-- [X] Make caching of metadata files smarter - PR #5 |
|
18 |
-- [ ] ~~Add configuration for CLI commands. Order of configuration priority from most to least: flags, config file, defaults~~ Notary should be the official CLI |
|
19 |
-- [X] Reasses organization of data types. Possibly consolidate a few things into the data package but break up package into a few more distinct files |
|
20 |
-- [ ] Comprehensive test cases |
|
21 |
-- [ ] Delete files no longer in use |
|
22 |
-- [ ] Fix up errors. Some have to be instantiated, others don't, the inconsistency is annoying. |
|
23 |
-- [X] Bump version numbers in meta files (could probably be done better) |
|
24 |
- |
|
25 | 1 |
## Credits |
26 | 2 |
|
27 |
-This implementation was originally forked from [flynn/go-tuf](https://github.com/flynn/go-tuf), |
|
28 |
-however in attempting to add delegations I found I was making such |
|
29 |
-significant changes that I could not maintain backwards compatibility |
|
30 |
-without the code becoming overly convoluted. |
|
31 |
- |
|
32 |
-Some features such as pluggable verifiers have already been merged upstream to flynn/go-tuf |
|
33 |
-and we are in discussion with [titanous](https://github.com/titanous) about working to merge the 2 implementations. |
|
3 |
+This implementation was originally forked from [flynn/go-tuf](https://github.com/flynn/go-tuf) |
|
34 | 4 |
|
35 | 5 |
This implementation retains the same 3 Clause BSD license present on |
36 | 6 |
the original flynn implementation. |
... | ... |
@@ -18,7 +18,7 @@ var ErrBuildDone = fmt.Errorf( |
18 | 18 |
"the builder has finished building and cannot accept any more input or produce any more output") |
19 | 19 |
|
20 | 20 |
// ErrInvalidBuilderInput is returned when RepoBuilder.Load is called |
21 |
-// with the wrong type of metadata for thes tate that it's in |
|
21 |
+// with the wrong type of metadata for the state that it's in |
|
22 | 22 |
type ErrInvalidBuilderInput struct{ msg string } |
23 | 23 |
|
24 | 24 |
func (e ErrInvalidBuilderInput) Error() string { |
... | ... |
@@ -59,8 +59,9 @@ type RepoBuilder interface { |
59 | 59 |
Load(roleName string, content []byte, minVersion int, allowExpired bool) error |
60 | 60 |
GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) |
61 | 61 |
GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error) |
62 |
- Finish() (*Repo, error) |
|
62 |
+ Finish() (*Repo, *Repo, error) |
|
63 | 63 |
BootstrapNewBuilder() RepoBuilder |
64 |
+ BootstrapNewBuilderWithNewTrustpin(trustpin trustpinning.TrustPinConfig) RepoBuilder |
|
64 | 65 |
|
65 | 66 |
// informative functions |
66 | 67 |
IsLoaded(roleName string) bool |
... | ... |
@@ -80,8 +81,11 @@ func (f finishedBuilder) GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, in |
80 | 80 |
func (f finishedBuilder) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error) { |
81 | 81 |
return nil, 0, ErrBuildDone |
82 | 82 |
} |
83 |
-func (f finishedBuilder) Finish() (*Repo, error) { return nil, ErrBuildDone } |
|
84 |
-func (f finishedBuilder) BootstrapNewBuilder() RepoBuilder { return f } |
|
83 |
+func (f finishedBuilder) Finish() (*Repo, *Repo, error) { return nil, nil, ErrBuildDone } |
|
84 |
+func (f finishedBuilder) BootstrapNewBuilder() RepoBuilder { return f } |
|
85 |
+func (f finishedBuilder) BootstrapNewBuilderWithNewTrustpin(trustpin trustpinning.TrustPinConfig) RepoBuilder { |
|
86 |
+ return f |
|
87 |
+} |
|
85 | 88 |
func (f finishedBuilder) IsLoaded(roleName string) bool { return false } |
86 | 89 |
func (f finishedBuilder) GetLoadedVersion(roleName string) int { return 0 } |
87 | 90 |
func (f finishedBuilder) GetConsistentInfo(roleName string) ConsistentInfo { |
... | ... |
@@ -90,12 +94,21 @@ func (f finishedBuilder) GetConsistentInfo(roleName string) ConsistentInfo { |
90 | 90 |
|
91 | 91 |
// NewRepoBuilder is the only way to get a pre-built RepoBuilder |
92 | 92 |
func NewRepoBuilder(gun string, cs signed.CryptoService, trustpin trustpinning.TrustPinConfig) RepoBuilder { |
93 |
- return &repoBuilderWrapper{RepoBuilder: &repoBuilder{ |
|
94 |
- repo: NewRepo(cs), |
|
95 |
- gun: gun, |
|
96 |
- trustpin: trustpin, |
|
97 |
- loadedNotChecksummed: make(map[string][]byte), |
|
98 |
- }} |
|
93 |
+ return NewBuilderFromRepo(gun, NewRepo(cs), trustpin) |
|
94 |
+} |
|
95 |
+ |
|
96 |
+// NewBuilderFromRepo allows us to bootstrap a builder given existing repo data. |
|
97 |
+// YOU PROBABLY SHOULDN'T BE USING THIS OUTSIDE OF TESTING CODE!!! |
|
98 |
+func NewBuilderFromRepo(gun string, repo *Repo, trustpin trustpinning.TrustPinConfig) RepoBuilder { |
|
99 |
+ return &repoBuilderWrapper{ |
|
100 |
+ RepoBuilder: &repoBuilder{ |
|
101 |
+ repo: repo, |
|
102 |
+ invalidRoles: NewRepo(nil), |
|
103 |
+ gun: gun, |
|
104 |
+ trustpin: trustpin, |
|
105 |
+ loadedNotChecksummed: make(map[string][]byte), |
|
106 |
+ }, |
|
107 |
+ } |
|
99 | 108 |
} |
100 | 109 |
|
101 | 110 |
// repoBuilderWrapper embeds a repoBuilder, but once Finish is called, swaps |
... | ... |
@@ -104,7 +117,7 @@ type repoBuilderWrapper struct { |
104 | 104 |
RepoBuilder |
105 | 105 |
} |
106 | 106 |
|
107 |
-func (rbw *repoBuilderWrapper) Finish() (*Repo, error) { |
|
107 |
+func (rbw *repoBuilderWrapper) Finish() (*Repo, *Repo, error) { |
|
108 | 108 |
switch rbw.RepoBuilder.(type) { |
109 | 109 |
case finishedBuilder: |
110 | 110 |
return rbw.RepoBuilder.Finish() |
... | ... |
@@ -117,7 +130,8 @@ func (rbw *repoBuilderWrapper) Finish() (*Repo, error) { |
117 | 117 |
|
118 | 118 |
// repoBuilder actually builds a tuf.Repo |
119 | 119 |
type repoBuilder struct { |
120 |
- repo *Repo |
|
120 |
+ repo *Repo |
|
121 |
+ invalidRoles *Repo |
|
121 | 122 |
|
122 | 123 |
// needed for root trust pininng verification |
123 | 124 |
gun string |
... | ... |
@@ -136,13 +150,14 @@ type repoBuilder struct { |
136 | 136 |
nextRootChecksum *data.FileMeta |
137 | 137 |
} |
138 | 138 |
|
139 |
-func (rb *repoBuilder) Finish() (*Repo, error) { |
|
140 |
- return rb.repo, nil |
|
139 |
+func (rb *repoBuilder) Finish() (*Repo, *Repo, error) { |
|
140 |
+ return rb.repo, rb.invalidRoles, nil |
|
141 | 141 |
} |
142 | 142 |
|
143 | 143 |
func (rb *repoBuilder) BootstrapNewBuilder() RepoBuilder { |
144 | 144 |
return &repoBuilderWrapper{RepoBuilder: &repoBuilder{ |
145 | 145 |
repo: NewRepo(rb.repo.cryptoService), |
146 |
+ invalidRoles: NewRepo(nil), |
|
146 | 147 |
gun: rb.gun, |
147 | 148 |
loadedNotChecksummed: make(map[string][]byte), |
148 | 149 |
trustpin: rb.trustpin, |
... | ... |
@@ -152,6 +167,18 @@ func (rb *repoBuilder) BootstrapNewBuilder() RepoBuilder { |
152 | 152 |
}} |
153 | 153 |
} |
154 | 154 |
|
155 |
+func (rb *repoBuilder) BootstrapNewBuilderWithNewTrustpin(trustpin trustpinning.TrustPinConfig) RepoBuilder { |
|
156 |
+ return &repoBuilderWrapper{RepoBuilder: &repoBuilder{ |
|
157 |
+ repo: NewRepo(rb.repo.cryptoService), |
|
158 |
+ gun: rb.gun, |
|
159 |
+ loadedNotChecksummed: make(map[string][]byte), |
|
160 |
+ trustpin: trustpin, |
|
161 |
+ |
|
162 |
+ prevRoot: rb.repo.Root, |
|
163 |
+ bootstrappedRootChecksum: rb.nextRootChecksum, |
|
164 |
+ }} |
|
165 |
+} |
|
166 |
+ |
|
155 | 167 |
// IsLoaded returns whether a particular role has already been loaded |
156 | 168 |
func (rb *repoBuilder) IsLoaded(roleName string) bool { |
157 | 169 |
switch roleName { |
... | ... |
@@ -338,7 +365,7 @@ func (rb *repoBuilder) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, in |
338 | 338 |
return nil, 0, ErrInvalidBuilderInput{msg: "timestamp has already been loaded"} |
339 | 339 |
} |
340 | 340 |
|
341 |
- // SignTimetamp always serializes the loaded snapshot and signs in the data, so we must always |
|
341 |
+ // SignTimestamp always serializes the loaded snapshot and signs in the data, so we must always |
|
342 | 342 |
// have the snapshot loaded first |
343 | 343 |
if err := rb.checkPrereqsLoaded([]string{data.CanonicalRootRole, data.CanonicalSnapshotRole}); err != nil { |
344 | 344 |
return nil, 0, err |
... | ... |
@@ -411,7 +438,6 @@ func (rb *repoBuilder) loadRoot(content []byte, minVersion int, allowExpired boo |
411 | 411 |
if err != nil { // this should never happen since the root has been validated |
412 | 412 |
return err |
413 | 413 |
} |
414 |
- |
|
415 | 414 |
rb.repo.Root = signedRoot |
416 | 415 |
rb.repo.originalRootRole = rootRole |
417 | 416 |
return nil |
... | ... |
@@ -524,6 +550,7 @@ func (rb *repoBuilder) loadTargets(content []byte, minVersion int, allowExpired |
524 | 524 |
} |
525 | 525 |
} |
526 | 526 |
|
527 |
+ signedTargets.Signatures = signedObj.Signatures |
|
527 | 528 |
rb.repo.Targets[roleName] = signedTargets |
528 | 529 |
return nil |
529 | 530 |
} |
... | ... |
@@ -534,7 +561,8 @@ func (rb *repoBuilder) loadDelegation(roleName string, content []byte, minVersio |
534 | 534 |
return err |
535 | 535 |
} |
536 | 536 |
|
537 |
- signedObj, err := rb.bytesToSignedAndValidateSigs(delegationRole.BaseRole, content) |
|
537 |
+ // bytesToSigned checks checksum |
|
538 |
+ signedObj, err := rb.bytesToSigned(content, roleName) |
|
538 | 539 |
if err != nil { |
539 | 540 |
return err |
540 | 541 |
} |
... | ... |
@@ -545,15 +573,24 @@ func (rb *repoBuilder) loadDelegation(roleName string, content []byte, minVersio |
545 | 545 |
} |
546 | 546 |
|
547 | 547 |
if err := signed.VerifyVersion(&(signedTargets.Signed.SignedCommon), minVersion); err != nil { |
548 |
+ // don't capture in invalidRoles because the role we received is a rollback |
|
549 |
+ return err |
|
550 |
+ } |
|
551 |
+ |
|
552 |
+ // verify signature |
|
553 |
+ if err := signed.VerifySignatures(signedObj, delegationRole.BaseRole); err != nil { |
|
554 |
+ rb.invalidRoles.Targets[roleName] = signedTargets |
|
548 | 555 |
return err |
549 | 556 |
} |
550 | 557 |
|
551 | 558 |
if !allowExpired { // check must go at the end because all other validation should pass |
552 | 559 |
if err := signed.VerifyExpiry(&(signedTargets.Signed.SignedCommon), roleName); err != nil { |
560 |
+ rb.invalidRoles.Targets[roleName] = signedTargets |
|
553 | 561 |
return err |
554 | 562 |
} |
555 | 563 |
} |
556 | 564 |
|
565 |
+ signedTargets.Signatures = signedObj.Signatures |
|
557 | 566 |
rb.repo.Targets[roleName] = signedTargets |
558 | 567 |
return nil |
559 | 568 |
} |
560 | 569 |
deleted file mode 100644 |
... | ... |
@@ -1,229 +0,0 @@ |
1 |
-package client |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "encoding/json" |
|
5 |
- |
|
6 |
- "github.com/Sirupsen/logrus" |
|
7 |
- "github.com/docker/notary" |
|
8 |
- tuf "github.com/docker/notary/tuf" |
|
9 |
- "github.com/docker/notary/tuf/data" |
|
10 |
- "github.com/docker/notary/tuf/store" |
|
11 |
-) |
|
12 |
- |
|
13 |
-// Client is a usability wrapper around a raw TUF repo |
|
14 |
-type Client struct { |
|
15 |
- remote store.RemoteStore |
|
16 |
- cache store.MetadataStore |
|
17 |
- oldBuilder tuf.RepoBuilder |
|
18 |
- newBuilder tuf.RepoBuilder |
|
19 |
-} |
|
20 |
- |
|
21 |
-// NewClient initialized a Client with the given repo, remote source of content, and cache |
|
22 |
-func NewClient(oldBuilder, newBuilder tuf.RepoBuilder, remote store.RemoteStore, cache store.MetadataStore) *Client { |
|
23 |
- return &Client{ |
|
24 |
- oldBuilder: oldBuilder, |
|
25 |
- newBuilder: newBuilder, |
|
26 |
- remote: remote, |
|
27 |
- cache: cache, |
|
28 |
- } |
|
29 |
-} |
|
30 |
- |
|
31 |
-// Update performs an update to the TUF repo as defined by the TUF spec |
|
32 |
-func (c *Client) Update() (*tuf.Repo, error) { |
|
33 |
- // 1. Get timestamp |
|
34 |
- // a. If timestamp error (verification, expired, etc...) download new root and return to 1. |
|
35 |
- // 2. Check if local snapshot is up to date |
|
36 |
- // a. If out of date, get updated snapshot |
|
37 |
- // i. If snapshot error, download new root and return to 1. |
|
38 |
- // 3. Check if root correct against snapshot |
|
39 |
- // a. If incorrect, download new root and return to 1. |
|
40 |
- // 4. Iteratively download and search targets and delegations to find target meta |
|
41 |
- logrus.Debug("updating TUF client") |
|
42 |
- err := c.update() |
|
43 |
- if err != nil { |
|
44 |
- logrus.Debug("Error occurred. Root will be downloaded and another update attempted") |
|
45 |
- logrus.Debug("Resetting the TUF builder...") |
|
46 |
- |
|
47 |
- c.newBuilder = c.newBuilder.BootstrapNewBuilder() |
|
48 |
- |
|
49 |
- if err := c.downloadRoot(); err != nil { |
|
50 |
- logrus.Debug("Client Update (Root):", err) |
|
51 |
- return nil, err |
|
52 |
- } |
|
53 |
- // If we error again, we now have the latest root and just want to fail |
|
54 |
- // out as there's no expectation the problem can be resolved automatically |
|
55 |
- logrus.Debug("retrying TUF client update") |
|
56 |
- if err := c.update(); err != nil { |
|
57 |
- return nil, err |
|
58 |
- } |
|
59 |
- } |
|
60 |
- return c.newBuilder.Finish() |
|
61 |
-} |
|
62 |
- |
|
63 |
-func (c *Client) update() error { |
|
64 |
- if err := c.downloadTimestamp(); err != nil { |
|
65 |
- logrus.Debugf("Client Update (Timestamp): %s", err.Error()) |
|
66 |
- return err |
|
67 |
- } |
|
68 |
- if err := c.downloadSnapshot(); err != nil { |
|
69 |
- logrus.Debugf("Client Update (Snapshot): %s", err.Error()) |
|
70 |
- return err |
|
71 |
- } |
|
72 |
- // will always need top level targets at a minimum |
|
73 |
- if err := c.downloadTargets(); err != nil { |
|
74 |
- logrus.Debugf("Client Update (Targets): %s", err.Error()) |
|
75 |
- return err |
|
76 |
- } |
|
77 |
- return nil |
|
78 |
-} |
|
79 |
- |
|
80 |
-// downloadRoot is responsible for downloading the root.json |
|
81 |
-func (c *Client) downloadRoot() error { |
|
82 |
- role := data.CanonicalRootRole |
|
83 |
- consistentInfo := c.newBuilder.GetConsistentInfo(role) |
|
84 |
- |
|
85 |
- // We can't read an exact size for the root metadata without risking getting stuck in the TUF update cycle |
|
86 |
- // since it's possible that downloading timestamp/snapshot metadata may fail due to a signature mismatch |
|
87 |
- if !consistentInfo.ChecksumKnown() { |
|
88 |
- logrus.Debugf("Loading root with no expected checksum") |
|
89 |
- |
|
90 |
- // get the cached root, if it exists, just for version checking |
|
91 |
- cachedRoot, _ := c.cache.GetMeta(role, -1) |
|
92 |
- // prefer to download a new root |
|
93 |
- _, remoteErr := c.tryLoadRemote(consistentInfo, cachedRoot) |
|
94 |
- return remoteErr |
|
95 |
- } |
|
96 |
- |
|
97 |
- _, err := c.tryLoadCacheThenRemote(consistentInfo) |
|
98 |
- return err |
|
99 |
-} |
|
100 |
- |
|
101 |
-// downloadTimestamp is responsible for downloading the timestamp.json |
|
102 |
-// Timestamps are special in that we ALWAYS attempt to download and only |
|
103 |
-// use cache if the download fails (and the cache is still valid). |
|
104 |
-func (c *Client) downloadTimestamp() error { |
|
105 |
- logrus.Debug("Loading timestamp...") |
|
106 |
- role := data.CanonicalTimestampRole |
|
107 |
- consistentInfo := c.newBuilder.GetConsistentInfo(role) |
|
108 |
- |
|
109 |
- // get the cached timestamp, if it exists |
|
110 |
- cachedTS, cachedErr := c.cache.GetMeta(role, notary.MaxTimestampSize) |
|
111 |
- // always get the remote timestamp, since it supercedes the local one |
|
112 |
- _, remoteErr := c.tryLoadRemote(consistentInfo, cachedTS) |
|
113 |
- |
|
114 |
- switch { |
|
115 |
- case remoteErr == nil: |
|
116 |
- return nil |
|
117 |
- case cachedErr == nil: |
|
118 |
- logrus.Debug(remoteErr.Error()) |
|
119 |
- logrus.Warn("Error while downloading remote metadata, using cached timestamp - this might not be the latest version available remotely") |
|
120 |
- |
|
121 |
- err := c.newBuilder.Load(role, cachedTS, 1, false) |
|
122 |
- if err == nil { |
|
123 |
- logrus.Debug("successfully verified cached timestamp") |
|
124 |
- } |
|
125 |
- return err |
|
126 |
- default: |
|
127 |
- logrus.Debug("no cached or remote timestamp available") |
|
128 |
- return remoteErr |
|
129 |
- } |
|
130 |
-} |
|
131 |
- |
|
132 |
-// downloadSnapshot is responsible for downloading the snapshot.json |
|
133 |
-func (c *Client) downloadSnapshot() error { |
|
134 |
- logrus.Debug("Loading snapshot...") |
|
135 |
- role := data.CanonicalSnapshotRole |
|
136 |
- consistentInfo := c.newBuilder.GetConsistentInfo(role) |
|
137 |
- |
|
138 |
- _, err := c.tryLoadCacheThenRemote(consistentInfo) |
|
139 |
- return err |
|
140 |
-} |
|
141 |
- |
|
142 |
-// downloadTargets downloads all targets and delegated targets for the repository. |
|
143 |
-// It uses a pre-order tree traversal as it's necessary to download parents first |
|
144 |
-// to obtain the keys to validate children. |
|
145 |
-func (c *Client) downloadTargets() error { |
|
146 |
- toDownload := []data.DelegationRole{{ |
|
147 |
- BaseRole: data.BaseRole{Name: data.CanonicalTargetsRole}, |
|
148 |
- Paths: []string{""}, |
|
149 |
- }} |
|
150 |
- for len(toDownload) > 0 { |
|
151 |
- role := toDownload[0] |
|
152 |
- toDownload = toDownload[1:] |
|
153 |
- |
|
154 |
- consistentInfo := c.newBuilder.GetConsistentInfo(role.Name) |
|
155 |
- if !consistentInfo.ChecksumKnown() { |
|
156 |
- logrus.Debugf("skipping %s because there is no checksum for it", role.Name) |
|
157 |
- continue |
|
158 |
- } |
|
159 |
- |
|
160 |
- children, err := c.getTargetsFile(role, consistentInfo) |
|
161 |
- if err != nil { |
|
162 |
- if _, ok := err.(data.ErrMissingMeta); ok && role.Name != data.CanonicalTargetsRole { |
|
163 |
- // if the role meta hasn't been published, |
|
164 |
- // that's ok, continue |
|
165 |
- continue |
|
166 |
- } |
|
167 |
- logrus.Debugf("Error getting %s: %s", role.Name, err) |
|
168 |
- return err |
|
169 |
- } |
|
170 |
- toDownload = append(children, toDownload...) |
|
171 |
- } |
|
172 |
- return nil |
|
173 |
-} |
|
174 |
- |
|
175 |
-func (c Client) getTargetsFile(role data.DelegationRole, ci tuf.ConsistentInfo) ([]data.DelegationRole, error) { |
|
176 |
- logrus.Debugf("Loading %s...", role.Name) |
|
177 |
- tgs := &data.SignedTargets{} |
|
178 |
- |
|
179 |
- raw, err := c.tryLoadCacheThenRemote(ci) |
|
180 |
- if err != nil { |
|
181 |
- return nil, err |
|
182 |
- } |
|
183 |
- |
|
184 |
- // we know it unmarshals because if `tryLoadCacheThenRemote` didn't fail, then |
|
185 |
- // the raw has already been loaded into the builder |
|
186 |
- json.Unmarshal(raw, tgs) |
|
187 |
- return tgs.GetValidDelegations(role), nil |
|
188 |
-} |
|
189 |
- |
|
190 |
-func (c *Client) tryLoadCacheThenRemote(consistentInfo tuf.ConsistentInfo) ([]byte, error) { |
|
191 |
- cachedTS, err := c.cache.GetMeta(consistentInfo.RoleName, consistentInfo.Length()) |
|
192 |
- if err != nil { |
|
193 |
- logrus.Debugf("no %s in cache, must download", consistentInfo.RoleName) |
|
194 |
- return c.tryLoadRemote(consistentInfo, nil) |
|
195 |
- } |
|
196 |
- |
|
197 |
- if err = c.newBuilder.Load(consistentInfo.RoleName, cachedTS, 1, false); err == nil { |
|
198 |
- logrus.Debugf("successfully verified cached %s", consistentInfo.RoleName) |
|
199 |
- return cachedTS, nil |
|
200 |
- } |
|
201 |
- |
|
202 |
- logrus.Debugf("cached %s is invalid (must download): %s", consistentInfo.RoleName, err) |
|
203 |
- return c.tryLoadRemote(consistentInfo, cachedTS) |
|
204 |
-} |
|
205 |
- |
|
206 |
-func (c *Client) tryLoadRemote(consistentInfo tuf.ConsistentInfo, old []byte) ([]byte, error) { |
|
207 |
- consistentName := consistentInfo.ConsistentName() |
|
208 |
- raw, err := c.remote.GetMeta(consistentName, consistentInfo.Length()) |
|
209 |
- if err != nil { |
|
210 |
- logrus.Debugf("error downloading %s: %s", consistentName, err) |
|
211 |
- return old, err |
|
212 |
- } |
|
213 |
- |
|
214 |
- // try to load the old data into the old builder - only use it to validate |
|
215 |
- // versions if it loads successfully. If it errors, then the loaded version |
|
216 |
- // will be 1 |
|
217 |
- c.oldBuilder.Load(consistentInfo.RoleName, old, 1, true) |
|
218 |
- minVersion := c.oldBuilder.GetLoadedVersion(consistentInfo.RoleName) |
|
219 |
- |
|
220 |
- if err := c.newBuilder.Load(consistentInfo.RoleName, raw, minVersion, false); err != nil { |
|
221 |
- logrus.Debugf("downloaded %s is invalid: %s", consistentName, err) |
|
222 |
- return raw, err |
|
223 |
- } |
|
224 |
- logrus.Debugf("successfully verified downloaded %s", consistentName) |
|
225 |
- if err := c.cache.SetMeta(consistentInfo.RoleName, raw); err != nil { |
|
226 |
- logrus.Debugf("Unable to write %s to cache: %s", consistentInfo.RoleName, err) |
|
227 |
- } |
|
228 |
- return raw, nil |
|
229 |
-} |
230 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,14 +0,0 @@ |
1 |
-package client |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "fmt" |
|
5 |
-) |
|
6 |
- |
|
7 |
-// ErrCorruptedCache - local data is incorrect |
|
8 |
-type ErrCorruptedCache struct { |
|
9 |
- file string |
|
10 |
-} |
|
11 |
- |
|
12 |
-func (e ErrCorruptedCache) Error() string { |
|
13 |
- return fmt.Sprintf("cache is corrupted: %s", e.file) |
|
14 |
-} |
... | ... |
@@ -42,3 +42,12 @@ func (e ErrMismatchedChecksum) Error() string { |
42 | 42 |
return fmt.Sprintf("%s checksum for %s did not match: expected %s", e.alg, e.name, |
43 | 43 |
e.expected) |
44 | 44 |
} |
45 |
+ |
|
46 |
+// ErrCertExpired is the error to be returned when a certificate has expired |
|
47 |
+type ErrCertExpired struct { |
|
48 |
+ CN string |
|
49 |
+} |
|
50 |
+ |
|
51 |
+func (e ErrCertExpired) Error() string { |
|
52 |
+ return fmt.Sprintf("certificate with CN %s is expired", e.CN) |
|
53 |
+} |
... | ... |
@@ -86,6 +86,31 @@ func IsDelegation(role string) bool { |
86 | 86 |
isClean |
87 | 87 |
} |
88 | 88 |
|
89 |
+// IsBaseRole checks if the role is a base role |
|
90 |
+func IsBaseRole(role string) bool { |
|
91 |
+ for _, baseRole := range BaseRoles { |
|
92 |
+ if role == baseRole { |
|
93 |
+ return true |
|
94 |
+ } |
|
95 |
+ } |
|
96 |
+ return false |
|
97 |
+} |
|
98 |
+ |
|
99 |
+// IsWildDelegation determines if a role represents a valid wildcard delegation |
|
100 |
+// path, i.e. targets/*, targets/foo/*. |
|
101 |
+// The wildcard may only appear as the final part of the delegation and must |
|
102 |
+// be a whole segment, i.e. targets/foo* is not a valid wildcard delegation. |
|
103 |
+func IsWildDelegation(role string) bool { |
|
104 |
+ if path.Clean(role) != role { |
|
105 |
+ return false |
|
106 |
+ } |
|
107 |
+ base := path.Dir(role) |
|
108 |
+ if !(IsDelegation(base) || base == CanonicalTargetsRole) { |
|
109 |
+ return false |
|
110 |
+ } |
|
111 |
+ return role[len(role)-2:] == "/*" |
|
112 |
+} |
|
113 |
+ |
|
89 | 114 |
// BaseRole is an internal representation of a root/targets/snapshot/timestamp role, with its public keys included |
90 | 115 |
type BaseRole struct { |
91 | 116 |
Keys map[string]PublicKey |
... | ... |
@@ -107,7 +107,10 @@ func (t *SignedTargets) BuildDelegationRole(roleName string) (DelegationRole, er |
107 | 107 |
pubKey, ok := t.Signed.Delegations.Keys[keyID] |
108 | 108 |
if !ok { |
109 | 109 |
// Couldn't retrieve all keys, so stop walking and return invalid role |
110 |
- return DelegationRole{}, ErrInvalidRole{Role: roleName, Reason: "delegation does not exist with all specified keys"} |
|
110 |
+ return DelegationRole{}, ErrInvalidRole{ |
|
111 |
+ Role: roleName, |
|
112 |
+ Reason: "role lists unknown key " + keyID + " as a signing key", |
|
113 |
+ } |
|
111 | 114 |
} |
112 | 115 |
pubKeys[keyID] = pubKey |
113 | 116 |
} |
... | ... |
@@ -111,6 +111,7 @@ type Signature struct { |
111 | 111 |
KeyID string `json:"keyid"` |
112 | 112 |
Method SigAlgorithm `json:"method"` |
113 | 113 |
Signature []byte `json:"sig"` |
114 |
+ IsValid bool `json:"-"` |
|
114 | 115 |
} |
115 | 116 |
|
116 | 117 |
// Files is the map of paths to file meta container in targets and delegations |
... | ... |
@@ -161,6 +162,40 @@ func CheckHashes(payload []byte, name string, hashes Hashes) error { |
161 | 161 |
return nil |
162 | 162 |
} |
163 | 163 |
|
164 |
+// CompareMultiHashes verifies that the two Hashes passed in can represent the same data. |
|
165 |
+// This means that both maps must have at least one key defined for which they map, and no conflicts. |
|
166 |
+// Note that we check the intersection of map keys, which adds support for non-default hash algorithms in notary |
|
167 |
+func CompareMultiHashes(hashes1, hashes2 Hashes) error { |
|
168 |
+ // First check if the two hash structures are valid |
|
169 |
+ if err := CheckValidHashStructures(hashes1); err != nil { |
|
170 |
+ return err |
|
171 |
+ } |
|
172 |
+ if err := CheckValidHashStructures(hashes2); err != nil { |
|
173 |
+ return err |
|
174 |
+ } |
|
175 |
+ // Check if they have at least one matching hash, and no conflicts |
|
176 |
+ cnt := 0 |
|
177 |
+ for hashAlg, hash1 := range hashes1 { |
|
178 |
+ |
|
179 |
+ hash2, ok := hashes2[hashAlg] |
|
180 |
+ if !ok { |
|
181 |
+ continue |
|
182 |
+ } |
|
183 |
+ |
|
184 |
+ if subtle.ConstantTimeCompare(hash1[:], hash2[:]) == 0 { |
|
185 |
+ return fmt.Errorf("mismatched %s checksum", hashAlg) |
|
186 |
+ } |
|
187 |
+ // If we reached here, we had a match |
|
188 |
+ cnt++ |
|
189 |
+ } |
|
190 |
+ |
|
191 |
+ if cnt == 0 { |
|
192 |
+ return fmt.Errorf("at least one matching hash needed") |
|
193 |
+ } |
|
194 |
+ |
|
195 |
+ return nil |
|
196 |
+} |
|
197 |
+ |
|
164 | 198 |
// CheckValidHashStructures returns an error, or nil, depending on whether |
165 | 199 |
// the content of the hashes is valid or not. |
166 | 200 |
func CheckValidHashStructures(hashes Hashes) error { |
... | ... |
@@ -6,6 +6,7 @@ import ( |
6 | 6 |
|
7 | 7 |
"github.com/docker/notary/trustmanager" |
8 | 8 |
"github.com/docker/notary/tuf/data" |
9 |
+ "github.com/docker/notary/tuf/utils" |
|
9 | 10 |
) |
10 | 11 |
|
11 | 12 |
type edCryptoKey struct { |
... | ... |
@@ -72,7 +73,7 @@ func (e *Ed25519) Create(role, gun, algorithm string) (data.PublicKey, error) { |
72 | 72 |
return nil, errors.New("only ED25519 supported by this cryptoservice") |
73 | 73 |
} |
74 | 74 |
|
75 |
- private, err := trustmanager.GenerateED25519Key(rand.Reader) |
|
75 |
+ private, err := utils.GenerateED25519Key(rand.Reader) |
|
76 | 76 |
if err != nil { |
77 | 77 |
return nil, err |
78 | 78 |
} |
... | ... |
@@ -95,7 +96,10 @@ func (e *Ed25519) PublicKeys(keyIDs ...string) (map[string]data.PublicKey, error |
95 | 95 |
|
96 | 96 |
// GetKey returns a single public key based on the ID |
97 | 97 |
func (e *Ed25519) GetKey(keyID string) data.PublicKey { |
98 |
- return data.PublicKeyFromPrivate(e.keys[keyID].privKey) |
|
98 |
+ if privKey, _, err := e.GetPrivateKey(keyID); err == nil { |
|
99 |
+ return data.PublicKeyFromPrivate(privKey) |
|
100 |
+ } |
|
101 |
+ return nil |
|
99 | 102 |
} |
100 | 103 |
|
101 | 104 |
// GetPrivateKey returns a single private key and role if present, based on the ID |
... | ... |
@@ -14,12 +14,17 @@ type ErrInsufficientSignatures struct { |
14 | 14 |
} |
15 | 15 |
|
16 | 16 |
func (e ErrInsufficientSignatures) Error() string { |
17 |
- candidates := strings.Join(e.MissingKeyIDs, ", ") |
|
17 |
+ candidates := "" |
|
18 |
+ if len(e.MissingKeyIDs) > 0 { |
|
19 |
+ candidates = fmt.Sprintf(" (%s)", strings.Join(e.MissingKeyIDs, ", ")) |
|
20 |
+ } |
|
21 |
+ |
|
18 | 22 |
if e.FoundKeys == 0 { |
19 |
- return fmt.Sprintf("signing keys not available, need %d keys out of: %s", e.NeededKeys, candidates) |
|
23 |
+ return fmt.Sprintf("signing keys not available: need %d keys from %d possible keys%s", |
|
24 |
+ e.NeededKeys, len(e.MissingKeyIDs), candidates) |
|
20 | 25 |
} |
21 |
- return fmt.Sprintf("not enough signing keys: got %d of %d needed keys, other candidates: %s", |
|
22 |
- e.FoundKeys, e.NeededKeys, candidates) |
|
26 |
+ return fmt.Sprintf("not enough signing keys: found %d of %d needed keys - %d other possible keys%s", |
|
27 |
+ e.FoundKeys, e.NeededKeys, len(e.MissingKeyIDs), candidates) |
|
23 | 28 |
} |
24 | 29 |
|
25 | 30 |
// ErrExpired indicates a piece of metadata has expired |
... | ... |
@@ -100,7 +100,7 @@ func Sign(service CryptoService, s *data.Signed, signingKeys []data.PublicKey, |
100 | 100 |
// key is no longer a valid signing key |
101 | 101 |
continue |
102 | 102 |
} |
103 |
- if err := VerifySignature(*s.Signed, sig, k); err != nil { |
|
103 |
+ if err := VerifySignature(*s.Signed, &sig, k); err != nil { |
|
104 | 104 |
// signature is no longer valid |
105 | 105 |
continue |
106 | 106 |
} |
... | ... |
@@ -66,7 +66,8 @@ func VerifySignatures(s *data.Signed, roleData data.BaseRole) error { |
66 | 66 |
} |
67 | 67 |
|
68 | 68 |
valid := make(map[string]struct{}) |
69 |
- for _, sig := range s.Signatures { |
|
69 |
+ for i := range s.Signatures { |
|
70 |
+ sig := &(s.Signatures[i]) |
|
70 | 71 |
logrus.Debug("verifying signature for key ID: ", sig.KeyID) |
71 | 72 |
key, ok := roleData.Keys[sig.KeyID] |
72 | 73 |
if !ok { |
... | ... |
@@ -82,17 +83,20 @@ func VerifySignatures(s *data.Signed, roleData data.BaseRole) error { |
82 | 82 |
continue |
83 | 83 |
} |
84 | 84 |
valid[sig.KeyID] = struct{}{} |
85 |
- |
|
86 | 85 |
} |
87 | 86 |
if len(valid) < roleData.Threshold { |
88 |
- return ErrRoleThreshold{} |
|
87 |
+ return ErrRoleThreshold{ |
|
88 |
+ Msg: fmt.Sprintf("valid signatures did not meet threshold for %s", roleData.Name), |
|
89 |
+ } |
|
89 | 90 |
} |
90 | 91 |
|
91 | 92 |
return nil |
92 | 93 |
} |
93 | 94 |
|
94 | 95 |
// VerifySignature checks a single signature and public key against a payload |
95 |
-func VerifySignature(msg []byte, sig data.Signature, pk data.PublicKey) error { |
|
96 |
+// If the signature is verified, the signature's is valid field will actually |
|
97 |
+// be mutated to be equal to the boolean true |
|
98 |
+func VerifySignature(msg []byte, sig *data.Signature, pk data.PublicKey) error { |
|
96 | 99 |
// method lookup is consistent due to Unmarshal JSON doing lower case for us. |
97 | 100 |
method := sig.Method |
98 | 101 |
verifier, ok := Verifiers[method] |
... | ... |
@@ -103,5 +107,6 @@ func VerifySignature(msg []byte, sig data.Signature, pk data.PublicKey) error { |
103 | 103 |
if err := verifier.Verify(pk, sig.Signature, msg); err != nil { |
104 | 104 |
return fmt.Errorf("signature was invalid\n") |
105 | 105 |
} |
106 |
+ sig.IsValid = true |
|
106 | 107 |
return nil |
107 | 108 |
} |
108 | 109 |
deleted file mode 100644 |
... | ... |
@@ -1,13 +0,0 @@ |
1 |
-package store |
|
2 |
- |
|
3 |
-import "fmt" |
|
4 |
- |
|
5 |
-// ErrMetaNotFound indicates we did not find a particular piece |
|
6 |
-// of metadata in the store |
|
7 |
-type ErrMetaNotFound struct { |
|
8 |
- Resource string |
|
9 |
-} |
|
10 |
- |
|
11 |
-func (err ErrMetaNotFound) Error() string { |
|
12 |
- return fmt.Sprintf("%s trust data unavailable. Has a notary repository been initialized?", err.Resource) |
|
13 |
-} |
14 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,102 +0,0 @@ |
1 |
-package store |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "fmt" |
|
5 |
- "github.com/docker/notary" |
|
6 |
- "io/ioutil" |
|
7 |
- "os" |
|
8 |
- "path" |
|
9 |
- "path/filepath" |
|
10 |
-) |
|
11 |
- |
|
12 |
-// NewFilesystemStore creates a new store in a directory tree |
|
13 |
-func NewFilesystemStore(baseDir, metaSubDir, metaExtension string) (*FilesystemStore, error) { |
|
14 |
- metaDir := path.Join(baseDir, metaSubDir) |
|
15 |
- |
|
16 |
- // Make sure we can create the necessary dirs and they are writable |
|
17 |
- err := os.MkdirAll(metaDir, 0700) |
|
18 |
- if err != nil { |
|
19 |
- return nil, err |
|
20 |
- } |
|
21 |
- |
|
22 |
- return &FilesystemStore{ |
|
23 |
- baseDir: baseDir, |
|
24 |
- metaDir: metaDir, |
|
25 |
- metaExtension: metaExtension, |
|
26 |
- }, nil |
|
27 |
-} |
|
28 |
- |
|
29 |
-// FilesystemStore is a store in a locally accessible directory |
|
30 |
-type FilesystemStore struct { |
|
31 |
- baseDir string |
|
32 |
- metaDir string |
|
33 |
- metaExtension string |
|
34 |
-} |
|
35 |
- |
|
36 |
-func (f *FilesystemStore) getPath(name string) string { |
|
37 |
- fileName := fmt.Sprintf("%s.%s", name, f.metaExtension) |
|
38 |
- return filepath.Join(f.metaDir, fileName) |
|
39 |
-} |
|
40 |
- |
|
41 |
-// GetMeta returns the meta for the given name (a role) up to size bytes |
|
42 |
-// If size is "NoSizeLimit", this corresponds to "infinite," but we cut off at a |
|
43 |
-// predefined threshold "notary.MaxDownloadSize". |
|
44 |
-func (f *FilesystemStore) GetMeta(name string, size int64) ([]byte, error) { |
|
45 |
- meta, err := ioutil.ReadFile(f.getPath(name)) |
|
46 |
- if err != nil { |
|
47 |
- if os.IsNotExist(err) { |
|
48 |
- err = ErrMetaNotFound{Resource: name} |
|
49 |
- } |
|
50 |
- return nil, err |
|
51 |
- } |
|
52 |
- if size == NoSizeLimit { |
|
53 |
- size = notary.MaxDownloadSize |
|
54 |
- } |
|
55 |
- // Only return up to size bytes |
|
56 |
- if int64(len(meta)) < size { |
|
57 |
- return meta, nil |
|
58 |
- } |
|
59 |
- return meta[:size], nil |
|
60 |
-} |
|
61 |
- |
|
62 |
-// SetMultiMeta sets the metadata for multiple roles in one operation |
|
63 |
-func (f *FilesystemStore) SetMultiMeta(metas map[string][]byte) error { |
|
64 |
- for role, blob := range metas { |
|
65 |
- err := f.SetMeta(role, blob) |
|
66 |
- if err != nil { |
|
67 |
- return err |
|
68 |
- } |
|
69 |
- } |
|
70 |
- return nil |
|
71 |
-} |
|
72 |
- |
|
73 |
-// SetMeta sets the meta for a single role |
|
74 |
-func (f *FilesystemStore) SetMeta(name string, meta []byte) error { |
|
75 |
- fp := f.getPath(name) |
|
76 |
- |
|
77 |
- // Ensures the parent directories of the file we are about to write exist |
|
78 |
- err := os.MkdirAll(filepath.Dir(fp), 0700) |
|
79 |
- if err != nil { |
|
80 |
- return err |
|
81 |
- } |
|
82 |
- |
|
83 |
- // if something already exists, just delete it and re-write it |
|
84 |
- os.RemoveAll(fp) |
|
85 |
- |
|
86 |
- // Write the file to disk |
|
87 |
- if err = ioutil.WriteFile(fp, meta, 0600); err != nil { |
|
88 |
- return err |
|
89 |
- } |
|
90 |
- return nil |
|
91 |
-} |
|
92 |
- |
|
93 |
-// RemoveAll clears the existing filestore by removing its base directory |
|
94 |
-func (f *FilesystemStore) RemoveAll() error { |
|
95 |
- return os.RemoveAll(f.baseDir) |
|
96 |
-} |
|
97 |
- |
|
98 |
-// RemoveMeta removes the metadata for a single role - if the metadata doesn't |
|
99 |
-// exist, no error is returned |
|
100 |
-func (f *FilesystemStore) RemoveMeta(name string) error { |
|
101 |
- return os.RemoveAll(f.getPath(name)) // RemoveAll succeeds if path doesn't exist |
|
102 |
-} |
103 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,297 +0,0 @@ |
1 |
-// A Store that can fetch and set metadata on a remote server. |
|
2 |
-// Some API constraints: |
|
3 |
-// - Response bodies for error codes should be unmarshallable as: |
|
4 |
-// {"errors": [{..., "detail": <serialized validation error>}]} |
|
5 |
-// else validation error details, etc. will be unparsable. The errors |
|
6 |
-// should have a github.com/docker/notary/tuf/validation/SerializableError |
|
7 |
-// in the Details field. |
|
8 |
-// If writing your own server, please have a look at |
|
9 |
-// github.com/docker/distribution/registry/api/errcode |
|
10 |
- |
|
11 |
-package store |
|
12 |
- |
|
13 |
-import ( |
|
14 |
- "bytes" |
|
15 |
- "encoding/json" |
|
16 |
- "errors" |
|
17 |
- "fmt" |
|
18 |
- "io" |
|
19 |
- "io/ioutil" |
|
20 |
- "mime/multipart" |
|
21 |
- "net/http" |
|
22 |
- "net/url" |
|
23 |
- "path" |
|
24 |
- |
|
25 |
- "github.com/Sirupsen/logrus" |
|
26 |
- "github.com/docker/notary" |
|
27 |
- "github.com/docker/notary/tuf/validation" |
|
28 |
-) |
|
29 |
- |
|
30 |
-// ErrServerUnavailable indicates an error from the server. code allows us to |
|
31 |
-// populate the http error we received |
|
32 |
-type ErrServerUnavailable struct { |
|
33 |
- code int |
|
34 |
-} |
|
35 |
- |
|
36 |
-func (err ErrServerUnavailable) Error() string { |
|
37 |
- if err.code == 401 { |
|
38 |
- return fmt.Sprintf("you are not authorized to perform this operation: server returned 401.") |
|
39 |
- } |
|
40 |
- return fmt.Sprintf("unable to reach trust server at this time: %d.", err.code) |
|
41 |
-} |
|
42 |
- |
|
43 |
-// ErrMaliciousServer indicates the server returned a response that is highly suspected |
|
44 |
-// of being malicious. i.e. it attempted to send us more data than the known size of a |
|
45 |
-// particular role metadata. |
|
46 |
-type ErrMaliciousServer struct{} |
|
47 |
- |
|
48 |
-func (err ErrMaliciousServer) Error() string { |
|
49 |
- return "trust server returned a bad response." |
|
50 |
-} |
|
51 |
- |
|
52 |
-// ErrInvalidOperation indicates that the server returned a 400 response and |
|
53 |
-// propagate any body we received. |
|
54 |
-type ErrInvalidOperation struct { |
|
55 |
- msg string |
|
56 |
-} |
|
57 |
- |
|
58 |
-func (err ErrInvalidOperation) Error() string { |
|
59 |
- if err.msg != "" { |
|
60 |
- return fmt.Sprintf("trust server rejected operation: %s", err.msg) |
|
61 |
- } |
|
62 |
- return "trust server rejected operation." |
|
63 |
-} |
|
64 |
- |
|
65 |
-// HTTPStore manages pulling and pushing metadata from and to a remote |
|
66 |
-// service over HTTP. It assumes the URL structure of the remote service |
|
67 |
-// maps identically to the structure of the TUF repo: |
|
68 |
-// <baseURL>/<metaPrefix>/(root|targets|snapshot|timestamp).json |
|
69 |
-// <baseURL>/<targetsPrefix>/foo.sh |
|
70 |
-// |
|
71 |
-// If consistent snapshots are disabled, it is advised that caching is not |
|
72 |
-// enabled. Simple set a cachePath (and ensure it's writeable) to enable |
|
73 |
-// caching. |
|
74 |
-type HTTPStore struct { |
|
75 |
- baseURL url.URL |
|
76 |
- metaPrefix string |
|
77 |
- metaExtension string |
|
78 |
- keyExtension string |
|
79 |
- roundTrip http.RoundTripper |
|
80 |
-} |
|
81 |
- |
|
82 |
-// NewHTTPStore initializes a new store against a URL and a number of configuration options |
|
83 |
-func NewHTTPStore(baseURL, metaPrefix, metaExtension, keyExtension string, roundTrip http.RoundTripper) (RemoteStore, error) { |
|
84 |
- base, err := url.Parse(baseURL) |
|
85 |
- if err != nil { |
|
86 |
- return nil, err |
|
87 |
- } |
|
88 |
- if !base.IsAbs() { |
|
89 |
- return nil, errors.New("HTTPStore requires an absolute baseURL") |
|
90 |
- } |
|
91 |
- if roundTrip == nil { |
|
92 |
- return &OfflineStore{}, nil |
|
93 |
- } |
|
94 |
- return &HTTPStore{ |
|
95 |
- baseURL: *base, |
|
96 |
- metaPrefix: metaPrefix, |
|
97 |
- metaExtension: metaExtension, |
|
98 |
- keyExtension: keyExtension, |
|
99 |
- roundTrip: roundTrip, |
|
100 |
- }, nil |
|
101 |
-} |
|
102 |
- |
|
103 |
-func tryUnmarshalError(resp *http.Response, defaultError error) error { |
|
104 |
- bodyBytes, err := ioutil.ReadAll(resp.Body) |
|
105 |
- if err != nil { |
|
106 |
- return defaultError |
|
107 |
- } |
|
108 |
- var parsedErrors struct { |
|
109 |
- Errors []struct { |
|
110 |
- Detail validation.SerializableError `json:"detail"` |
|
111 |
- } `json:"errors"` |
|
112 |
- } |
|
113 |
- if err := json.Unmarshal(bodyBytes, &parsedErrors); err != nil { |
|
114 |
- return defaultError |
|
115 |
- } |
|
116 |
- if len(parsedErrors.Errors) != 1 { |
|
117 |
- return defaultError |
|
118 |
- } |
|
119 |
- err = parsedErrors.Errors[0].Detail.Error |
|
120 |
- if err == nil { |
|
121 |
- return defaultError |
|
122 |
- } |
|
123 |
- return err |
|
124 |
-} |
|
125 |
- |
|
126 |
-func translateStatusToError(resp *http.Response, resource string) error { |
|
127 |
- switch resp.StatusCode { |
|
128 |
- case http.StatusOK: |
|
129 |
- return nil |
|
130 |
- case http.StatusNotFound: |
|
131 |
- return ErrMetaNotFound{Resource: resource} |
|
132 |
- case http.StatusBadRequest: |
|
133 |
- return tryUnmarshalError(resp, ErrInvalidOperation{}) |
|
134 |
- default: |
|
135 |
- return ErrServerUnavailable{code: resp.StatusCode} |
|
136 |
- } |
|
137 |
-} |
|
138 |
- |
|
139 |
-// GetMeta downloads the named meta file with the given size. A short body |
|
140 |
-// is acceptable because in the case of timestamp.json, the size is a cap, |
|
141 |
-// not an exact length. |
|
142 |
-// If size is "NoSizeLimit", this corresponds to "infinite," but we cut off at a |
|
143 |
-// predefined threshold "notary.MaxDownloadSize". |
|
144 |
-func (s HTTPStore) GetMeta(name string, size int64) ([]byte, error) { |
|
145 |
- url, err := s.buildMetaURL(name) |
|
146 |
- if err != nil { |
|
147 |
- return nil, err |
|
148 |
- } |
|
149 |
- req, err := http.NewRequest("GET", url.String(), nil) |
|
150 |
- if err != nil { |
|
151 |
- return nil, err |
|
152 |
- } |
|
153 |
- resp, err := s.roundTrip.RoundTrip(req) |
|
154 |
- if err != nil { |
|
155 |
- return nil, err |
|
156 |
- } |
|
157 |
- defer resp.Body.Close() |
|
158 |
- if err := translateStatusToError(resp, name); err != nil { |
|
159 |
- logrus.Debugf("received HTTP status %d when requesting %s.", resp.StatusCode, name) |
|
160 |
- return nil, err |
|
161 |
- } |
|
162 |
- if size == NoSizeLimit { |
|
163 |
- size = notary.MaxDownloadSize |
|
164 |
- } |
|
165 |
- if resp.ContentLength > size { |
|
166 |
- return nil, ErrMaliciousServer{} |
|
167 |
- } |
|
168 |
- logrus.Debugf("%d when retrieving metadata for %s", resp.StatusCode, name) |
|
169 |
- b := io.LimitReader(resp.Body, size) |
|
170 |
- body, err := ioutil.ReadAll(b) |
|
171 |
- if err != nil { |
|
172 |
- return nil, err |
|
173 |
- } |
|
174 |
- return body, nil |
|
175 |
-} |
|
176 |
- |
|
177 |
-// SetMeta uploads a piece of TUF metadata to the server |
|
178 |
-func (s HTTPStore) SetMeta(name string, blob []byte) error { |
|
179 |
- url, err := s.buildMetaURL("") |
|
180 |
- if err != nil { |
|
181 |
- return err |
|
182 |
- } |
|
183 |
- req, err := http.NewRequest("POST", url.String(), bytes.NewReader(blob)) |
|
184 |
- if err != nil { |
|
185 |
- return err |
|
186 |
- } |
|
187 |
- resp, err := s.roundTrip.RoundTrip(req) |
|
188 |
- if err != nil { |
|
189 |
- return err |
|
190 |
- } |
|
191 |
- defer resp.Body.Close() |
|
192 |
- return translateStatusToError(resp, "POST "+name) |
|
193 |
-} |
|
194 |
- |
|
195 |
-// RemoveMeta always fails, because we should never be able to delete metadata |
|
196 |
-// remotely |
|
197 |
-func (s HTTPStore) RemoveMeta(name string) error { |
|
198 |
- return ErrInvalidOperation{msg: "cannot delete metadata"} |
|
199 |
-} |
|
200 |
- |
|
201 |
-// NewMultiPartMetaRequest builds a request with the provided metadata updates |
|
202 |
-// in multipart form |
|
203 |
-func NewMultiPartMetaRequest(url string, metas map[string][]byte) (*http.Request, error) { |
|
204 |
- body := &bytes.Buffer{} |
|
205 |
- writer := multipart.NewWriter(body) |
|
206 |
- for role, blob := range metas { |
|
207 |
- part, err := writer.CreateFormFile("files", role) |
|
208 |
- _, err = io.Copy(part, bytes.NewBuffer(blob)) |
|
209 |
- if err != nil { |
|
210 |
- return nil, err |
|
211 |
- } |
|
212 |
- } |
|
213 |
- err := writer.Close() |
|
214 |
- if err != nil { |
|
215 |
- return nil, err |
|
216 |
- } |
|
217 |
- req, err := http.NewRequest("POST", url, body) |
|
218 |
- if err != nil { |
|
219 |
- return nil, err |
|
220 |
- } |
|
221 |
- req.Header.Set("Content-Type", writer.FormDataContentType()) |
|
222 |
- return req, nil |
|
223 |
-} |
|
224 |
- |
|
225 |
-// SetMultiMeta does a single batch upload of multiple pieces of TUF metadata. |
|
226 |
-// This should be preferred for updating a remote server as it enable the server |
|
227 |
-// to remain consistent, either accepting or rejecting the complete update. |
|
228 |
-func (s HTTPStore) SetMultiMeta(metas map[string][]byte) error { |
|
229 |
- url, err := s.buildMetaURL("") |
|
230 |
- if err != nil { |
|
231 |
- return err |
|
232 |
- } |
|
233 |
- req, err := NewMultiPartMetaRequest(url.String(), metas) |
|
234 |
- if err != nil { |
|
235 |
- return err |
|
236 |
- } |
|
237 |
- resp, err := s.roundTrip.RoundTrip(req) |
|
238 |
- if err != nil { |
|
239 |
- return err |
|
240 |
- } |
|
241 |
- defer resp.Body.Close() |
|
242 |
- // if this 404's something is pretty wrong |
|
243 |
- return translateStatusToError(resp, "POST metadata endpoint") |
|
244 |
-} |
|
245 |
- |
|
246 |
-// RemoveAll in the interface is not supported, admins should use the DeleteHandler endpoint directly to delete remote data for a GUN |
|
247 |
-func (s HTTPStore) RemoveAll() error { |
|
248 |
- return errors.New("remove all functionality not supported for HTTPStore") |
|
249 |
-} |
|
250 |
- |
|
251 |
-func (s HTTPStore) buildMetaURL(name string) (*url.URL, error) { |
|
252 |
- var filename string |
|
253 |
- if name != "" { |
|
254 |
- filename = fmt.Sprintf("%s.%s", name, s.metaExtension) |
|
255 |
- } |
|
256 |
- uri := path.Join(s.metaPrefix, filename) |
|
257 |
- return s.buildURL(uri) |
|
258 |
-} |
|
259 |
- |
|
260 |
-func (s HTTPStore) buildKeyURL(name string) (*url.URL, error) { |
|
261 |
- filename := fmt.Sprintf("%s.%s", name, s.keyExtension) |
|
262 |
- uri := path.Join(s.metaPrefix, filename) |
|
263 |
- return s.buildURL(uri) |
|
264 |
-} |
|
265 |
- |
|
266 |
-func (s HTTPStore) buildURL(uri string) (*url.URL, error) { |
|
267 |
- sub, err := url.Parse(uri) |
|
268 |
- if err != nil { |
|
269 |
- return nil, err |
|
270 |
- } |
|
271 |
- return s.baseURL.ResolveReference(sub), nil |
|
272 |
-} |
|
273 |
- |
|
274 |
-// GetKey retrieves a public key from the remote server |
|
275 |
-func (s HTTPStore) GetKey(role string) ([]byte, error) { |
|
276 |
- url, err := s.buildKeyURL(role) |
|
277 |
- if err != nil { |
|
278 |
- return nil, err |
|
279 |
- } |
|
280 |
- req, err := http.NewRequest("GET", url.String(), nil) |
|
281 |
- if err != nil { |
|
282 |
- return nil, err |
|
283 |
- } |
|
284 |
- resp, err := s.roundTrip.RoundTrip(req) |
|
285 |
- if err != nil { |
|
286 |
- return nil, err |
|
287 |
- } |
|
288 |
- defer resp.Body.Close() |
|
289 |
- if err := translateStatusToError(resp, role+" key"); err != nil { |
|
290 |
- return nil, err |
|
291 |
- } |
|
292 |
- body, err := ioutil.ReadAll(resp.Body) |
|
293 |
- if err != nil { |
|
294 |
- return nil, err |
|
295 |
- } |
|
296 |
- return body, nil |
|
297 |
-} |
298 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,31 +0,0 @@ |
1 |
-package store |
|
2 |
- |
|
3 |
-// NoSizeLimit is represented as -1 for arguments to GetMeta |
|
4 |
-const NoSizeLimit int64 = -1 |
|
5 |
- |
|
6 |
-// MetadataStore must be implemented by anything that intends to interact |
|
7 |
-// with a store of TUF files |
|
8 |
-type MetadataStore interface { |
|
9 |
- GetMeta(name string, size int64) ([]byte, error) |
|
10 |
- SetMeta(name string, blob []byte) error |
|
11 |
- SetMultiMeta(map[string][]byte) error |
|
12 |
- RemoveAll() error |
|
13 |
- RemoveMeta(name string) error |
|
14 |
-} |
|
15 |
- |
|
16 |
-// PublicKeyStore must be implemented by a key service |
|
17 |
-type PublicKeyStore interface { |
|
18 |
- GetKey(role string) ([]byte, error) |
|
19 |
-} |
|
20 |
- |
|
21 |
-// LocalStore represents a local TUF sture |
|
22 |
-type LocalStore interface { |
|
23 |
- MetadataStore |
|
24 |
-} |
|
25 |
- |
|
26 |
-// RemoteStore is similar to LocalStore with the added expectation that it should |
|
27 |
-// provide a way to download targets once located |
|
28 |
-type RemoteStore interface { |
|
29 |
- MetadataStore |
|
30 |
- PublicKeyStore |
|
31 |
-} |
32 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,107 +0,0 @@ |
1 |
-package store |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "crypto/sha256" |
|
5 |
- "fmt" |
|
6 |
- |
|
7 |
- "github.com/docker/notary" |
|
8 |
- "github.com/docker/notary/tuf/data" |
|
9 |
- "github.com/docker/notary/tuf/utils" |
|
10 |
-) |
|
11 |
- |
|
12 |
-// NewMemoryStore returns a MetadataStore that operates entirely in memory. |
|
13 |
-// Very useful for testing |
|
14 |
-func NewMemoryStore(meta map[string][]byte) *MemoryStore { |
|
15 |
- var consistent = make(map[string][]byte) |
|
16 |
- if meta == nil { |
|
17 |
- meta = make(map[string][]byte) |
|
18 |
- } else { |
|
19 |
- // add all seed meta to consistent |
|
20 |
- for name, data := range meta { |
|
21 |
- checksum := sha256.Sum256(data) |
|
22 |
- path := utils.ConsistentName(name, checksum[:]) |
|
23 |
- consistent[path] = data |
|
24 |
- } |
|
25 |
- } |
|
26 |
- return &MemoryStore{ |
|
27 |
- meta: meta, |
|
28 |
- consistent: consistent, |
|
29 |
- keys: make(map[string][]data.PrivateKey), |
|
30 |
- } |
|
31 |
-} |
|
32 |
- |
|
33 |
-// MemoryStore implements a mock RemoteStore entirely in memory. |
|
34 |
-// For testing purposes only. |
|
35 |
-type MemoryStore struct { |
|
36 |
- meta map[string][]byte |
|
37 |
- consistent map[string][]byte |
|
38 |
- keys map[string][]data.PrivateKey |
|
39 |
-} |
|
40 |
- |
|
41 |
-// GetMeta returns up to size bytes of data references by name. |
|
42 |
-// If size is "NoSizeLimit", this corresponds to "infinite," but we cut off at a |
|
43 |
-// predefined threshold "notary.MaxDownloadSize", as we will always know the |
|
44 |
-// size for everything but a timestamp and sometimes a root, |
|
45 |
-// neither of which should be exceptionally large |
|
46 |
-func (m *MemoryStore) GetMeta(name string, size int64) ([]byte, error) { |
|
47 |
- d, ok := m.meta[name] |
|
48 |
- if ok { |
|
49 |
- if size == NoSizeLimit { |
|
50 |
- size = notary.MaxDownloadSize |
|
51 |
- } |
|
52 |
- if int64(len(d)) < size { |
|
53 |
- return d, nil |
|
54 |
- } |
|
55 |
- return d[:size], nil |
|
56 |
- } |
|
57 |
- d, ok = m.consistent[name] |
|
58 |
- if ok { |
|
59 |
- if int64(len(d)) < size { |
|
60 |
- return d, nil |
|
61 |
- } |
|
62 |
- return d[:size], nil |
|
63 |
- } |
|
64 |
- return nil, ErrMetaNotFound{Resource: name} |
|
65 |
-} |
|
66 |
- |
|
67 |
-// SetMeta sets the metadata value for the given name |
|
68 |
-func (m *MemoryStore) SetMeta(name string, meta []byte) error { |
|
69 |
- m.meta[name] = meta |
|
70 |
- |
|
71 |
- checksum := sha256.Sum256(meta) |
|
72 |
- path := utils.ConsistentName(name, checksum[:]) |
|
73 |
- m.consistent[path] = meta |
|
74 |
- return nil |
|
75 |
-} |
|
76 |
- |
|
77 |
-// SetMultiMeta sets multiple pieces of metadata for multiple names |
|
78 |
-// in a single operation. |
|
79 |
-func (m *MemoryStore) SetMultiMeta(metas map[string][]byte) error { |
|
80 |
- for role, blob := range metas { |
|
81 |
- m.SetMeta(role, blob) |
|
82 |
- } |
|
83 |
- return nil |
|
84 |
-} |
|
85 |
- |
|
86 |
-// RemoveMeta removes the metadata for a single role - if the metadata doesn't |
|
87 |
-// exist, no error is returned |
|
88 |
-func (m *MemoryStore) RemoveMeta(name string) error { |
|
89 |
- if meta, ok := m.meta[name]; ok { |
|
90 |
- checksum := sha256.Sum256(meta) |
|
91 |
- path := utils.ConsistentName(name, checksum[:]) |
|
92 |
- delete(m.meta, name) |
|
93 |
- delete(m.consistent, path) |
|
94 |
- } |
|
95 |
- return nil |
|
96 |
-} |
|
97 |
- |
|
98 |
-// GetKey returns the public key for the given role |
|
99 |
-func (m *MemoryStore) GetKey(role string) ([]byte, error) { |
|
100 |
- return nil, fmt.Errorf("GetKey is not implemented for the MemoryStore") |
|
101 |
-} |
|
102 |
- |
|
103 |
-// RemoveAll clears the existing memory store by setting this store as new empty one |
|
104 |
-func (m *MemoryStore) RemoveAll() error { |
|
105 |
- *m = *NewMemoryStore(nil) |
|
106 |
- return nil |
|
107 |
-} |
108 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,53 +0,0 @@ |
1 |
-package store |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "io" |
|
5 |
-) |
|
6 |
- |
|
7 |
-// ErrOffline is used to indicate we are operating offline |
|
8 |
-type ErrOffline struct{} |
|
9 |
- |
|
10 |
-func (e ErrOffline) Error() string { |
|
11 |
- return "client is offline" |
|
12 |
-} |
|
13 |
- |
|
14 |
-var err = ErrOffline{} |
|
15 |
- |
|
16 |
-// OfflineStore is to be used as a placeholder for a nil store. It simply |
|
17 |
-// returns ErrOffline for every operation |
|
18 |
-type OfflineStore struct{} |
|
19 |
- |
|
20 |
-// GetMeta returns ErrOffline |
|
21 |
-func (es OfflineStore) GetMeta(name string, size int64) ([]byte, error) { |
|
22 |
- return nil, err |
|
23 |
-} |
|
24 |
- |
|
25 |
-// SetMeta returns ErrOffline |
|
26 |
-func (es OfflineStore) SetMeta(name string, blob []byte) error { |
|
27 |
- return err |
|
28 |
-} |
|
29 |
- |
|
30 |
-// SetMultiMeta returns ErrOffline |
|
31 |
-func (es OfflineStore) SetMultiMeta(map[string][]byte) error { |
|
32 |
- return err |
|
33 |
-} |
|
34 |
- |
|
35 |
-// RemoveMeta returns ErrOffline |
|
36 |
-func (es OfflineStore) RemoveMeta(name string) error { |
|
37 |
- return err |
|
38 |
-} |
|
39 |
- |
|
40 |
-// GetKey returns ErrOffline |
|
41 |
-func (es OfflineStore) GetKey(role string) ([]byte, error) { |
|
42 |
- return nil, err |
|
43 |
-} |
|
44 |
- |
|
45 |
-// GetTarget returns ErrOffline |
|
46 |
-func (es OfflineStore) GetTarget(path string) (io.ReadCloser, error) { |
|
47 |
- return nil, err |
|
48 |
-} |
|
49 |
- |
|
50 |
-// RemoveAll return ErrOffline |
|
51 |
-func (es OfflineStore) RemoveAll() error { |
|
52 |
- return err |
|
53 |
-} |
... | ... |
@@ -77,11 +77,10 @@ type Repo struct { |
77 | 77 |
// If the Repo will only be used for reading, the CryptoService |
78 | 78 |
// can be nil. |
79 | 79 |
func NewRepo(cryptoService signed.CryptoService) *Repo { |
80 |
- repo := &Repo{ |
|
80 |
+ return &Repo{ |
|
81 | 81 |
Targets: make(map[string]*data.SignedTargets), |
82 | 82 |
cryptoService: cryptoService, |
83 | 83 |
} |
84 |
- return repo |
|
85 | 84 |
} |
86 | 85 |
|
87 | 86 |
// AddBaseKeys is used to add keys to the role in root.json |
... | ... |
@@ -245,6 +244,21 @@ func (tr *Repo) GetDelegationRole(name string) (data.DelegationRole, error) { |
245 | 245 |
if err != nil { |
246 | 246 |
return err |
247 | 247 |
} |
248 |
+ // Check all public key certificates in the role for expiry |
|
249 |
+ // Currently we do not reject expired delegation keys but warn if they might expire soon or have already |
|
250 |
+ for keyID, pubKey := range delgRole.Keys { |
|
251 |
+ certFromKey, err := utils.LoadCertFromPEM(pubKey.Public()) |
|
252 |
+ if err != nil { |
|
253 |
+ continue |
|
254 |
+ } |
|
255 |
+ if err := utils.ValidateCertificate(certFromKey, true); err != nil { |
|
256 |
+ if _, ok := err.(data.ErrCertExpired); !ok { |
|
257 |
+ // do not allow other invalid cert errors |
|
258 |
+ return err |
|
259 |
+ } |
|
260 |
+ logrus.Warnf("error with delegation %s key ID %d: %s", delgRole.Name, keyID, err) |
|
261 |
+ } |
|
262 |
+ } |
|
248 | 263 |
foundRole = &delgRole |
249 | 264 |
return StopWalk{} |
250 | 265 |
} |
... | ... |
@@ -325,17 +339,16 @@ func delegationUpdateVisitor(roleName string, addKeys data.KeyList, removeKeys, |
325 | 325 |
break |
326 | 326 |
} |
327 | 327 |
} |
328 |
- // We didn't find the role earlier, so create it only if we have keys to add |
|
328 |
+ // We didn't find the role earlier, so create it. |
|
329 |
+ if addKeys == nil { |
|
330 |
+ addKeys = data.KeyList{} // initialize to empty list if necessary so calling .IDs() below won't panic |
|
331 |
+ } |
|
329 | 332 |
if delgRole == nil { |
330 |
- if len(addKeys) > 0 { |
|
331 |
- delgRole, err = data.NewRole(roleName, newThreshold, addKeys.IDs(), addPaths) |
|
332 |
- if err != nil { |
|
333 |
- return err |
|
334 |
- } |
|
335 |
- } else { |
|
336 |
- // If we can't find the role and didn't specify keys to add, this is an error |
|
337 |
- return data.ErrInvalidRole{Role: roleName, Reason: "cannot create new delegation without keys"} |
|
333 |
+ delgRole, err = data.NewRole(roleName, newThreshold, addKeys.IDs(), addPaths) |
|
334 |
+ if err != nil { |
|
335 |
+ return err |
|
338 | 336 |
} |
337 |
+ |
|
339 | 338 |
} |
340 | 339 |
// Add the key IDs to the role and the keys themselves to the parent |
341 | 340 |
for _, k := range addKeys { |
... | ... |
@@ -345,7 +358,7 @@ func delegationUpdateVisitor(roleName string, addKeys data.KeyList, removeKeys, |
345 | 345 |
} |
346 | 346 |
// Make sure we have a valid role still |
347 | 347 |
if len(delgRole.KeyIDs) < delgRole.Threshold { |
348 |
- return data.ErrInvalidRole{Role: roleName, Reason: "insufficient keys to meet threshold"} |
|
348 |
+ logrus.Warnf("role %s has fewer keys than its threshold of %d; it will not be usable until keys are added to it", delgRole.Name, delgRole.Threshold) |
|
349 | 349 |
} |
350 | 350 |
// NOTE: this closure CANNOT error after this point, as we've committed to editing the SignedTargets metadata in the repo object. |
351 | 351 |
// Any errors related to updating this delegation must occur before this point. |
... | ... |
@@ -392,11 +405,77 @@ func (tr *Repo) UpdateDelegationKeys(roleName string, addKeys data.KeyList, remo |
392 | 392 |
// Walk to the parent of this delegation, since that is where its role metadata exists |
393 | 393 |
// We do not have to verify that the walker reached its desired role in this scenario |
394 | 394 |
// since we've already done another walk to the parent role in VerifyCanSign, and potentially made a targets file |
395 |
- err := tr.WalkTargets("", parent, delegationUpdateVisitor(roleName, addKeys, removeKeys, []string{}, []string{}, false, newThreshold)) |
|
396 |
- if err != nil { |
|
397 |
- return err |
|
395 |
+ return tr.WalkTargets("", parent, delegationUpdateVisitor(roleName, addKeys, removeKeys, []string{}, []string{}, false, newThreshold)) |
|
396 |
+} |
|
397 |
+ |
|
398 |
+// PurgeDelegationKeys removes the provided canonical key IDs from all delegations |
|
399 |
+// present in the subtree rooted at role. The role argument must be provided in a wildcard |
|
400 |
+// format, i.e. targets/* would remove the key from all delegations in the repo |
|
401 |
+func (tr *Repo) PurgeDelegationKeys(role string, removeKeys []string) error { |
|
402 |
+ if !data.IsWildDelegation(role) { |
|
403 |
+ return data.ErrInvalidRole{ |
|
404 |
+ Role: role, |
|
405 |
+ Reason: "only wildcard roles can be used in a purge", |
|
406 |
+ } |
|
398 | 407 |
} |
399 |
- return nil |
|
408 |
+ |
|
409 |
+ removeIDs := make(map[string]struct{}) |
|
410 |
+ for _, id := range removeKeys { |
|
411 |
+ removeIDs[id] = struct{}{} |
|
412 |
+ } |
|
413 |
+ |
|
414 |
+ start := path.Dir(role) |
|
415 |
+ tufIDToCanon := make(map[string]string) |
|
416 |
+ |
|
417 |
+ purgeKeys := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { |
|
418 |
+ var ( |
|
419 |
+ deleteCandidates []string |
|
420 |
+ err error |
|
421 |
+ ) |
|
422 |
+ for id, key := range tgt.Signed.Delegations.Keys { |
|
423 |
+ var ( |
|
424 |
+ canonID string |
|
425 |
+ ok bool |
|
426 |
+ ) |
|
427 |
+ if canonID, ok = tufIDToCanon[id]; !ok { |
|
428 |
+ canonID, err = utils.CanonicalKeyID(key) |
|
429 |
+ if err != nil { |
|
430 |
+ return err |
|
431 |
+ } |
|
432 |
+ tufIDToCanon[id] = canonID |
|
433 |
+ } |
|
434 |
+ if _, ok := removeIDs[canonID]; ok { |
|
435 |
+ deleteCandidates = append(deleteCandidates, id) |
|
436 |
+ } |
|
437 |
+ } |
|
438 |
+ if len(deleteCandidates) == 0 { |
|
439 |
+ // none of the interesting keys were present. We're done with this role |
|
440 |
+ return nil |
|
441 |
+ } |
|
442 |
+ // now we know there are changes, check if we'll be able to sign them in |
|
443 |
+ if err := tr.VerifyCanSign(validRole.Name); err != nil { |
|
444 |
+ logrus.Warnf( |
|
445 |
+ "role %s contains keys being purged but you do not have the necessary keys present to sign it; keys will not be purged from %s or its immediate children", |
|
446 |
+ validRole.Name, |
|
447 |
+ validRole.Name, |
|
448 |
+ ) |
|
449 |
+ return nil |
|
450 |
+ } |
|
451 |
+ // we know we can sign in the changes, delete the keys |
|
452 |
+ for _, id := range deleteCandidates { |
|
453 |
+ delete(tgt.Signed.Delegations.Keys, id) |
|
454 |
+ } |
|
455 |
+ // delete candidate keys from all roles. |
|
456 |
+ for _, role := range tgt.Signed.Delegations.Roles { |
|
457 |
+ role.RemoveKeys(deleteCandidates) |
|
458 |
+ if len(role.KeyIDs) < role.Threshold { |
|
459 |
+ logrus.Warnf("role %s has fewer keys than its threshold of %d; it will not be usable until keys are added to it", role.Name, role.Threshold) |
|
460 |
+ } |
|
461 |
+ } |
|
462 |
+ tgt.Dirty = true |
|
463 |
+ return nil |
|
464 |
+ } |
|
465 |
+ return tr.WalkTargets("", start, purgeKeys) |
|
400 | 466 |
} |
401 | 467 |
|
402 | 468 |
// UpdateDelegationPaths updates the appropriate delegation's paths. |
... | ... |
@@ -655,7 +734,7 @@ func (tr *Repo) WalkTargets(targetPath, rolePath string, visitTargets walkVisito |
655 | 655 |
} |
656 | 656 |
|
657 | 657 |
// Determine whether to visit this role or not: |
658 |
- // If the paths validate against the specified targetPath and the rolePath is empty or is in the subtree |
|
658 |
+ // If the paths validate against the specified targetPath and the rolePath is empty or is in the subtree. |
|
659 | 659 |
// Also check if we are choosing to skip visiting this role on this walk (see ListTargets and GetTargetByName priority) |
660 | 660 |
if isValidPath(targetPath, role) && isAncestorRole(role.Name, rolePath) && !utils.StrSliceContains(skipRoles, role.Name) { |
661 | 661 |
// If we had matching path or role name, visit this target and determine whether or not to keep walking |
... | ... |
@@ -948,7 +1027,7 @@ func (tr *Repo) SignTargets(role string, expires time.Time) (*data.Signed, error |
948 | 948 |
if _, ok := tr.Targets[role]; !ok { |
949 | 949 |
return nil, data.ErrInvalidRole{ |
950 | 950 |
Role: role, |
951 |
- Reason: "SignTargets called with non-existant targets role", |
|
951 |
+ Reason: "SignTargets called with non-existent targets role", |
|
952 | 952 |
} |
953 | 953 |
} |
954 | 954 |
tr.Targets[role].Signed.Expires = expires |
955 | 955 |
deleted file mode 100644 |
... | ... |
@@ -1,109 +0,0 @@ |
1 |
-package utils |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "crypto/hmac" |
|
5 |
- "encoding/hex" |
|
6 |
- "errors" |
|
7 |
- "fmt" |
|
8 |
- gopath "path" |
|
9 |
- "path/filepath" |
|
10 |
- |
|
11 |
- "github.com/docker/notary/trustmanager" |
|
12 |
- "github.com/docker/notary/tuf/data" |
|
13 |
-) |
|
14 |
- |
|
15 |
-// ErrWrongLength indicates the length was different to that expected |
|
16 |
-var ErrWrongLength = errors.New("wrong length") |
|
17 |
- |
|
18 |
-// ErrWrongHash indicates the hash was different to that expected |
|
19 |
-type ErrWrongHash struct { |
|
20 |
- Type string |
|
21 |
- Expected []byte |
|
22 |
- Actual []byte |
|
23 |
-} |
|
24 |
- |
|
25 |
-// Error implements error interface |
|
26 |
-func (e ErrWrongHash) Error() string { |
|
27 |
- return fmt.Sprintf("wrong %s hash, expected %#x got %#x", e.Type, e.Expected, e.Actual) |
|
28 |
-} |
|
29 |
- |
|
30 |
-// ErrNoCommonHash indicates the metadata did not provide any hashes this |
|
31 |
-// client recognizes |
|
32 |
-type ErrNoCommonHash struct { |
|
33 |
- Expected data.Hashes |
|
34 |
- Actual data.Hashes |
|
35 |
-} |
|
36 |
- |
|
37 |
-// Error implements error interface |
|
38 |
-func (e ErrNoCommonHash) Error() string { |
|
39 |
- types := func(a data.Hashes) []string { |
|
40 |
- t := make([]string, 0, len(a)) |
|
41 |
- for typ := range a { |
|
42 |
- t = append(t, typ) |
|
43 |
- } |
|
44 |
- return t |
|
45 |
- } |
|
46 |
- return fmt.Sprintf("no common hash function, expected one of %s, got %s", types(e.Expected), types(e.Actual)) |
|
47 |
-} |
|
48 |
- |
|
49 |
-// ErrUnknownHashAlgorithm - client was ashed to use a hash algorithm |
|
50 |
-// it is not familiar with |
|
51 |
-type ErrUnknownHashAlgorithm struct { |
|
52 |
- Name string |
|
53 |
-} |
|
54 |
- |
|
55 |
-// Error implements error interface |
|
56 |
-func (e ErrUnknownHashAlgorithm) Error() string { |
|
57 |
- return fmt.Sprintf("unknown hash algorithm: %s", e.Name) |
|
58 |
-} |
|
59 |
- |
|
60 |
-// PassphraseFunc type for func that request a passphrase |
|
61 |
-type PassphraseFunc func(role string, confirm bool) ([]byte, error) |
|
62 |
- |
|
63 |
-// FileMetaEqual checks whether 2 FileMeta objects are consistent with eachother |
|
64 |
-func FileMetaEqual(actual data.FileMeta, expected data.FileMeta) error { |
|
65 |
- if actual.Length != expected.Length { |
|
66 |
- return ErrWrongLength |
|
67 |
- } |
|
68 |
- hashChecked := false |
|
69 |
- for typ, hash := range expected.Hashes { |
|
70 |
- if h, ok := actual.Hashes[typ]; ok { |
|
71 |
- hashChecked = true |
|
72 |
- if !hmac.Equal(h, hash) { |
|
73 |
- return ErrWrongHash{typ, hash, h} |
|
74 |
- } |
|
75 |
- } |
|
76 |
- } |
|
77 |
- if !hashChecked { |
|
78 |
- return ErrNoCommonHash{expected.Hashes, actual.Hashes} |
|
79 |
- } |
|
80 |
- return nil |
|
81 |
-} |
|
82 |
- |
|
83 |
-// NormalizeTarget adds a slash, if required, to the front of a target path |
|
84 |
-func NormalizeTarget(path string) string { |
|
85 |
- return gopath.Join("/", path) |
|
86 |
-} |
|
87 |
- |
|
88 |
-// HashedPaths prefixes the filename with the known hashes for the file, |
|
89 |
-// returning a list of possible consistent paths. |
|
90 |
-func HashedPaths(path string, hashes data.Hashes) []string { |
|
91 |
- paths := make([]string, 0, len(hashes)) |
|
92 |
- for _, hash := range hashes { |
|
93 |
- hashedPath := filepath.Join(filepath.Dir(path), hex.EncodeToString(hash)+"."+filepath.Base(path)) |
|
94 |
- paths = append(paths, hashedPath) |
|
95 |
- } |
|
96 |
- return paths |
|
97 |
-} |
|
98 |
- |
|
99 |
-// CanonicalKeyID returns the ID of the public bytes version of a TUF key. |
|
100 |
-// On regular RSA/ECDSA TUF keys, this is just the key ID. On X509 RSA/ECDSA |
|
101 |
-// TUF keys, this is the key ID of the public key part of the key in the leaf cert |
|
102 |
-func CanonicalKeyID(k data.PublicKey) (string, error) { |
|
103 |
- switch k.Algorithm() { |
|
104 |
- case data.ECDSAx509Key, data.RSAx509Key: |
|
105 |
- return trustmanager.X509PublicKeyID(k) |
|
106 |
- default: |
|
107 |
- return k.ID(), nil |
|
108 |
- } |
|
109 |
-} |
110 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,551 @@ |
0 |
+package utils |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "bytes" |
|
4 |
+ "crypto/ecdsa" |
|
5 |
+ "crypto/elliptic" |
|
6 |
+ "crypto/rand" |
|
7 |
+ "crypto/rsa" |
|
8 |
+ "crypto/x509" |
|
9 |
+ "crypto/x509/pkix" |
|
10 |
+ "encoding/pem" |
|
11 |
+ "errors" |
|
12 |
+ "fmt" |
|
13 |
+ "io" |
|
14 |
+ "io/ioutil" |
|
15 |
+ "math/big" |
|
16 |
+ "time" |
|
17 |
+ |
|
18 |
+ "github.com/Sirupsen/logrus" |
|
19 |
+ "github.com/agl/ed25519" |
|
20 |
+ "github.com/docker/notary" |
|
21 |
+ "github.com/docker/notary/tuf/data" |
|
22 |
+) |
|
23 |
+ |
|
24 |
+// CanonicalKeyID returns the ID of the public bytes version of a TUF key. |
|
25 |
+// On regular RSA/ECDSA TUF keys, this is just the key ID. On X509 RSA/ECDSA |
|
26 |
+// TUF keys, this is the key ID of the public key part of the key in the leaf cert |
|
27 |
+func CanonicalKeyID(k data.PublicKey) (string, error) { |
|
28 |
+ switch k.Algorithm() { |
|
29 |
+ case data.ECDSAx509Key, data.RSAx509Key: |
|
30 |
+ return X509PublicKeyID(k) |
|
31 |
+ default: |
|
32 |
+ return k.ID(), nil |
|
33 |
+ } |
|
34 |
+} |
|
35 |
+ |
|
36 |
+// LoadCertFromPEM returns the first certificate found in a bunch of bytes or error |
|
37 |
+// if nothing is found. Taken from https://golang.org/src/crypto/x509/cert_pool.go#L85. |
|
38 |
+func LoadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) { |
|
39 |
+ for len(pemBytes) > 0 { |
|
40 |
+ var block *pem.Block |
|
41 |
+ block, pemBytes = pem.Decode(pemBytes) |
|
42 |
+ if block == nil { |
|
43 |
+ return nil, errors.New("no certificates found in PEM data") |
|
44 |
+ } |
|
45 |
+ if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { |
|
46 |
+ continue |
|
47 |
+ } |
|
48 |
+ |
|
49 |
+ cert, err := x509.ParseCertificate(block.Bytes) |
|
50 |
+ if err != nil { |
|
51 |
+ continue |
|
52 |
+ } |
|
53 |
+ |
|
54 |
+ return cert, nil |
|
55 |
+ } |
|
56 |
+ |
|
57 |
+ return nil, errors.New("no certificates found in PEM data") |
|
58 |
+} |
|
59 |
+ |
|
60 |
+// X509PublicKeyID returns a public key ID as a string, given a |
|
61 |
+// data.PublicKey that contains an X509 Certificate |
|
62 |
+func X509PublicKeyID(certPubKey data.PublicKey) (string, error) { |
|
63 |
+ // Note that this only loads the first certificate from the public key |
|
64 |
+ cert, err := LoadCertFromPEM(certPubKey.Public()) |
|
65 |
+ if err != nil { |
|
66 |
+ return "", err |
|
67 |
+ } |
|
68 |
+ pubKeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey) |
|
69 |
+ if err != nil { |
|
70 |
+ return "", err |
|
71 |
+ } |
|
72 |
+ |
|
73 |
+ var key data.PublicKey |
|
74 |
+ switch certPubKey.Algorithm() { |
|
75 |
+ case data.ECDSAx509Key: |
|
76 |
+ key = data.NewECDSAPublicKey(pubKeyBytes) |
|
77 |
+ case data.RSAx509Key: |
|
78 |
+ key = data.NewRSAPublicKey(pubKeyBytes) |
|
79 |
+ } |
|
80 |
+ |
|
81 |
+ return key.ID(), nil |
|
82 |
+} |
|
83 |
+ |
|
84 |
+// ParsePEMPrivateKey returns a data.PrivateKey from a PEM encoded private key. It |
|
85 |
+// only supports RSA (PKCS#1) and attempts to decrypt using the passphrase, if encrypted. |
|
86 |
+func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (data.PrivateKey, error) { |
|
87 |
+ block, _ := pem.Decode(pemBytes) |
|
88 |
+ if block == nil { |
|
89 |
+ return nil, errors.New("no valid private key found") |
|
90 |
+ } |
|
91 |
+ |
|
92 |
+ var privKeyBytes []byte |
|
93 |
+ var err error |
|
94 |
+ if x509.IsEncryptedPEMBlock(block) { |
|
95 |
+ privKeyBytes, err = x509.DecryptPEMBlock(block, []byte(passphrase)) |
|
96 |
+ if err != nil { |
|
97 |
+ return nil, errors.New("could not decrypt private key") |
|
98 |
+ } |
|
99 |
+ } else { |
|
100 |
+ privKeyBytes = block.Bytes |
|
101 |
+ } |
|
102 |
+ |
|
103 |
+ switch block.Type { |
|
104 |
+ case "RSA PRIVATE KEY": |
|
105 |
+ rsaPrivKey, err := x509.ParsePKCS1PrivateKey(privKeyBytes) |
|
106 |
+ if err != nil { |
|
107 |
+ return nil, fmt.Errorf("could not parse DER encoded key: %v", err) |
|
108 |
+ } |
|
109 |
+ |
|
110 |
+ tufRSAPrivateKey, err := RSAToPrivateKey(rsaPrivKey) |
|
111 |
+ if err != nil { |
|
112 |
+ return nil, fmt.Errorf("could not convert rsa.PrivateKey to data.PrivateKey: %v", err) |
|
113 |
+ } |
|
114 |
+ |
|
115 |
+ return tufRSAPrivateKey, nil |
|
116 |
+ case "EC PRIVATE KEY": |
|
117 |
+ ecdsaPrivKey, err := x509.ParseECPrivateKey(privKeyBytes) |
|
118 |
+ if err != nil { |
|
119 |
+ return nil, fmt.Errorf("could not parse DER encoded private key: %v", err) |
|
120 |
+ } |
|
121 |
+ |
|
122 |
+ tufECDSAPrivateKey, err := ECDSAToPrivateKey(ecdsaPrivKey) |
|
123 |
+ if err != nil { |
|
124 |
+ return nil, fmt.Errorf("could not convert ecdsa.PrivateKey to data.PrivateKey: %v", err) |
|
125 |
+ } |
|
126 |
+ |
|
127 |
+ return tufECDSAPrivateKey, nil |
|
128 |
+ case "ED25519 PRIVATE KEY": |
|
129 |
+ // We serialize ED25519 keys by concatenating the private key |
|
130 |
+ // to the public key and encoding with PEM. See the |
|
131 |
+ // ED25519ToPrivateKey function. |
|
132 |
+ tufECDSAPrivateKey, err := ED25519ToPrivateKey(privKeyBytes) |
|
133 |
+ if err != nil { |
|
134 |
+ return nil, fmt.Errorf("could not convert ecdsa.PrivateKey to data.PrivateKey: %v", err) |
|
135 |
+ } |
|
136 |
+ |
|
137 |
+ return tufECDSAPrivateKey, nil |
|
138 |
+ |
|
139 |
+ default: |
|
140 |
+ return nil, fmt.Errorf("unsupported key type %q", block.Type) |
|
141 |
+ } |
|
142 |
+} |
|
143 |
+ |
|
144 |
+// CertToPEM is a utility function returns a PEM encoded x509 Certificate |
|
145 |
+func CertToPEM(cert *x509.Certificate) []byte { |
|
146 |
+ pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) |
|
147 |
+ |
|
148 |
+ return pemCert |
|
149 |
+} |
|
150 |
+ |
|
151 |
+// CertChainToPEM is a utility function returns a PEM encoded chain of x509 Certificates, in the order they are passed |
|
152 |
+func CertChainToPEM(certChain []*x509.Certificate) ([]byte, error) { |
|
153 |
+ var pemBytes bytes.Buffer |
|
154 |
+ for _, cert := range certChain { |
|
155 |
+ if err := pem.Encode(&pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil { |
|
156 |
+ return nil, err |
|
157 |
+ } |
|
158 |
+ } |
|
159 |
+ return pemBytes.Bytes(), nil |
|
160 |
+} |
|
161 |
+ |
|
162 |
+// LoadCertFromFile loads the first certificate from the file provided. The |
|
163 |
+// data is expected to be PEM Encoded and contain one of more certificates |
|
164 |
+// with PEM type "CERTIFICATE" |
|
165 |
+func LoadCertFromFile(filename string) (*x509.Certificate, error) { |
|
166 |
+ certs, err := LoadCertBundleFromFile(filename) |
|
167 |
+ if err != nil { |
|
168 |
+ return nil, err |
|
169 |
+ } |
|
170 |
+ return certs[0], nil |
|
171 |
+} |
|
172 |
+ |
|
173 |
+// LoadCertBundleFromFile loads certificates from the []byte provided. The |
|
174 |
+// data is expected to be PEM Encoded and contain one of more certificates |
|
175 |
+// with PEM type "CERTIFICATE" |
|
176 |
+func LoadCertBundleFromFile(filename string) ([]*x509.Certificate, error) { |
|
177 |
+ b, err := ioutil.ReadFile(filename) |
|
178 |
+ if err != nil { |
|
179 |
+ return nil, err |
|
180 |
+ } |
|
181 |
+ |
|
182 |
+ return LoadCertBundleFromPEM(b) |
|
183 |
+} |
|
184 |
+ |
|
185 |
+// LoadCertBundleFromPEM loads certificates from the []byte provided. The |
|
186 |
+// data is expected to be PEM Encoded and contain one of more certificates |
|
187 |
+// with PEM type "CERTIFICATE" |
|
188 |
+func LoadCertBundleFromPEM(pemBytes []byte) ([]*x509.Certificate, error) { |
|
189 |
+ certificates := []*x509.Certificate{} |
|
190 |
+ var block *pem.Block |
|
191 |
+ block, pemBytes = pem.Decode(pemBytes) |
|
192 |
+ for ; block != nil; block, pemBytes = pem.Decode(pemBytes) { |
|
193 |
+ if block.Type == "CERTIFICATE" { |
|
194 |
+ cert, err := x509.ParseCertificate(block.Bytes) |
|
195 |
+ if err != nil { |
|
196 |
+ return nil, err |
|
197 |
+ } |
|
198 |
+ certificates = append(certificates, cert) |
|
199 |
+ } else { |
|
200 |
+ return nil, fmt.Errorf("invalid pem block type: %s", block.Type) |
|
201 |
+ } |
|
202 |
+ } |
|
203 |
+ |
|
204 |
+ if len(certificates) == 0 { |
|
205 |
+ return nil, fmt.Errorf("no valid certificates found") |
|
206 |
+ } |
|
207 |
+ |
|
208 |
+ return certificates, nil |
|
209 |
+} |
|
210 |
+ |
|
211 |
+// GetLeafCerts parses a list of x509 Certificates and returns all of them |
|
212 |
+// that aren't CA |
|
213 |
+func GetLeafCerts(certs []*x509.Certificate) []*x509.Certificate { |
|
214 |
+ var leafCerts []*x509.Certificate |
|
215 |
+ for _, cert := range certs { |
|
216 |
+ if cert.IsCA { |
|
217 |
+ continue |
|
218 |
+ } |
|
219 |
+ leafCerts = append(leafCerts, cert) |
|
220 |
+ } |
|
221 |
+ return leafCerts |
|
222 |
+} |
|
223 |
+ |
|
224 |
+// GetIntermediateCerts parses a list of x509 Certificates and returns all of the |
|
225 |
+// ones marked as a CA, to be used as intermediates |
|
226 |
+func GetIntermediateCerts(certs []*x509.Certificate) []*x509.Certificate { |
|
227 |
+ var intCerts []*x509.Certificate |
|
228 |
+ for _, cert := range certs { |
|
229 |
+ if cert.IsCA { |
|
230 |
+ intCerts = append(intCerts, cert) |
|
231 |
+ } |
|
232 |
+ } |
|
233 |
+ return intCerts |
|
234 |
+} |
|
235 |
+ |
|
236 |
+// ParsePEMPublicKey returns a data.PublicKey from a PEM encoded public key or certificate. |
|
237 |
+func ParsePEMPublicKey(pubKeyBytes []byte) (data.PublicKey, error) { |
|
238 |
+ pemBlock, _ := pem.Decode(pubKeyBytes) |
|
239 |
+ if pemBlock == nil { |
|
240 |
+ return nil, errors.New("no valid public key found") |
|
241 |
+ } |
|
242 |
+ |
|
243 |
+ switch pemBlock.Type { |
|
244 |
+ case "CERTIFICATE": |
|
245 |
+ cert, err := x509.ParseCertificate(pemBlock.Bytes) |
|
246 |
+ if err != nil { |
|
247 |
+ return nil, fmt.Errorf("could not parse provided certificate: %v", err) |
|
248 |
+ } |
|
249 |
+ err = ValidateCertificate(cert, true) |
|
250 |
+ if err != nil { |
|
251 |
+ return nil, fmt.Errorf("invalid certificate: %v", err) |
|
252 |
+ } |
|
253 |
+ return CertToKey(cert), nil |
|
254 |
+ default: |
|
255 |
+ return nil, fmt.Errorf("unsupported PEM block type %q, expected certificate", pemBlock.Type) |
|
256 |
+ } |
|
257 |
+} |
|
258 |
+ |
|
259 |
+// ValidateCertificate returns an error if the certificate is not valid for notary |
|
260 |
+// Currently this is only ensuring the public key has a large enough modulus if RSA, |
|
261 |
+// using a non SHA1 signature algorithm, and an optional time expiry check |
|
262 |
+func ValidateCertificate(c *x509.Certificate, checkExpiry bool) error { |
|
263 |
+ if (c.NotBefore).After(c.NotAfter) { |
|
264 |
+ return fmt.Errorf("certificate validity window is invalid") |
|
265 |
+ } |
|
266 |
+ // Can't have SHA1 sig algorithm |
|
267 |
+ if c.SignatureAlgorithm == x509.SHA1WithRSA || c.SignatureAlgorithm == x509.DSAWithSHA1 || c.SignatureAlgorithm == x509.ECDSAWithSHA1 { |
|
268 |
+ return fmt.Errorf("certificate with CN %s uses invalid SHA1 signature algorithm", c.Subject.CommonName) |
|
269 |
+ } |
|
270 |
+ // If we have an RSA key, make sure it's long enough |
|
271 |
+ if c.PublicKeyAlgorithm == x509.RSA { |
|
272 |
+ rsaKey, ok := c.PublicKey.(*rsa.PublicKey) |
|
273 |
+ if !ok { |
|
274 |
+ return fmt.Errorf("unable to parse RSA public key") |
|
275 |
+ } |
|
276 |
+ if rsaKey.N.BitLen() < notary.MinRSABitSize { |
|
277 |
+ return fmt.Errorf("RSA bit length is too short") |
|
278 |
+ } |
|
279 |
+ } |
|
280 |
+ if checkExpiry { |
|
281 |
+ now := time.Now() |
|
282 |
+ tomorrow := now.AddDate(0, 0, 1) |
|
283 |
+ // Give one day leeway on creation "before" time, check "after" against today |
|
284 |
+ if (tomorrow).Before(c.NotBefore) || now.After(c.NotAfter) { |
|
285 |
+ return data.ErrCertExpired{CN: c.Subject.CommonName} |
|
286 |
+ } |
|
287 |
+ // If this certificate is expiring within 6 months, put out a warning |
|
288 |
+ if (c.NotAfter).Before(time.Now().AddDate(0, 6, 0)) { |
|
289 |
+ logrus.Warnf("certificate with CN %s is near expiry", c.Subject.CommonName) |
|
290 |
+ } |
|
291 |
+ } |
|
292 |
+ return nil |
|
293 |
+} |
|
294 |
+ |
|
295 |
+// GenerateRSAKey generates an RSA private key and returns a TUF PrivateKey |
|
296 |
+func GenerateRSAKey(random io.Reader, bits int) (data.PrivateKey, error) { |
|
297 |
+ rsaPrivKey, err := rsa.GenerateKey(random, bits) |
|
298 |
+ if err != nil { |
|
299 |
+ return nil, fmt.Errorf("could not generate private key: %v", err) |
|
300 |
+ } |
|
301 |
+ |
|
302 |
+ tufPrivKey, err := RSAToPrivateKey(rsaPrivKey) |
|
303 |
+ if err != nil { |
|
304 |
+ return nil, err |
|
305 |
+ } |
|
306 |
+ |
|
307 |
+ logrus.Debugf("generated RSA key with keyID: %s", tufPrivKey.ID()) |
|
308 |
+ |
|
309 |
+ return tufPrivKey, nil |
|
310 |
+} |
|
311 |
+ |
|
312 |
+// RSAToPrivateKey converts an rsa.Private key to a TUF data.PrivateKey type |
|
313 |
+func RSAToPrivateKey(rsaPrivKey *rsa.PrivateKey) (data.PrivateKey, error) { |
|
314 |
+ // Get a DER-encoded representation of the PublicKey |
|
315 |
+ rsaPubBytes, err := x509.MarshalPKIXPublicKey(&rsaPrivKey.PublicKey) |
|
316 |
+ if err != nil { |
|
317 |
+ return nil, fmt.Errorf("failed to marshal public key: %v", err) |
|
318 |
+ } |
|
319 |
+ |
|
320 |
+ // Get a DER-encoded representation of the PrivateKey |
|
321 |
+ rsaPrivBytes := x509.MarshalPKCS1PrivateKey(rsaPrivKey) |
|
322 |
+ |
|
323 |
+ pubKey := data.NewRSAPublicKey(rsaPubBytes) |
|
324 |
+ return data.NewRSAPrivateKey(pubKey, rsaPrivBytes) |
|
325 |
+} |
|
326 |
+ |
|
327 |
+// GenerateECDSAKey generates an ECDSA Private key and returns a TUF PrivateKey |
|
328 |
+func GenerateECDSAKey(random io.Reader) (data.PrivateKey, error) { |
|
329 |
+ ecdsaPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), random) |
|
330 |
+ if err != nil { |
|
331 |
+ return nil, err |
|
332 |
+ } |
|
333 |
+ |
|
334 |
+ tufPrivKey, err := ECDSAToPrivateKey(ecdsaPrivKey) |
|
335 |
+ if err != nil { |
|
336 |
+ return nil, err |
|
337 |
+ } |
|
338 |
+ |
|
339 |
+ logrus.Debugf("generated ECDSA key with keyID: %s", tufPrivKey.ID()) |
|
340 |
+ |
|
341 |
+ return tufPrivKey, nil |
|
342 |
+} |
|
343 |
+ |
|
344 |
+// GenerateED25519Key generates an ED25519 private key and returns a TUF |
|
345 |
+// PrivateKey. The serialization format we use is just the public key bytes |
|
346 |
+// followed by the private key bytes |
|
347 |
+func GenerateED25519Key(random io.Reader) (data.PrivateKey, error) { |
|
348 |
+ pub, priv, err := ed25519.GenerateKey(random) |
|
349 |
+ if err != nil { |
|
350 |
+ return nil, err |
|
351 |
+ } |
|
352 |
+ |
|
353 |
+ var serialized [ed25519.PublicKeySize + ed25519.PrivateKeySize]byte |
|
354 |
+ copy(serialized[:], pub[:]) |
|
355 |
+ copy(serialized[ed25519.PublicKeySize:], priv[:]) |
|
356 |
+ |
|
357 |
+ tufPrivKey, err := ED25519ToPrivateKey(serialized[:]) |
|
358 |
+ if err != nil { |
|
359 |
+ return nil, err |
|
360 |
+ } |
|
361 |
+ |
|
362 |
+ logrus.Debugf("generated ED25519 key with keyID: %s", tufPrivKey.ID()) |
|
363 |
+ |
|
364 |
+ return tufPrivKey, nil |
|
365 |
+} |
|
366 |
+ |
|
367 |
+// ECDSAToPrivateKey converts an ecdsa.Private key to a TUF data.PrivateKey type |
|
368 |
+func ECDSAToPrivateKey(ecdsaPrivKey *ecdsa.PrivateKey) (data.PrivateKey, error) { |
|
369 |
+ // Get a DER-encoded representation of the PublicKey |
|
370 |
+ ecdsaPubBytes, err := x509.MarshalPKIXPublicKey(&ecdsaPrivKey.PublicKey) |
|
371 |
+ if err != nil { |
|
372 |
+ return nil, fmt.Errorf("failed to marshal public key: %v", err) |
|
373 |
+ } |
|
374 |
+ |
|
375 |
+ // Get a DER-encoded representation of the PrivateKey |
|
376 |
+ ecdsaPrivKeyBytes, err := x509.MarshalECPrivateKey(ecdsaPrivKey) |
|
377 |
+ if err != nil { |
|
378 |
+ return nil, fmt.Errorf("failed to marshal private key: %v", err) |
|
379 |
+ } |
|
380 |
+ |
|
381 |
+ pubKey := data.NewECDSAPublicKey(ecdsaPubBytes) |
|
382 |
+ return data.NewECDSAPrivateKey(pubKey, ecdsaPrivKeyBytes) |
|
383 |
+} |
|
384 |
+ |
|
385 |
+// ED25519ToPrivateKey converts a serialized ED25519 key to a TUF |
|
386 |
+// data.PrivateKey type |
|
387 |
+func ED25519ToPrivateKey(privKeyBytes []byte) (data.PrivateKey, error) { |
|
388 |
+ if len(privKeyBytes) != ed25519.PublicKeySize+ed25519.PrivateKeySize { |
|
389 |
+ return nil, errors.New("malformed ed25519 private key") |
|
390 |
+ } |
|
391 |
+ |
|
392 |
+ pubKey := data.NewED25519PublicKey(privKeyBytes[:ed25519.PublicKeySize]) |
|
393 |
+ return data.NewED25519PrivateKey(*pubKey, privKeyBytes) |
|
394 |
+} |
|
395 |
+ |
|
396 |
+func blockType(k data.PrivateKey) (string, error) { |
|
397 |
+ switch k.Algorithm() { |
|
398 |
+ case data.RSAKey, data.RSAx509Key: |
|
399 |
+ return "RSA PRIVATE KEY", nil |
|
400 |
+ case data.ECDSAKey, data.ECDSAx509Key: |
|
401 |
+ return "EC PRIVATE KEY", nil |
|
402 |
+ case data.ED25519Key: |
|
403 |
+ return "ED25519 PRIVATE KEY", nil |
|
404 |
+ default: |
|
405 |
+ return "", fmt.Errorf("algorithm %s not supported", k.Algorithm()) |
|
406 |
+ } |
|
407 |
+} |
|
408 |
+ |
|
409 |
+// KeyToPEM returns a PEM encoded key from a Private Key |
|
410 |
+func KeyToPEM(privKey data.PrivateKey, role string) ([]byte, error) { |
|
411 |
+ bt, err := blockType(privKey) |
|
412 |
+ if err != nil { |
|
413 |
+ return nil, err |
|
414 |
+ } |
|
415 |
+ |
|
416 |
+ headers := map[string]string{} |
|
417 |
+ if role != "" { |
|
418 |
+ headers = map[string]string{ |
|
419 |
+ "role": role, |
|
420 |
+ } |
|
421 |
+ } |
|
422 |
+ |
|
423 |
+ block := &pem.Block{ |
|
424 |
+ Type: bt, |
|
425 |
+ Headers: headers, |
|
426 |
+ Bytes: privKey.Private(), |
|
427 |
+ } |
|
428 |
+ |
|
429 |
+ return pem.EncodeToMemory(block), nil |
|
430 |
+} |
|
431 |
+ |
|
432 |
+// EncryptPrivateKey returns an encrypted PEM key given a Privatekey |
|
433 |
+// and a passphrase |
|
434 |
+func EncryptPrivateKey(key data.PrivateKey, role, gun, passphrase string) ([]byte, error) { |
|
435 |
+ bt, err := blockType(key) |
|
436 |
+ if err != nil { |
|
437 |
+ return nil, err |
|
438 |
+ } |
|
439 |
+ |
|
440 |
+ password := []byte(passphrase) |
|
441 |
+ cipherType := x509.PEMCipherAES256 |
|
442 |
+ |
|
443 |
+ encryptedPEMBlock, err := x509.EncryptPEMBlock(rand.Reader, |
|
444 |
+ bt, |
|
445 |
+ key.Private(), |
|
446 |
+ password, |
|
447 |
+ cipherType) |
|
448 |
+ if err != nil { |
|
449 |
+ return nil, err |
|
450 |
+ } |
|
451 |
+ |
|
452 |
+ if encryptedPEMBlock.Headers == nil { |
|
453 |
+ return nil, fmt.Errorf("unable to encrypt key - invalid PEM file produced") |
|
454 |
+ } |
|
455 |
+ encryptedPEMBlock.Headers["role"] = role |
|
456 |
+ |
|
457 |
+ if gun != "" { |
|
458 |
+ encryptedPEMBlock.Headers["gun"] = gun |
|
459 |
+ } |
|
460 |
+ |
|
461 |
+ return pem.EncodeToMemory(encryptedPEMBlock), nil |
|
462 |
+} |
|
463 |
+ |
|
464 |
+// ReadRoleFromPEM returns the value from the role PEM header, if it exists |
|
465 |
+func ReadRoleFromPEM(pemBytes []byte) string { |
|
466 |
+ pemBlock, _ := pem.Decode(pemBytes) |
|
467 |
+ if pemBlock == nil || pemBlock.Headers == nil { |
|
468 |
+ return "" |
|
469 |
+ } |
|
470 |
+ role, ok := pemBlock.Headers["role"] |
|
471 |
+ if !ok { |
|
472 |
+ return "" |
|
473 |
+ } |
|
474 |
+ return role |
|
475 |
+} |
|
476 |
+ |
|
477 |
+// CertToKey transforms a single input certificate into its corresponding |
|
478 |
+// PublicKey |
|
479 |
+func CertToKey(cert *x509.Certificate) data.PublicKey { |
|
480 |
+ block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw} |
|
481 |
+ pemdata := pem.EncodeToMemory(&block) |
|
482 |
+ |
|
483 |
+ switch cert.PublicKeyAlgorithm { |
|
484 |
+ case x509.RSA: |
|
485 |
+ return data.NewRSAx509PublicKey(pemdata) |
|
486 |
+ case x509.ECDSA: |
|
487 |
+ return data.NewECDSAx509PublicKey(pemdata) |
|
488 |
+ default: |
|
489 |
+ logrus.Debugf("Unknown key type parsed from certificate: %v", cert.PublicKeyAlgorithm) |
|
490 |
+ return nil |
|
491 |
+ } |
|
492 |
+} |
|
493 |
+ |
|
494 |
+// CertsToKeys transforms each of the input certificate chains into its corresponding |
|
495 |
+// PublicKey |
|
496 |
+func CertsToKeys(leafCerts map[string]*x509.Certificate, intCerts map[string][]*x509.Certificate) map[string]data.PublicKey { |
|
497 |
+ keys := make(map[string]data.PublicKey) |
|
498 |
+ for id, leafCert := range leafCerts { |
|
499 |
+ if key, err := CertBundleToKey(leafCert, intCerts[id]); err == nil { |
|
500 |
+ keys[key.ID()] = key |
|
501 |
+ } |
|
502 |
+ } |
|
503 |
+ return keys |
|
504 |
+} |
|
505 |
+ |
|
506 |
+// CertBundleToKey creates a TUF key from a leaf certs and a list of |
|
507 |
+// intermediates |
|
508 |
+func CertBundleToKey(leafCert *x509.Certificate, intCerts []*x509.Certificate) (data.PublicKey, error) { |
|
509 |
+ certBundle := []*x509.Certificate{leafCert} |
|
510 |
+ certBundle = append(certBundle, intCerts...) |
|
511 |
+ certChainPEM, err := CertChainToPEM(certBundle) |
|
512 |
+ if err != nil { |
|
513 |
+ return nil, err |
|
514 |
+ } |
|
515 |
+ var newKey data.PublicKey |
|
516 |
+ // Use the leaf cert's public key algorithm for typing |
|
517 |
+ switch leafCert.PublicKeyAlgorithm { |
|
518 |
+ case x509.RSA: |
|
519 |
+ newKey = data.NewRSAx509PublicKey(certChainPEM) |
|
520 |
+ case x509.ECDSA: |
|
521 |
+ newKey = data.NewECDSAx509PublicKey(certChainPEM) |
|
522 |
+ default: |
|
523 |
+ logrus.Debugf("Unknown key type parsed from certificate: %v", leafCert.PublicKeyAlgorithm) |
|
524 |
+ return nil, x509.ErrUnsupportedAlgorithm |
|
525 |
+ } |
|
526 |
+ return newKey, nil |
|
527 |
+} |
|
528 |
+ |
|
529 |
+// NewCertificate returns an X509 Certificate following a template, given a GUN and validity interval. |
|
530 |
+func NewCertificate(gun string, startTime, endTime time.Time) (*x509.Certificate, error) { |
|
531 |
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) |
|
532 |
+ |
|
533 |
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) |
|
534 |
+ if err != nil { |
|
535 |
+ return nil, fmt.Errorf("failed to generate new certificate: %v", err) |
|
536 |
+ } |
|
537 |
+ |
|
538 |
+ return &x509.Certificate{ |
|
539 |
+ SerialNumber: serialNumber, |
|
540 |
+ Subject: pkix.Name{ |
|
541 |
+ CommonName: gun, |
|
542 |
+ }, |
|
543 |
+ NotBefore: startTime, |
|
544 |
+ NotAfter: endTime, |
|
545 |
+ |
|
546 |
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, |
|
547 |
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, |
|
548 |
+ BasicConstraintsValid: true, |
|
549 |
+ }, nil |
|
550 |
+} |