1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Merge pull request #22317 from cyli/bump-notary-version

Bump notary version to v0.3.0-RC1
This commit is contained in:
Alexander Morozov 2016-05-06 12:54:53 -07:00
commit a603fa33d5
48 changed files with 1240 additions and 647 deletions

View file

@ -186,7 +186,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install notary and notary-server
ENV NOTARY_VERSION docker-v1.11-3
ENV NOTARY_VERSION v0.3.0-RC1
RUN set -x \
&& export GO15VENDOREXPERIMENT=1 \
&& export GOPATH="$(mktemp -d)" \

View file

@ -117,7 +117,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install notary and notary-server
ENV NOTARY_VERSION docker-v1.11-3
ENV NOTARY_VERSION v0.3.0-RC1
RUN set -x \
&& export GO15VENDOREXPERIMENT=1 \
&& export GOPATH="$(mktemp -d)" \

View file

@ -128,7 +128,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install notary and notary-server
ENV NOTARY_VERSION docker-v1.11-3
ENV NOTARY_VERSION v0.3.0-RC1
RUN set -x \
&& export GO15VENDOREXPERIMENT=1 \
&& export GOPATH="$(mktemp -d)" \

View file

@ -141,7 +141,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install notary and notary-server
ENV NOTARY_VERSION docker-v1.11-3
ENV NOTARY_VERSION v0.3.0-RC1
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \

View file

@ -130,7 +130,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install notary and notary-server
ENV NOTARY_VERSION docker-v1.11-3
ENV NOTARY_VERSION v0.3.0-RC1
RUN set -x \
&& export GO15VENDOREXPERIMENT=1 \
&& export GOPATH="$(mktemp -d)" \
@ -158,7 +158,7 @@ RUN useradd --create-home --gid docker unprivilegeduser
VOLUME /var/lib/docker
WORKDIR /go/src/github.com/docker/docker
ENV DOCKER_BUILDTAGS apparmor pkcs11 selinux
ENV DOCKER_BUILDTAGS apparmor selinux
# Let us use a .bashrc file
RUN ln -sfv $PWD/.bashrc ~/.bashrc

View file

@ -33,6 +33,7 @@ import (
"github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/store"
@ -184,7 +185,9 @@ func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, aut
modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)))
tr := transport.NewTransport(base, modifiers...)
return client.NewNotaryRepository(cli.trustDirectory(), repoInfo.FullName(), server, tr, cli.getPassphraseRetriever())
return client.NewNotaryRepository(
cli.trustDirectory(), repoInfo.FullName(), server, tr, cli.getPassphraseRetriever(),
trustpinning.TrustPinConfig{})
}
func convertTarget(t client.Target) (target, error) {
@ -474,7 +477,7 @@ func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, ref referen
}
// get the latest repository metadata so we can figure out which roles to sign
_, err = repo.Update(false)
err = repo.Update(false)
switch err.(type) {
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:

View file

@ -53,7 +53,7 @@ clone git github.com/docker/distribution 9ec0d742d69f77caa4dd5f49ceb70c3067d39f3
clone git github.com/vbatts/tar-split v0.9.11
# get desired notary commit, might also need to be updated in Dockerfile
clone git github.com/docker/notary docker-v1.11-3
clone git github.com/docker/notary v0.3.0-RC1
clone git google.golang.org/grpc a22b6611561e9f0a3e0919690dd2caf48f14c517 https://github.com/grpc/grpc-go.git
clone git github.com/miekg/pkcs11 df8ae6ca730422dba20c768ff38ef7d79077a59f

View file

@ -1,15 +1,35 @@
FROM golang:1.6.0
FROM golang:1.6.1
RUN apt-get update && apt-get install -y \
curl \
clang \
libltdl-dev \
libsqlite3-dev \
patch \
tar \
xz-utils \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
RUN go get golang.org/x/tools/cmd/vet \
&& go get golang.org/x/tools/cmd/cover \
&& go get github.com/tools/godep
RUN go get golang.org/x/tools/cmd/cover
COPY . /go/src/github.com/docker/notary
# Configure the container for OSX cross compilation
ENV OSX_SDK MacOSX10.11.sdk
ENV OSX_CROSS_COMMIT 8aa9b71a394905e6c5f4b59e2b97b87a004658a4
RUN set -x \
&& export OSXCROSS_PATH="/osxcross" \
&& git clone https://github.com/tpoechtrager/osxcross.git $OSXCROSS_PATH \
&& ( cd $OSXCROSS_PATH && git checkout -q $OSX_CROSS_COMMIT) \
&& curl -sSL https://s3.dockerproject.org/darwin/v2/${OSX_SDK}.tar.xz -o "${OSXCROSS_PATH}/tarballs/${OSX_SDK}.tar.xz" \
&& UNATTENDED=yes OSX_VERSION_MIN=10.6 ${OSXCROSS_PATH}/build.sh > /dev/null
ENV PATH /osxcross/target/bin:$PATH
WORKDIR /go/src/github.com/docker/notary
ENV NOTARYDIR /go/src/github.com/docker/notary
COPY . ${NOTARYDIR}
ENV GOPATH ${NOTARYDIR}/Godeps/_workspace:$GOPATH
WORKDIR ${NOTARYDIR}
# Note this cannot use alpine because of the MacOSX Cross SDK: the cctools there uses sys/cdefs.h and that cannot be used in alpine: http://wiki.musl-libc.org/wiki/FAQ#Q:_I.27m_trying_to_compile_something_against_musl_and_I_get_error_messages_about_sys.2Fcdefs.h

View file

@ -13,14 +13,18 @@ endif
CTIMEVAR=-X $(NOTARY_PKG)/version.GitCommit=$(GITCOMMIT) -X $(NOTARY_PKG)/version.NotaryVersion=$(NOTARY_VERSION)
GO_LDFLAGS=-ldflags "-w $(CTIMEVAR)"
GO_LDFLAGS_STATIC=-ldflags "-w $(CTIMEVAR) -extldflags -static"
GOOSES = darwin freebsd linux
GOARCHS = amd64
GOOSES = darwin linux
NOTARY_BUILDTAGS ?= pkcs11
GO_EXC = go
NOTARYDIR := /go/src/github.com/docker/notary
GO_VERSION := $(shell go version | grep "1\.[6-9]\(\.[0-9]+\)*")
# check to make sure we have the right version
ifeq ($(strip $(GO_VERSION)),)
$(error Bad Go version - please install Go >= 1.6)
endif
# check to be sure pkcs11 lib is always imported with a build tag
GO_LIST_PKCS11 := $(shell go list -e -f '{{join .Deps "\n"}}' ./... | grep -v /vendor/ | xargs go list -e -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | grep -q pkcs11)
GO_LIST_PKCS11 := $(shell go list -tags "${NOTARY_BUILDTAGS}" -e -f '{{join .Deps "\n"}}' ./... | grep -v /vendor/ | xargs go list -e -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | grep -q pkcs11)
ifeq ($(GO_LIST_PKCS11),)
$(info pkcs11 import was not found anywhere without a build tag, yay)
else
@ -34,7 +38,7 @@ _space := $(empty) $(empty)
COVERDIR=.cover
COVERPROFILE?=$(COVERDIR)/cover.out
COVERMODE=count
PKGS ?= $(shell go list ./... | grep -v /vendor/ | tr '\n' ' ')
PKGS ?= $(shell go list -tags "${NOTARY_BUILDTAGS}" ./... | grep -v /vendor/ | tr '\n' ' ')
GO_VERSION = $(shell go version | awk '{print $$3}')
@ -53,15 +57,15 @@ version/version.go:
${PREFIX}/bin/notary-server: NOTARY_VERSION $(shell find . -type f -name '*.go')
@echo "+ $@"
@godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-server
@go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-server
${PREFIX}/bin/notary: NOTARY_VERSION $(shell find . -type f -name '*.go')
@echo "+ $@"
@godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary
@go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary
${PREFIX}/bin/notary-signer: NOTARY_VERSION $(shell find . -type f -name '*.go')
@echo "+ $@"
@godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-signer
@go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-signer
ifeq ($(shell uname -s),Darwin)
${PREFIX}/bin/static/notary-server:
@ -72,11 +76,11 @@ ${PREFIX}/bin/static/notary-signer:
else
${PREFIX}/bin/static/notary-server: NOTARY_VERSION $(shell find . -type f -name '*.go')
@echo "+ $@"
@godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary-server
@go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary-server
${PREFIX}/bin/static/notary-signer: NOTARY_VERSION $(shell find . -type f -name '*.go')
@echo "+ $@"
@godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary-signer
@go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary-signer
endif
vet:
@ -94,7 +98,7 @@ fmt:
lint:
@echo "+ $@"
@test -z "$$(golint ./... | grep -v .pb. | grep -v vendor/ | tee /dev/stderr)"
@test -z "$(shell find . -type f -name "*.go" -not -path "./vendor/*" -not -name "*.pb.*" -exec golint {} \; | tee /dev/stderr)"
# Requires that the following:
# go get -u github.com/client9/misspell/cmd/misspell
@ -126,6 +130,9 @@ test-full: vet lint
@echo
go test -tags "${NOTARY_BUILDTAGS}" $(TESTOPTS) -v $(PKGS)
integration:
buildscripts/integrationtest.sh development.yml
protos:
@protoc --go_out=plugins=grpc:. proto/*.proto
@ -136,7 +143,7 @@ protos:
# be run first
define gocover
$(GO_EXC) test $(OPTS) $(TESTOPTS) -covermode="$(COVERMODE)" -coverprofile="$(COVERDIR)/$(subst /,-,$(1)).$(subst $(_space),.,$(NOTARY_BUILDTAGS)).coverage.txt" "$(1)" || exit 1;
go test $(OPTS) $(TESTOPTS) -covermode="$(COVERMODE)" -coverprofile="$(COVERDIR)/$(subst /,-,$(1)).$(subst $(_space),.,$(NOTARY_BUILDTAGS)).coverage.txt" "$(1)" || exit 1;
endef
gen-cover:
@ -146,15 +153,13 @@ gen-cover:
# Generates the cover binaries and runs them all in serial, so this can be used
# run all tests with a yubikey without any problems
cover: GO_EXC := go
OPTS = -tags "${NOTARY_BUILDTAGS}" -coverpkg "$(shell ./coverpkg.sh $(1) $(NOTARY_PKG))"
cover: OPTS = -tags "${NOTARY_BUILDTAGS}" -coverpkg "$(shell ./coverpkg.sh $(1) $(NOTARY_PKG))"
cover: gen-cover covmerge
@go tool cover -html="$(COVERPROFILE)"
# Generates the cover binaries and runs them all in serial, so this can be used
# run all tests with a yubikey without any problems
ci: OPTS = -tags "${NOTARY_BUILDTAGS}" -race -coverpkg "$(shell ./coverpkg.sh $(1) $(NOTARY_PKG))"
GO_EXC := godep go
# Codecov knows how to merge multiple coverage files, so covmerge is not needed
ci: gen-cover
@ -168,21 +173,15 @@ covmerge:
clean-protos:
@rm proto/*.pb.go
client: ${PREFIX}/bin/notary
@echo "+ $@"
binaries: ${PREFIX}/bin/notary-server ${PREFIX}/bin/notary ${PREFIX}/bin/notary-signer
@echo "+ $@"
static: ${PREFIX}/bin/static/notary-server ${PREFIX}/bin/static/notary-signer
@echo "+ $@"
define template
mkdir -p ${PREFIX}/cross/$(1)/$(2);
GOOS=$(1) GOARCH=$(2) CGO_ENABLED=0 go build -o ${PREFIX}/cross/$(1)/$(2)/notary -a -tags "static_build netgo" -installsuffix netgo ${GO_LDFLAGS_STATIC} ./cmd/notary;
endef
cross:
$(foreach GOARCH,$(GOARCHS),$(foreach GOOS,$(GOOSES),$(call template,$(GOOS),$(GOARCH))))
notary-dockerfile:
@docker build --rm --force-rm -t notary .
@ -197,6 +196,10 @@ docker-images: notary-dockerfile server-dockerfile signer-dockerfile
shell: notary-dockerfile
docker run --rm -it -v $(CURDIR)/cross:$(NOTARYDIR)/cross -v $(CURDIR)/bin:$(NOTARYDIR)/bin notary bash
cross: notary-dockerfile
@rm -rf $(CURDIR)/cross
docker run --rm -v $(CURDIR)/cross:$(NOTARYDIR)/cross -e NOTARY_BUILDTAGS=$(NOTARY_BUILDTAGS) notary buildscripts/cross.sh $(GOOSES)
clean:
@echo "+ $@"

View file

@ -1,8 +1,9 @@
# Notary
# Notary
[![Circle CI](https://circleci.com/gh/docker/notary/tree/master.svg?style=shield)](https://circleci.com/gh/docker/notary/tree/master) [![CodeCov](https://codecov.io/github/docker/notary/coverage.svg?branch=master)](https://codecov.io/github/docker/notary)
The Notary project comprises a [server](cmd/notary-server) and a [client](cmd/notary) for running and interacting
with trusted collections.
with trusted collections. Please see the [service architecture](docs/service_architecture.md) documentation
for more information.
Notary aims to make the internet more secure by making it easy for people to
@ -21,7 +22,7 @@ received content.
## Goals
Notary is based on [The Update Framework](http://theupdateframework.com/), a secure general design for the problem of software distribution and updates. By using TUF, notary achieves a number of key advantages:
Notary is based on [The Update Framework](https://www.theupdateframework.com/), a secure general design for the problem of software distribution and updates. By using TUF, notary achieves a number of key advantages:
* **Survivable Key Compromise**: Content publishers must manage keys in order to sign their content. Signing keys may be compromised or lost so systems must be designed in order to be flexible and recoverable in the case of key compromise. TUF's notion of key roles is utilized to separate responsibilities across a hierarchy of keys such that loss of any particular key (except the root role) by itself is not fatal to the security of the system.
* **Freshness Guarantees**: Replay attacks are a common problem in designing secure systems, where previously valid payloads are replayed to trick another system. The same problem exists in the software update systems, where old signed can be presented as the most recent. notary makes use of timestamping on publishing so that consumers can know that they are receiving the most up to date content. This is particularly important when dealing with software update where old vulnerable versions could be used to attack users.
@ -30,165 +31,60 @@ Notary is based on [The Update Framework](http://theupdateframework.com/), a sec
* **Use of Existing Distribution**: Notary's trust guarantees are not tied at all to particular distribution channels from which content is delivered. Therefore, trust can be added to any existing content delivery mechanism.
* **Untrusted Mirrors and Transport**: All of the notary metadata can be mirrored and distributed via arbitrary channels.
# Notary CLI
## Security
Notary is a tool for publishing and managing trusted collections of content. Publishers can digitally sign collections and consumers can verify integrity and origin of content. This ability is built on a straightforward key management and signing interface to create signed collections and configure trusted publishers.
Please see our [service architecture docs](docs/service_architecture.md#threat-model) for more information about our threat model, which details the varying survivability and severities for key compromise as well as mitigations.
## Using Notary
Lets try using notary.
Our last security audit was on July 31, 2015 by NCC ([results](docs/resources/ncc_docker_notary_audit_2015_07_31.pdf)).
Any security vulnerabilities can be reported to security@docker.com.
# Getting started with the Notary CLI
Please get the Notary Client CLI binary from [the official releases page](https://github.com/docker/notary/releases) or you can [build one yourself](#building-notary).
The version of Notary server and signer should be greater than or equal to Notary CLI's version to ensure feature compatibility (ex: CLI version 0.2, server/signer version >= 0.2), and all official releases are associated with GitHub tags.
To use the Notary CLI with Docker hub images, please have a look at our
[getting started docs](docs/getting_started.md).
For more advanced usage, please see the
[advanced usage docs](docs/advanced_usage.md).
To use the CLI against a local Notary server rather than against Docker Hub:
1. Please ensure that you have [docker and docker-compose](http://docs.docker.com/compose/install/) installed.
1. `git clone https://github.com/docker/notary.git` and from the cloned repository path,
start up a local Notary server and signer and copy the config file and testing certs to your
local notary config directory:
```sh
$ docker-compose build
$ docker-compose up -d
$ mkdir -p ~/.notary && cp cmd/notary/config.json cmd/notary/root-ca.crt ~/.notary
```
1. Add `127.0.0.1 notary-server` to your `/etc/hosts`, or if using docker-machine,
add `$(docker-machine ip) notary-server`).
You can run through the examples in the
[getting started docs](docs/getting_started.md) and
[advanced usage docs](docs/advanced_usage.md), but
without the `-s` (server URL) argument to the `notary` command since the server
URL is specified already in the configuration, file you copied.
You can also leave off the `-d ~/.docker/trust` argument if you do not care
to use `notary` with Docker images.
## Building Notary
Prerequisites:
- Requirements from the [Compiling Notary Server](#compiling-notary-server) section (such as go 1.5.1)
- [docker and docker-compose](http://docs.docker.com/compose/install/)
- [Notary server configuration](#configuring-notary-server)
As setup, let's build notary and then start up a local notary-server (don't forget to add `127.0.0.1 notary-server` to your `/etc/hosts`, or if using docker-machine, add `$(docker-machine ip) notary-server`).
```sh
make binaries
docker-compose build
docker-compose up -d
```
Note: In order to have notary use the local notary server and development root CA we can load the local development configuration by appending `-c cmd/notary/config.json` to every command. If you would rather not have to use `-c` on every command, copy `cmd/notary/config.json and cmd/notary/root-ca.crt` to `~/.notary`.
First, let's initiate a notary collection called `example.com/scripts`
```sh
notary init example.com/scripts
```
Now, look at the keys you created as a result of initialization
```sh
notary key list
```
Cool, now add a local file `install.sh` and call it `v1`
```sh
notary add example.com/scripts v1 install.sh
```
Wouldn't it be nice if others could know that you've signed this content? Use `publish` to publish your collection to your default notary-server
```sh
notary publish example.com/scripts
```
Now, others can pull your trusted collection
```sh
notary list example.com/scripts
```
More importantly, they can verify the content of your script by using `notary verify`:
```sh
curl example.com/install.sh | notary verify example.com/scripts v1 | sh
```
# Notary Server
Notary Server manages TUF data over an HTTP API compatible with the
[notary client](cmd/notary).
It may be configured to use either JWT or HTTP Basic Auth for authentication.
Currently it only supports MySQL for storage of the TUF data, we intend to
expand this to other storage options.
## Setup for Development
The notary repository comes with Dockerfiles and a docker-compose file
to facilitate development. Simply run the following commands to start
a notary server with a temporary MySQL database in containers:
```
$ docker-compose build
$ docker-compose up
```
If you are on Mac OSX with boot2docker or kitematic, you'll need to
update your hosts file such that the name `notary` is associated with
the IP address of your VM (for boot2docker, this can be determined
by running `boot2docker ip`, with kitematic, `echo $DOCKER_HOST` should
show the IP of the VM). If you are using the default Linux setup,
you need to add `127.0.0.1 notary` to your hosts file.
## Successfully connecting over TLS
By default notary-server runs with TLS with certificates signed by a local
CA. In order to be able to successfully connect to it using
either `curl` or `openssl`, you will have to use the root CA file in `fixtures/root-ca.crt`.
OpenSSL example:
`openssl s_client -connect notary-server:4443 -CAfile fixtures/root-ca.crt`
## Compiling Notary Server
Prerequisites:
- Go = 1.5.1
- Go >= 1.6.1
- [godep](https://github.com/tools/godep) installed
- libtool development headers installed
- Ubuntu: `apt-get install libtool-dev`
- CentOS/RedHat: `yum install libtool-ltdl-devel`
- Mac OS ([Homebrew](http://brew.sh/)): `brew install libtool`
Install dependencies by running `godep restore`.
From the root of this git repository, run `make binaries`. This will
compile the `notary`, `notary-server`, and `notary-signer` applications and
place them in a `bin` directory at the root of the git repository (the `bin`
directory is ignored by the .gitignore file).
`notary-signer` depends upon `pkcs11`, which requires that libtool headers be installed (`libtool-dev` on Ubuntu, `libtool-ltdl-devel` on CentOS/RedHat). If you are using Mac OS, you can `brew install libtool`, and run `make binaries` with the following environment variables (assuming a standard installation of Homebrew):
```sh
export CPATH=/usr/local/include:${CPATH}
export LIBRARY_PATH=/usr/local/lib:${LIBRARY_PATH}
```
## Running Notary Server
The `notary-server` application has the following usage:
```
$ bin/notary-server --help
usage: bin/notary-serve
-config="": Path to configuration file
-debug=false: Enable the debugging server on localhost:8080
```
## Configuring Notary Server
The configuration file must be a json file with the following format:
```json
{
"server": {
"addr": ":4443",
"tls_cert_file": "./fixtures/notary-server.crt",
"tls_key_file": "./fixtures/notary-server.key"
},
"logging": {
"level": 5
}
}
```
The pem and key provided in fixtures are purely for local development and
testing. For production, you must create your own keypair and certificate,
either via the CA of your choice, or a self signed certificate.
If using the pem and key provided in fixtures, either:
- Add `fixtures/root-ca.crt` to your trusted root certificates
- Use the default configuration for notary client that loads the CA root for you by using the flag `-c ./cmd/notary/config.json`
- Disable TLS verification by adding the following option notary configuration file in `~/.notary/config.json`:
"skipTLSVerify": true
Otherwise, you will see TLS errors or X509 errors upon initializing the
notary collection:
```
$ notary list diogomonica.com/openvpn
* fatal: Get https://notary-server:4443/v2/: x509: certificate signed by unknown authority
$ notary list diogomonica.com/openvpn -c cmd/notary/config.json
latest b1df2ad7cbc19f06f08b69b4bcd817649b509f3e5420cdd2245a85144288e26d 4056
```
Run `make binaries`, which creates the Notary Client CLI binary at `bin/notary`.

View file

@ -3,10 +3,18 @@ machine:
pre:
# Install gvm
- bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/1.0.22/binscripts/gvm-installer)
# Upgrade docker
- sudo curl -L -o /usr/bin/docker 'https://s3-external-1.amazonaws.com/circle-downloads/docker-1.9.1-circleci'
- sudo chmod 0755 /usr/bin/docker
post:
# Install many go versions
- gvm install go1.6 -B --name=stable
- gvm install go1.6.1 -B --name=stable
# upgrade compose
- sudo pip install --upgrade docker-compose
services:
- docker
environment:
# Convenient shortcuts to "common" locations
@ -32,16 +40,13 @@ dependencies:
cp -R "$CHECKOUT" "$BASE_STABLE"
override:
# Install dependencies for every copied clone/go version
- gvm use stable && go get github.com/tools/godep:
pwd: $BASE_STABLE
post:
# For the stable go version, additionally install linting and misspell tools
# don't use circleci's default dependency installation step of `go get -d -u ./...`
# since we already vendor everything; additionally install linting and misspell tools
- >
gvm use stable &&
go get github.com/golang/lint/golint &&
go get -u github.com/client9/misspell/cmd/misspell
test:
pre:
# Output the go versions we are going to test
@ -70,13 +75,13 @@ test:
override:
# Test stable, and report
# hacking this to be parallel
- case $CIRCLE_NODE_INDEX in 0) gvm use stable && NOTARY_BUILDTAGS=pkcs11 make ci ;; 1) gvm use stable && NOTARY_BUILDTAGS=none make ci ;; esac:
- 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:
parallel: true
timeout: 600
pwd: $BASE_STABLE
post:
# Report to codecov.io
- bash <(curl -s https://codecov.io/bash):
- case $CIRCLE_NODE_INDEX in 0) bash <(curl -s https://codecov.io/bash) ;; 1) bash <(curl -s https://codecov.io/bash) ;; esac:
parallel: true
pwd: $BASE_STABLE

View file

@ -2,6 +2,7 @@ package client
import (
"bytes"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
@ -14,10 +15,10 @@ import (
"github.com/Sirupsen/logrus"
"github.com/docker/notary"
"github.com/docker/notary/certs"
"github.com/docker/notary/client/changelist"
"github.com/docker/notary/cryptoservice"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf"
tufclient "github.com/docker/notary/tuf/client"
"github.com/docker/notary/tuf/data"
@ -87,13 +88,14 @@ type NotaryRepository struct {
tufRepo *tuf.Repo
roundTrip http.RoundTripper
CertStore trustmanager.X509Store
trustPinning trustpinning.TrustPinConfig
}
// repositoryFromKeystores is a helper function for NewNotaryRepository that
// takes some basic NotaryRepository parameters as well as keystores (in order
// of usage preference), and returns a NotaryRepository.
func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper,
keyStores []trustmanager.KeyStore) (*NotaryRepository, error) {
keyStores []trustmanager.KeyStore, trustPin trustpinning.TrustPinConfig) (*NotaryRepository, error) {
certPath := filepath.Join(baseDir, notary.TrustedCertsDir)
certStore, err := trustmanager.NewX509FilteredFileStore(
@ -114,6 +116,7 @@ func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper,
CryptoService: cryptoService,
roundTrip: rt,
CertStore: certStore,
trustPinning: trustPin,
}
fileStore, err := store.NewFilesystemStore(
@ -159,8 +162,29 @@ func NewTarget(targetName string, targetPath string) (*Target, error) {
return &Target{Name: targetName, Hashes: meta.Hashes, Length: meta.Length}, nil
}
func rootCertKey(gun string, privKey data.PrivateKey) (*x509.Certificate, data.PublicKey, error) {
// Hard-coded policy: the generated certificate expires in 10 years.
startTime := time.Now()
cert, err := cryptoservice.GenerateCertificate(
privKey, gun, startTime, startTime.Add(notary.Year*10))
if err != nil {
return nil, nil, err
}
x509PublicKey := trustmanager.CertToKey(cert)
if x509PublicKey == nil {
return nil, nil, fmt.Errorf(
"cannot use regenerated certificate: format %s", cert.PublicKeyAlgorithm)
}
return cert, x509PublicKey, nil
}
// Initialize creates a new repository by using rootKey as the root Key for the
// TUF repository.
// TUF repository. The server must be reachable (and is asked to generate a
// timestamp key and possibly other serverManagedRoles), but the created repository
// result is only stored on local disk, not published to the server. To do that,
// use r.Publish() eventually.
func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...string) error {
privKey, _, err := r.CryptoService.GetPrivateKey(rootKeyID)
if err != nil {
@ -194,31 +218,12 @@ func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...st
}
}
// Hard-coded policy: the generated certificate expires in 10 years.
startTime := time.Now()
rootCert, err := cryptoservice.GenerateCertificate(
privKey, r.gun, startTime, startTime.AddDate(10, 0, 0))
rootCert, rootKey, err := rootCertKey(r.gun, privKey)
if err != nil {
return err
}
r.CertStore.AddCert(rootCert)
// The root key gets stored in the TUF metadata X509 encoded, linking
// the tuf root.json to our X509 PKI.
// If the key is RSA, we store it as type RSAx509, if it is ECDSA we store it
// as ECDSAx509 to allow the gotuf verifiers to correctly decode the
// key on verification of signatures.
var rootKey data.PublicKey
switch privKey.Algorithm() {
case data.RSAKey:
rootKey = data.NewRSAx509PublicKey(trustmanager.CertToPEM(rootCert))
case data.ECDSAKey:
rootKey = data.NewECDSAx509PublicKey(trustmanager.CertToPEM(rootCert))
default:
return fmt.Errorf("invalid format for root key: %s", privKey.Algorithm())
}
var (
rootRole = data.NewBaseRole(
data.CanonicalRootRole,
@ -341,9 +346,12 @@ func addChange(cl *changelist.FileChangelist, c changelist.Change, roles ...stri
// AddTarget creates new changelist entries to add a target to the given roles
// in the repository when the changelist gets applied at publish time.
// If roles are unspecified, the default role is "targets".
// If roles are unspecified, the default role is "targets"
func (r *NotaryRepository) AddTarget(target *Target, roles ...string) error {
if len(target.Hashes) == 0 {
return fmt.Errorf("no hashes specified for target \"%s\"", target.Name)
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
@ -386,7 +394,7 @@ func (r *NotaryRepository) RemoveTarget(targetName string, roles ...string) erro
// subtree and also the "targets/x" subtree, as we will defer parsing it until
// we explicitly reach it in our iteration of the provided list of roles.
func (r *NotaryRepository) ListTargets(roles ...string) ([]*TargetWithRole, error) {
_, err := r.Update(false)
err := r.Update(false)
if err != nil {
return nil, err
}
@ -432,8 +440,7 @@ func (r *NotaryRepository) ListTargets(roles ...string) ([]*TargetWithRole, erro
// will be returned
// See the IMPORTANT section on ListTargets above. Those roles also apply here.
func (r *NotaryRepository) GetTargetByName(name string, roles ...string) (*TargetWithRole, error) {
_, err := r.Update(false)
if err != nil {
if err := r.Update(false); err != nil {
return nil, err
}
@ -460,9 +467,8 @@ func (r *NotaryRepository) GetTargetByName(name string, roles ...string) (*Targe
}
return nil
}
err = r.tufRepo.WalkTargets(name, role, getTargetVisitorFunc, skipRoles...)
// Check that we didn't error, and that we assigned to our target
if err == nil && foundTarget {
if err := r.tufRepo.WalkTargets(name, role, getTargetVisitorFunc, skipRoles...); err == nil && foundTarget {
return &TargetWithRole{Target: Target{Name: name, Hashes: resultMeta.Hashes, Length: resultMeta.Length}, Role: resultRoleName}, nil
}
}
@ -491,8 +497,7 @@ type RoleWithSignatures struct {
// This represents the latest metadata for each role in this repo
func (r *NotaryRepository) ListRoles() ([]RoleWithSignatures, error) {
// Update to latest repo state
_, err := r.Update(false)
if err != nil {
if err := r.Update(false); err != nil {
return nil, err
}
@ -514,9 +519,8 @@ func (r *NotaryRepository) ListRoles() ([]RoleWithSignatures, error) {
case data.CanonicalTimestampRole:
roleWithSig.Signatures = r.tufRepo.Timestamp.Signatures
default:
// If the role isn't a delegation, we should error -- this is only possible if we have invalid state
if !data.IsDelegation(role.Name) {
return nil, data.ErrInvalidRole{Role: role.Name, Reason: "invalid role name"}
continue
}
if _, ok := r.tufRepo.Targets[role.Name]; ok {
// We'll only find a signature if we've published any targets with this delegation
@ -552,8 +556,7 @@ func (r *NotaryRepository) Publish() error {
func (r *NotaryRepository) publish(cl changelist.Changelist) error {
var initialPublish bool
// update first before publishing
_, err := r.Update(true)
if err != nil {
if err := r.Update(true); err != nil {
// If the remote is not aware of the repo, then this is being published
// for the first time. Try to load from disk instead for publishing.
if _, ok := err.(ErrRepositoryNotExist); ok {
@ -578,8 +581,7 @@ func (r *NotaryRepository) publish(cl changelist.Changelist) error {
}
}
// apply the changelist to the repo
err = applyChangelist(r.tufRepo, cl)
if err != nil {
if err := applyChangelist(r.tufRepo, cl); err != nil {
logrus.Debug("Error applying changelist")
return err
}
@ -631,7 +633,7 @@ func (r *NotaryRepository) publish(cl changelist.Changelist) error {
if err == nil {
// Only update the snapshot if we've successfully signed it.
updatedFiles[data.CanonicalSnapshotRole] = snapshotJSON
} else if _, ok := err.(signed.ErrNoKeys); ok {
} else if signErr, ok := err.(signed.ErrInsufficientSignatures); ok && signErr.FoundKeys == 0 {
// If signing fails due to us not having the snapshot key, then
// assume the server is going to sign, and do not include any snapshot
// data.
@ -650,16 +652,17 @@ func (r *NotaryRepository) publish(cl changelist.Changelist) error {
return remote.SetMultiMeta(updatedFiles)
}
// bootstrapRepo loads the repository from the local file system. This attempts
// to load metadata for all roles. Since server snapshots are supported,
// if the snapshot metadata fails to load, that's ok.
// bootstrapRepo loads the repository from the local file system (i.e.
// a not yet published repo or a possibly obsolete local copy) into
// r.tufRepo. This attempts to load metadata for all roles. Since server
// snapshots are supported, if the snapshot metadata fails to load, that's ok.
// This can also be unified with some cache reading tools from tuf/client.
// This assumes that bootstrapRepo is only used by Publish() or RotateKey()
func (r *NotaryRepository) bootstrapRepo() error {
tufRepo := tuf.NewRepo(r.CryptoService)
logrus.Debugf("Loading trusted collection.")
rootJSON, err := r.fileStore.GetMeta("root", -1)
rootJSON, err := r.fileStore.GetMeta(data.CanonicalRootRole, -1)
if err != nil {
return err
}
@ -672,7 +675,7 @@ func (r *NotaryRepository) bootstrapRepo() error {
if err != nil {
return err
}
targetsJSON, err := r.fileStore.GetMeta("targets", -1)
targetsJSON, err := r.fileStore.GetMeta(data.CanonicalTargetsRole, -1)
if err != nil {
return err
}
@ -681,9 +684,9 @@ func (r *NotaryRepository) bootstrapRepo() error {
if err != nil {
return err
}
tufRepo.SetTargets("targets", targets)
tufRepo.SetTargets(data.CanonicalTargetsRole, targets)
snapshotJSON, err := r.fileStore.GetMeta("snapshot", -1)
snapshotJSON, err := r.fileStore.GetMeta(data.CanonicalSnapshotRole, -1)
if err == nil {
snapshot := &data.SignedSnapshot{}
err = json.Unmarshal(snapshotJSON, snapshot)
@ -700,6 +703,8 @@ func (r *NotaryRepository) bootstrapRepo() error {
return nil
}
// saveMetadata saves contents of r.tufRepo onto the local disk, creating
// signatures as necessary, possibly prompting for passphrases.
func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error {
logrus.Debugf("Saving changes to Trusted Collection.")
@ -714,7 +719,7 @@ func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error {
targetsToSave := make(map[string][]byte)
for t := range r.tufRepo.Targets {
signedTargets, err := r.tufRepo.SignTargets(t, data.DefaultExpires("targets"))
signedTargets, err := r.tufRepo.SignTargets(t, data.DefaultExpires(data.CanonicalTargetsRole))
if err != nil {
return err
}
@ -756,25 +761,24 @@ func (r *NotaryRepository) errRepositoryNotExist() error {
// Update bootstraps a trust anchor (root.json) before updating all the
// metadata from the repo.
func (r *NotaryRepository) Update(forWrite bool) (*tufclient.Client, error) {
func (r *NotaryRepository) Update(forWrite bool) error {
c, err := r.bootstrapClient(forWrite)
if err != nil {
if _, ok := err.(store.ErrMetaNotFound); ok {
return nil, r.errRepositoryNotExist()
return r.errRepositoryNotExist()
}
return nil, err
return err
}
err = c.Update()
if err != nil {
if err := c.Update(); err != nil {
// notFound.Resource may include a checksum so when the role is root,
// it will be root.json or root.<checksum>.json. Therefore best we can
// do it match a "root." prefix
if notFound, ok := err.(store.ErrMetaNotFound); ok && strings.HasPrefix(notFound.Resource, data.CanonicalRootRole+".") {
return nil, r.errRepositoryNotExist()
return r.errRepositoryNotExist()
}
return nil, err
return err
}
return c, nil
return nil
}
// bootstrapClient attempts to bootstrap a root.json to be used as the trust
@ -782,6 +786,20 @@ func (r *NotaryRepository) Update(forWrite bool) (*tufclient.Client, error) {
// we should always attempt to contact the server to determine if the repository
// is initialized or not. If set to true, we will always attempt to download
// and return an error if the remote repository errors.
//
// Partially populates r.tufRepo with this root metadata (only; use
// tufclient.Client.Update to load the rest).
//
// As another side effect, r.CertManager's list of trusted certificates
// is updated with data from the loaded root.json.
//
// Fails if the remote server is reachable and does not know the repo
// (i.e. before the first r.Publish()), in which case the error is
// store.ErrMetaNotFound, or if the root metadata (from whichever source is used)
// is not trusted.
//
// Returns a tufclient.Client for the remote server, which may not be actually
// operational (if the URL is invalid but a root.json is cached).
func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Client, error) {
var (
rootJSON []byte
@ -791,7 +809,7 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl
// try to read root from cache first. We will trust this root
// until we detect a problem during update which will cause
// us to download a new root and perform a rotation.
rootJSON, cachedRootErr := r.fileStore.GetMeta("root", -1)
rootJSON, cachedRootErr := r.fileStore.GetMeta(data.CanonicalRootRole, -1)
if cachedRootErr == nil {
signedRoot, cachedRootErr = r.validateRoot(rootJSON)
@ -806,7 +824,7 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl
// if remote store successfully set up, try and get root from remote
// We don't have any local data to determine the size of root, so try the maximum (though it is restricted at 100MB)
tmpJSON, err := remote.GetMeta("root", -1)
tmpJSON, err := remote.GetMeta(data.CanonicalRootRole, -1)
if err != nil {
// we didn't have a root in cache and were unable to load one from
// the server. Nothing we can do but error.
@ -820,7 +838,7 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl
return nil, err
}
err = r.fileStore.SetMeta("root", tmpJSON)
err = r.fileStore.SetMeta(data.CanonicalRootRole, tmpJSON)
if err != nil {
// if we can't write cache we should still continue, just log error
logrus.Errorf("could not save root to cache: %s", err.Error())
@ -860,7 +878,7 @@ func (r *NotaryRepository) validateRoot(rootJSON []byte) (*data.SignedRoot, erro
return nil, err
}
err = certs.ValidateRoot(r.CertStore, root, r.gun)
err = trustpinning.ValidateRoot(r.CertStore, root, r.gun, r.trustPinning)
if err != nil {
return nil, err
}
@ -872,25 +890,19 @@ func (r *NotaryRepository) validateRoot(rootJSON []byte) (*data.SignedRoot, erro
// creates and adds one new key or delegates managing the key to the server.
// These changes are staged in a changelist until publish is called.
func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool) error {
// We currently support remotely managing timestamp and snapshot keys
canBeRemoteKey := role == data.CanonicalTimestampRole || role == data.CanonicalSnapshotRole
// And locally managing root, targets, and snapshot keys
canBeLocalKey := (role == data.CanonicalSnapshotRole || role == data.CanonicalTargetsRole ||
role == data.CanonicalRootRole)
switch {
// We currently support locally or remotely managing snapshot keys...
case role == data.CanonicalSnapshotRole:
break
// locally managing targets keys only
case role == data.CanonicalTargetsRole && !serverManagesKey:
break
case role == data.CanonicalTargetsRole && serverManagesKey:
return ErrInvalidRemoteRole{Role: data.CanonicalTargetsRole}
// and remotely managing timestamp keys only
case role == data.CanonicalTimestampRole && serverManagesKey:
break
case role == data.CanonicalTimestampRole && !serverManagesKey:
return ErrInvalidLocalRole{Role: data.CanonicalTimestampRole}
default:
case !data.ValidRole(role) || data.IsDelegation(role):
return fmt.Errorf("notary does not currently permit rotating the %s key", role)
case serverManagesKey && !canBeRemoteKey:
return ErrInvalidRemoteRole{Role: role}
case !serverManagesKey && !canBeLocalKey:
return ErrInvalidLocalRole{Role: role}
}
var (
@ -911,6 +923,18 @@ func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool) error {
return fmt.Errorf(errFmtMsg, err)
}
// if this is a root role, generate a root cert for the public key
if role == data.CanonicalRootRole {
privKey, _, err := r.CryptoService.GetPrivateKey(pubKey.ID())
if err != nil {
return err
}
_, pubKey, err = rootCertKey(r.gun, privKey)
if err != nil {
return err
}
}
cl := changelist.NewMemChangelist()
if err := r.rootFileKeyChange(cl, role, changelist.ActionCreate, pubKey); err != nil {
return err

View file

@ -240,7 +240,7 @@ func newDeleteDelegationChange(name string, content []byte) *changelist.TufChang
// Also converts key IDs to canonical key IDs to keep consistent with signing prompts
func (r *NotaryRepository) GetDelegationRoles() ([]*data.Role, error) {
// Update state of the repo to latest
if _, err := r.Update(false); err != nil {
if err := r.Update(false); err != nil {
return nil, err
}

View file

@ -8,13 +8,14 @@ import (
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
)
// NewNotaryRepository is a helper method that returns a new notary repository.
// It takes the base directory under where all the trust files will be stored
// (usually ~/.docker/trust/).
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
retriever passphrase.Retriever) (
retriever passphrase.Retriever, trustPinning trustpinning.TrustPinConfig) (
*NotaryRepository, error) {
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever)
@ -23,5 +24,5 @@ func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
}
return repositoryFromKeystores(baseDir, gun, baseURL, rt,
[]trustmanager.KeyStore{fileKeyStore})
[]trustmanager.KeyStore{fileKeyStore}, trustPinning)
}

View file

@ -9,13 +9,14 @@ import (
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustmanager/yubikey"
"github.com/docker/notary/trustpinning"
)
// NewNotaryRepository is a helper method that returns a new notary repository.
// It takes the base directory under where all the trust files will be stored
// (usually ~/.docker/trust/).
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
retriever passphrase.Retriever) (
retriever passphrase.Retriever, trustPinning trustpinning.TrustPinConfig) (
*NotaryRepository, error) {
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever)
@ -24,10 +25,10 @@ func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
}
keyStores := []trustmanager.KeyStore{fileKeyStore}
yubiKeyStore, _ := yubikey.NewYubiKeyStore(fileKeyStore, retriever)
yubiKeyStore, _ := yubikey.NewYubiStore(fileKeyStore, retriever)
if yubiKeyStore != nil {
keyStores = []trustmanager.KeyStore{yubiKeyStore, fileKeyStore}
}
return repositoryFromKeystores(baseDir, gun, baseURL, rt, keyStores)
return repositoryFromKeystores(baseDir, gun, baseURL, rt, keyStores, trustPinning)
}

View file

@ -20,6 +20,8 @@ const (
PubCertPerms = 0755
// Sha256HexSize is how big a Sha256 hex is in number of characters
Sha256HexSize = 64
// Sha512HexSize is how big a Sha512 hex is in number of characters
Sha512HexSize = 128
// SHA256 is the name of SHA256 hash algorithm
SHA256 = "sha256"
// SHA512 is the name of SHA512 hash algorithm
@ -49,6 +51,11 @@ const (
// (one year, in seconds, since one year is forever in terms of internet
// content)
CacheMaxAgeLimit = 1 * Year
MySQLBackend = "mysql"
MemoryBackend = "memory"
SQLiteBackend = "sqlite3"
RethinkDBBackend = "rethinkdb"
)
// NotaryDefaultExpiries is the construct used to configure the default expiry times of

View file

@ -21,13 +21,6 @@ func GenerateCertificate(rootKey data.PrivateKey, gun string, startTime, endTime
return generateCertificate(signer, gun, startTime, endTime)
}
// GenerateTestingCertificate generates a non-expired X509 Certificate from a template, given a GUN.
// Good enough for tests where expiration does not really matter; do not use if you care about the policy.
func GenerateTestingCertificate(signer crypto.Signer, gun string) (*x509.Certificate, error) {
startTime := time.Now()
return generateCertificate(signer, gun, startTime, startTime.AddDate(10, 0, 0))
}
func generateCertificate(signer crypto.Signer, gun string, startTime, endTime time.Time) (*x509.Certificate, error) {
template, err := trustmanager.NewCertificate(gun, startTime, endTime)
if err != nil {

View file

@ -0,0 +1,113 @@
version: "2"
services:
server:
build:
context: .
dockerfile: server.Dockerfile
volumes:
- ./fixtures/rethinkdb:/tls
networks:
- rdb
links:
- rdb-proxy:rdb-proxy.rdb
- signer
environment:
- SERVICE_NAME=notary_server
ports:
- "8080"
- "4443:4443"
entrypoint: /usr/bin/env sh
command: -c "sh migrations/rethink_migrate.sh && notary-server -config=fixtures/server-config.rethink.json"
depends_on:
- rdb-proxy
signer:
build:
context: .
dockerfile: signer.Dockerfile
volumes:
- ./fixtures/rethinkdb:/tls
networks:
rdb:
aliases:
- notarysigner
links:
- rdb-proxy:rdb-proxy.rdb
environment:
- SERVICE_NAME=notary_signer
entrypoint: /usr/bin/env sh
command: -c "sh migrations/rethink_migrate.sh && notary-signer -config=fixtures/signer-config.rethink.json"
depends_on:
- rdb-proxy
rdb-01:
image: jlhawn/rethinkdb-tls
volumes:
- ./fixtures/rethinkdb:/tls
- rdb-01-data:/var/data
networks:
rdb:
aliases:
- rdb
- rdb.rdb
- rdb-01.rdb
command: "--bind all --no-http-admin --server-name rdb_01 --canonical-address rdb-01.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem"
rdb-02:
image: jlhawn/rethinkdb-tls
volumes:
- ./fixtures/rethinkdb:/tls
- rdb-02-data:/var/data
networks:
rdb:
aliases:
- rdb
- rdb.rdb
- rdb-02.rdb
command: "--bind all --no-http-admin --server-name rdb_02 --canonical-address rdb-02.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem"
rdb-03:
image: jlhawn/rethinkdb-tls
volumes:
- ./fixtures/rethinkdb:/tls
- rdb-03-data:/var/data
networks:
rdb:
aliases:
- rdb
- rdb.rdb
- rdb-03.rdb
command: "--bind all --no-http-admin --server-name rdb_03 --canonical-address rdb-03.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem"
rdb-proxy:
image: jlhawn/rethinkdb-tls
ports:
- "8080:8080"
volumes:
- ./fixtures/rethinkdb:/tls
networks:
rdb:
aliases:
- rdb-proxy
- rdb-proxy.rdp
command: "proxy --bind all --join rdb.rdb --web-tls --web-tls-key /tls/key.pem --web-tls-cert /tls/cert.pem --driver-tls --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem"
depends_on:
- rdb-01
- rdb-02
- rdb-03
client:
volumes:
- ./test_output:/test_output
networks:
- rdb
build:
context: .
dockerfile: Dockerfile
links:
- server:notary-server
command: buildscripts/testclient.sh
volumes:
rdb-01-data:
external: false
rdb-02-data:
external: false
rdb-03-data:
external: false
networks:
rdb:
external: false

View file

@ -0,0 +1,36 @@
server:
build: .
dockerfile: server.Dockerfile
links:
- mysql
- signer
- signer:notarysigner
environment:
- SERVICE_NAME=notary_server
entrypoint: /usr/bin/env sh
command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.json"
signer:
build: .
dockerfile: signer.Dockerfile
links:
- mysql
environment:
- SERVICE_NAME=notary_signer
entrypoint: /usr/bin/env sh
command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.json"
mysql:
volumes:
- ./notarymysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
image: mariadb:10.1.10
environment:
- TERM=dumb
- MYSQL_ALLOW_EMPTY_PASSWORD="true"
command: mysqld --innodb_file_per_table
client:
volumes:
- ./test_output:/test_output
build: .
dockerfile: Dockerfile
links:
- server:notary-server
command: buildscripts/testclient.sh

View file

@ -0,0 +1,102 @@
version: "2"
services:
server:
build:
context: .
dockerfile: server.Dockerfile
volumes:
- ./fixtures/rethinkdb:/tls
networks:
- rdb
links:
- rdb-proxy:rdb-proxy.rdb
- signer
environment:
- SERVICE_NAME=notary_server
ports:
- "8080"
- "4443:4443"
entrypoint: /usr/bin/env sh
command: -c "sh migrations/rethink_migrate.sh && notary-server -config=fixtures/server-config.rethink.json"
depends_on:
- rdb-proxy
signer:
build:
context: .
dockerfile: signer.Dockerfile
volumes:
- ./fixtures/rethinkdb:/tls
networks:
rdb:
aliases:
- notarysigner
links:
- rdb-proxy:rdb-proxy.rdb
environment:
- SERVICE_NAME=notary_signer
entrypoint: /usr/bin/env sh
command: -c "sh migrations/rethink_migrate.sh && notary-signer -config=fixtures/signer-config.rethink.json"
depends_on:
- rdb-proxy
rdb-01:
image: jlhawn/rethinkdb-tls
volumes:
- ./fixtures/rethinkdb:/tls
- rdb-01-data:/var/data
networks:
rdb:
aliases:
- rdb
- rdb.rdb
- rdb-01.rdb
command: "--bind all --no-http-admin --server-name rdb_01 --canonical-address rdb-01.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem"
rdb-02:
image: jlhawn/rethinkdb-tls
volumes:
- ./fixtures/rethinkdb:/tls
- rdb-02-data:/var/data
networks:
rdb:
aliases:
- rdb
- rdb.rdb
- rdb-02.rdb
command: "--bind all --no-http-admin --server-name rdb_02 --canonical-address rdb-02.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem"
rdb-03:
image: jlhawn/rethinkdb-tls
volumes:
- ./fixtures/rethinkdb:/tls
- rdb-03-data:/var/data
networks:
rdb:
aliases:
- rdb
- rdb.rdb
- rdb-03.rdb
command: "--bind all --no-http-admin --server-name rdb_03 --canonical-address rdb-03.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem"
rdb-proxy:
image: jlhawn/rethinkdb-tls
ports:
- "8080:8080"
volumes:
- ./fixtures/rethinkdb:/tls
networks:
rdb:
aliases:
- rdb-proxy
- rdb-proxy.rdp
command: "proxy --bind all --join rdb.rdb --web-tls --web-tls-key /tls/key.pem --web-tls-cert /tls/cert.pem --driver-tls --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem"
depends_on:
- rdb-01
- rdb-02
- rdb-03
volumes:
rdb-01-data:
external: false
rdb-02-data:
external: false
rdb-03-data:
external: false
networks:
rdb:
external: false

View file

@ -10,7 +10,7 @@ server:
ports:
- "8080"
- "4443:4443"
entrypoint: /bin/bash
entrypoint: /usr/bin/env sh
command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.json"
signer:
build: .
@ -19,7 +19,7 @@ signer:
- mysql
environment:
- SERVICE_NAME=notary_signer
entrypoint: /bin/bash
entrypoint: /usr/bin/env sh
command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.json"
mysql:
volumes:

View file

@ -1,28 +1,25 @@
FROM golang:1.6.0
FROM golang:1.6.1-alpine
MAINTAINER David Lawrence "david.lawrence@docker.com"
RUN apt-get update && apt-get install -y \
libltdl-dev \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --update git gcc libc-dev && rm -rf /var/cache/apk/*
EXPOSE 4443
# Install DB migration tool
# Install SQL DB migration tool
RUN go get github.com/mattes/migrate
ENV NOTARYPKG github.com/docker/notary
# Copy the local repo to the expected go path
COPY . /go/src/github.com/docker/notary
COPY . /go/src/${NOTARYPKG}
WORKDIR /go/src/${NOTARYPKG}
EXPOSE 4443
# Install notary-server
RUN go install \
-tags pkcs11 \
-ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \
${NOTARYPKG}/cmd/notary-server
${NOTARYPKG}/cmd/notary-server && apk del git gcc libc-dev
ENTRYPOINT [ "notary-server" ]
CMD [ "-config=fixtures/server-config-local.json" ]

View file

@ -1,30 +1,28 @@
FROM golang:1.6.0
FROM golang:1.6.1-alpine
MAINTAINER David Lawrence "david.lawrence@docker.com"
RUN apt-get update && apt-get install -y \
libltdl-dev \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --update git gcc libc-dev && rm -rf /var/cache/apk/*
EXPOSE 4444
# Install DB migration tool
# Install SQL DB migration tool
RUN go get github.com/mattes/migrate
ENV NOTARYPKG github.com/docker/notary
# Copy the local repo to the expected go path
COPY . /go/src/${NOTARYPKG}
WORKDIR /go/src/${NOTARYPKG}
ENV NOTARY_SIGNER_DEFAULT_ALIAS="timestamp_1"
ENV NOTARY_SIGNER_TIMESTAMP_1="testpassword"
# Copy the local repo to the expected go path
COPY . /go/src/github.com/docker/notary
WORKDIR /go/src/${NOTARYPKG}
EXPOSE 4444
# Install notary-signer
RUN go install \
-tags pkcs11 \
-ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \
${NOTARYPKG}/cmd/notary-signer
${NOTARYPKG}/cmd/notary-signer && apk del git gcc libc-dev
ENTRYPOINT [ "notary-signer" ]
CMD [ "-config=fixtures/signer-config-local.json" ]

View file

@ -15,14 +15,12 @@ type SimpleFileStore struct {
perms os.FileMode
}
// NewSimpleFileStore creates a directory with 755 permissions
func NewSimpleFileStore(baseDir string, fileExt string) (*SimpleFileStore, error) {
// NewFileStore creates a fully configurable file store
func NewFileStore(baseDir, fileExt string, perms os.FileMode) (*SimpleFileStore, error) {
baseDir = filepath.Clean(baseDir)
if err := CreateDirectory(baseDir); err != nil {
if err := createDirectory(baseDir, perms); err != nil {
return nil, err
}
if !strings.HasPrefix(fileExt, ".") {
fileExt = "." + fileExt
}
@ -30,25 +28,20 @@ func NewSimpleFileStore(baseDir string, fileExt string) (*SimpleFileStore, error
return &SimpleFileStore{
baseDir: baseDir,
fileExt: fileExt,
perms: visible,
perms: perms,
}, nil
}
// NewPrivateSimpleFileStore creates a directory with 700 permissions
func NewPrivateSimpleFileStore(baseDir string, fileExt string) (*SimpleFileStore, error) {
if err := CreatePrivateDirectory(baseDir); err != nil {
return nil, err
}
// NewSimpleFileStore is a convenience wrapper to create a world readable,
// owner writeable filestore
func NewSimpleFileStore(baseDir, fileExt string) (*SimpleFileStore, error) {
return NewFileStore(baseDir, fileExt, visible)
}
if !strings.HasPrefix(fileExt, ".") {
fileExt = "." + fileExt
}
return &SimpleFileStore{
baseDir: baseDir,
fileExt: fileExt,
perms: private,
}, nil
// NewPrivateSimpleFileStore is a wrapper to create an owner readable/writeable
// _only_ filestore
func NewPrivateSimpleFileStore(baseDir, fileExt string) (*SimpleFileStore, error) {
return NewFileStore(baseDir, fileExt, private)
}
// Add writes data to a file with a given name
@ -71,24 +64,6 @@ func (f *SimpleFileStore) Remove(name string) error {
return os.Remove(filePath)
}
// RemoveDir removes the directory identified by name
func (f *SimpleFileStore) RemoveDir(name string) error {
dirPath := filepath.Join(f.baseDir, name)
// Check to see if directory exists
fi, err := os.Stat(dirPath)
if err != nil {
return err
}
// Check to see if it is a directory
if !fi.IsDir() {
return fmt.Errorf("directory not found: %s", name)
}
return os.RemoveAll(dirPath)
}
// Get returns the data given a file name
func (f *SimpleFileStore) Get(name string) ([]byte, error) {
filePath, err := f.GetPath(name)
@ -119,12 +94,6 @@ func (f *SimpleFileStore) ListFiles() []string {
return f.list(f.baseDir)
}
// ListDir lists all the files inside of a directory identified by a name
func (f *SimpleFileStore) ListDir(name string) []string {
fullPath := filepath.Join(f.baseDir, name)
return f.list(fullPath)
}
// list lists all the files in a directory given a full path. Ignores symlinks.
func (f *SimpleFileStore) list(path string) []string {
files := make([]string, 0, 0)
@ -170,16 +139,6 @@ func (f *SimpleFileStore) BaseDir() string {
return f.baseDir
}
// CreateDirectory uses createDirectory to create a chmod 755 Directory
func CreateDirectory(dir string) error {
return createDirectory(dir, visible)
}
// CreatePrivateDirectory uses createDirectory to create a chmod 700 Directory
func CreatePrivateDirectory(dir string) error {
return createDirectory(dir, private)
}
// createDirectory receives a string of the path to a directory.
// It does not support passing files, so the caller has to remove
// the filename by doing filepath.Dir(full_path_to_file)

View file

@ -62,7 +62,7 @@ func NewKeyFileStore(baseDir string, passphraseRetriever passphrase.Retriever) (
return keyStore, nil
}
func generateKeyInfoMap(s LimitedFileStore) map[string]KeyInfo {
func generateKeyInfoMap(s Storage) map[string]KeyInfo {
keyInfoMap := make(map[string]KeyInfo)
for _, keyPath := range s.ListFiles() {
d, err := s.Get(keyPath)
@ -309,7 +309,7 @@ func KeyInfoFromPEM(pemBytes []byte, filename string) (string, KeyInfo, error) {
return keyID, KeyInfo{Gun: gun, Role: role}, nil
}
func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error {
func addKey(s Storage, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error {
var (
chosenPassphrase string
@ -338,7 +338,7 @@ func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cached
// both in the newer format PEM headers, and also in the legacy filename
// format. It returns: the role, whether it was found in the legacy format
// (true == legacy), and an error
func getKeyRole(s LimitedFileStore, keyID string) (string, bool, error) {
func getKeyRole(s Storage, keyID string) (string, bool, error) {
name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(keyID), filepath.Ext(keyID)))
for _, file := range s.ListFiles() {
@ -361,11 +361,11 @@ func getKeyRole(s LimitedFileStore, keyID string) (string, bool, error) {
}
}
return "", false, &ErrKeyNotFound{KeyID: keyID}
return "", false, ErrKeyNotFound{KeyID: keyID}
}
// GetKey returns the PrivateKey given a KeyID
func getKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name string) (data.PrivateKey, string, error) {
func getKey(s Storage, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name string) (data.PrivateKey, string, error) {
cachedKeyEntry, ok := cachedKeys[name]
if ok {
return cachedKeyEntry.key, cachedKeyEntry.alias, nil
@ -389,7 +389,7 @@ func getKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cached
}
// RemoveKey removes the key from the keyfilestore
func removeKey(s LimitedFileStore, cachedKeys map[string]*cachedKey, name string) error {
func removeKey(s Storage, cachedKeys map[string]*cachedKey, name string) error {
role, legacy, err := getKeyRole(s, name)
if err != nil {
return err
@ -419,7 +419,7 @@ func getSubdir(alias string) string {
// Given a key ID, gets the bytes and alias belonging to that key if the key
// exists
func getRawKey(s LimitedFileStore, name string) ([]byte, string, error) {
func getRawKey(s Storage, name string) ([]byte, string, error) {
role, legacy, err := getKeyRole(s, name)
if err != nil {
return nil, "", err
@ -475,7 +475,7 @@ func GetPasswdDecryptBytes(passphraseRetriever passphrase.Retriever, pemBytes []
return privKey, passwd, nil
}
func encryptAndAddKey(s LimitedFileStore, passwd string, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error {
func encryptAndAddKey(s Storage, passwd string, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error {
var (
pemPrivKey []byte

View file

@ -43,6 +43,8 @@ type KeyStore interface {
// AddKey adds a key to the KeyStore, and if the key already exists,
// succeeds. Otherwise, returns an error if it cannot add.
AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error
// Should fail with ErrKeyNotFound if the keystore is operating normally
// and knows that it does not store the requested key.
GetKey(keyID string) (data.PrivateKey, string, error)
GetKeyInfo(keyID string) (KeyInfo, error)
ListKeys() map[string]KeyInfo

View file

@ -5,7 +5,7 @@ import (
"sync"
)
// MemoryFileStore is an implementation of LimitedFileStore that keeps
// MemoryFileStore is an implementation of Storage that keeps
// the contents in memory.
type MemoryFileStore struct {
sync.Mutex

View file

@ -17,8 +17,8 @@ var (
ErrPathOutsideStore = errors.New("path outside file store")
)
// LimitedFileStore implements the bare bones primitives (no hierarchy)
type LimitedFileStore interface {
// Storage implements the bare bones primitives (no hierarchy)
type Storage interface {
// Add writes a file to the specified location, returning an error if this
// is not possible (reasons may include permissions errors). The path is cleaned
// before being made absolute against the store's base dir.
@ -37,16 +37,6 @@ type LimitedFileStore interface {
// ListFiles returns a list of paths relative to the base directory of the
// filestore. Any of these paths must be retrievable via the
// LimitedFileStore.Get method.
// Storage.Get method.
ListFiles() []string
}
// FileStore is the interface for full-featured FileStores
type FileStore interface {
LimitedFileStore
RemoveDir(directoryName string) error
GetPath(fileName string) (string, error)
ListDir(directoryName string) []string
BaseDir() string
}

View file

@ -15,7 +15,7 @@ type X509FileStore struct {
fileMap map[CertID]string
fingerprintMap map[CertID]*x509.Certificate
nameMap map[string][]CertID
fileStore FileStore
fileStore Storage
}
// NewX509FileStore returns a new X509FileStore.
@ -88,11 +88,7 @@ func (s *X509FileStore) addNamedCert(cert *x509.Certificate) error {
certBytes := CertToPEM(cert)
// Save the file to disk if not already there.
filePath, err := s.fileStore.GetPath(fileName)
if err != nil {
return err
}
if _, err := os.Stat(filePath); os.IsNotExist(err) {
if _, err = s.fileStore.Get(fileName); os.IsNotExist(err) {
if err := s.fileStore.Add(fileName, certBytes); err != nil {
return err
}
@ -246,7 +242,7 @@ func (s *X509FileStore) GetCertificatesByCN(cn string) ([]*x509.Certificate, err
// as part of the roots list. This never allows the use of system roots, returning
// an error if there are no root CAs.
func (s *X509FileStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) {
// If we have no Certificates loaded return error (we don't want to rever to using
// If we have no Certificates loaded return error (we don't want to revert to using
// system CAs).
if len(s.fingerprintMap) == 0 {
return x509.VerifyOptions{}, errors.New("no root CAs available")

View file

@ -188,7 +188,7 @@ func (s *X509MemStore) GetCertificatesByCN(cn string) ([]*x509.Certificate, erro
// as part of the roots list. This never allows the use of system roots, returning
// an error if there are no root CAs.
func (s *X509MemStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) {
// If we have no Certificates loaded return error (we don't want to rever to using
// If we have no Certificates loaded return error (we don't want to revert to using
// system CAs).
if len(s.fingerprintMap) == 0 {
return x509.VerifyOptions{}, errors.New("no root CAs available")

View file

@ -1,6 +1,7 @@
package trustmanager
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
@ -57,13 +58,24 @@ func GetCertFromURL(urlStr string) (*x509.Certificate, error) {
return cert, nil
}
// CertToPEM is an utility function returns a PEM encoded x509 Certificate
// CertToPEM is a utility function returns a PEM encoded x509 Certificate
func CertToPEM(cert *x509.Certificate) []byte {
pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
return pemCert
}
// CertChainToPEM is a utility function returns a PEM encoded chain of x509 Certificates, in the order they are passed
func CertChainToPEM(certChain []*x509.Certificate) ([]byte, error) {
var pemBytes bytes.Buffer
for _, cert := range certChain {
if err := pem.Encode(&pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil {
return nil, err
}
}
return pemBytes.Bytes(), nil
}
// LoadCertFromPEM returns the first certificate found in a bunch of bytes or error
// if nothing is found. Taken from https://golang.org/src/crypto/x509/cert_pool.go#L85.
func LoadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) {
@ -224,20 +236,19 @@ func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (data.PrivateKey, er
return nil, errors.New("no valid private key found")
}
var privKeyBytes []byte
var err error
if x509.IsEncryptedPEMBlock(block) {
privKeyBytes, err = x509.DecryptPEMBlock(block, []byte(passphrase))
if err != nil {
return nil, errors.New("could not decrypt private key")
}
} else {
privKeyBytes = block.Bytes
}
switch block.Type {
case "RSA PRIVATE KEY":
var privKeyBytes []byte
var err error
if x509.IsEncryptedPEMBlock(block) {
privKeyBytes, err = x509.DecryptPEMBlock(block, []byte(passphrase))
if err != nil {
return nil, errors.New("could not decrypt private key")
}
} else {
privKeyBytes = block.Bytes
}
rsaPrivKey, err := x509.ParsePKCS1PrivateKey(privKeyBytes)
if err != nil {
return nil, fmt.Errorf("could not parse DER encoded key: %v", err)
@ -250,18 +261,6 @@ func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (data.PrivateKey, er
return tufRSAPrivateKey, nil
case "EC PRIVATE KEY":
var privKeyBytes []byte
var err error
if x509.IsEncryptedPEMBlock(block) {
privKeyBytes, err = x509.DecryptPEMBlock(block, []byte(passphrase))
if err != nil {
return nil, errors.New("could not decrypt private key")
}
} else {
privKeyBytes = block.Bytes
}
ecdsaPrivKey, err := x509.ParseECPrivateKey(privKeyBytes)
if err != nil {
return nil, fmt.Errorf("could not parse DER encoded private key: %v", err)
@ -277,18 +276,6 @@ func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (data.PrivateKey, er
// We serialize ED25519 keys by concatenating the private key
// to the public key and encoding with PEM. See the
// ED25519ToPrivateKey function.
var privKeyBytes []byte
var err error
if x509.IsEncryptedPEMBlock(block) {
privKeyBytes, err = x509.DecryptPEMBlock(block, []byte(passphrase))
if err != nil {
return nil, errors.New("could not decrypt private key")
}
} else {
privKeyBytes = block.Bytes
}
tufECDSAPrivateKey, err := ED25519ToPrivateKey(privKeyBytes)
if err != nil {
return nil, fmt.Errorf("could not convert ecdsa.PrivateKey to data.PrivateKey: %v", err)
@ -544,12 +531,34 @@ func CertToKey(cert *x509.Certificate) data.PublicKey {
}
}
// CertsToKeys transforms each of the input certificates into it's corresponding
// CertsToKeys transforms each of the input certificate chains into its corresponding
// PublicKey
func CertsToKeys(certs []*x509.Certificate) map[string]data.PublicKey {
func CertsToKeys(leafCerts []*x509.Certificate, intCerts map[string][]*x509.Certificate) map[string]data.PublicKey {
keys := make(map[string]data.PublicKey)
for _, cert := range certs {
newKey := CertToKey(cert)
for _, leafCert := range leafCerts {
certBundle := []*x509.Certificate{leafCert}
certID, err := FingerprintCert(leafCert)
if err != nil {
continue
}
if intCertsForLeafs, ok := intCerts[certID]; ok {
certBundle = append(certBundle, intCertsForLeafs...)
}
certChainPEM, err := CertChainToPEM(certBundle)
if err != nil {
continue
}
var newKey data.PublicKey
// Use the leaf cert's public key algorithm for typing
switch leafCert.PublicKeyAlgorithm {
case x509.RSA:
newKey = data.NewRSAx509PublicKey(certChainPEM)
case x509.ECDSA:
newKey = data.NewECDSAx509PublicKey(certChainPEM)
default:
logrus.Debugf("Unknown key type parsed from certificate: %v", leafCert.PublicKeyAlgorithm)
continue
}
keys[newKey.ID()] = newKey
}
return keys
@ -581,6 +590,7 @@ func NewCertificate(gun string, startTime, endTime time.Time) (*x509.Certificate
// X509PublicKeyID returns a public key ID as a string, given a
// data.PublicKey that contains an X509 Certificate
func X509PublicKeyID(certPubKey data.PublicKey) (string, error) {
// Note that this only loads the first certificate from the public key
cert, err := LoadCertFromPEM(certPubKey.Public())
if err != nil {
return "", err

View file

@ -25,14 +25,22 @@ import (
)
const (
USER_PIN = "123456"
SO_USER_PIN = "010203040506070801020304050607080102030405060708"
numSlots = 4 // number of slots in the yubikey
// UserPin is the user pin of a yubikey (in PIV parlance, is the PIN)
UserPin = "123456"
// SOUserPin is the "Security Officer" user pin - this is the PIV management
// (MGM) key, which is different than the admin pin of the Yubikey PGP interface
// (which in PIV parlance is the PUK, and defaults to 12345678)
SOUserPin = "010203040506070801020304050607080102030405060708"
numSlots = 4 // number of slots in the yubikey
KeymodeNone = 0
KeymodeTouch = 1 // touch enabled
KeymodePinOnce = 2 // require pin entry once
KeymodePinAlways = 4 // require pin entry all the time
// KeymodeNone means that no touch or PIN is required to sign with the yubikey
KeymodeNone = 0
// KeymodeTouch means that only touch is required to sign with the yubikey
KeymodeTouch = 1
// KeymodePinOnce means that the pin entry is required once the first time to sign with the yubikey
KeymodePinOnce = 2
// KeymodePinAlways means that pin entry is required every time to sign with the yubikey
KeymodePinAlways = 4
// the key size, when importing a key into yubikey, MUST be 32 bytes
ecdsaPrivateKeySize = 32
@ -95,6 +103,8 @@ func init() {
}
}
// ErrBackupFailed is returned when a YubiStore fails to back up a key that
// is added
type ErrBackupFailed struct {
err string
}
@ -103,6 +113,17 @@ func (err ErrBackupFailed) Error() string {
return fmt.Sprintf("Failed to backup private key to: %s", err.err)
}
// An error indicating that the HSM is not present (as opposed to failing),
// i.e. that we can confidently claim that the key is not stored in the HSM
// without notifying the user about a missing or failing HSM.
type errHSMNotPresent struct {
err string
}
func (err errHSMNotPresent) Error() string {
return err.err
}
type yubiSlot struct {
role string
slotID []byte
@ -116,10 +137,13 @@ type YubiPrivateKey struct {
libLoader pkcs11LibLoader
}
type YubikeySigner struct {
// YubiKeySigner wraps a YubiPrivateKey and implements the crypto.Signer interface
type yubikeySigner struct {
YubiPrivateKey
}
// NewYubiPrivateKey returns a YubiPrivateKey, which implements the data.PrivateKey
// interface except that the private material is inacessible
func NewYubiPrivateKey(slot []byte, pubKey data.ECDSAPublicKey,
passRetriever passphrase.Retriever) *YubiPrivateKey {
@ -131,7 +155,8 @@ func NewYubiPrivateKey(slot []byte, pubKey data.ECDSAPublicKey,
}
}
func (ys *YubikeySigner) Public() crypto.PublicKey {
// Public is a required method of the crypto.Signer interface
func (ys *yubikeySigner) Public() crypto.PublicKey {
publicKey, err := x509.ParsePKIXPublicKey(ys.YubiPrivateKey.Public())
if err != nil {
return nil
@ -147,7 +172,7 @@ func (y *YubiPrivateKey) setLibLoader(loader pkcs11LibLoader) {
// CryptoSigner returns a crypto.Signer tha wraps the YubiPrivateKey. Needed for
// Certificate generation only
func (y *YubiPrivateKey) CryptoSigner() crypto.Signer {
return &YubikeySigner{YubiPrivateKey: *y}
return &yubikeySigner{YubiPrivateKey: *y}
}
// Private is not implemented in hardware keys
@ -157,10 +182,14 @@ func (y *YubiPrivateKey) Private() []byte {
return nil
}
// SignatureAlgorithm returns which algorithm this key uses to sign - currently
// hardcoded to ECDSA
func (y YubiPrivateKey) SignatureAlgorithm() data.SigAlgorithm {
return data.ECDSASignature
}
// Sign is a required method of the crypto.Signer interface and the data.PrivateKey
// interface
func (y *YubiPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) ([]byte, error) {
ctx, session, err := SetupHSMEnv(pkcs11Lib, y.libLoader)
if err != nil {
@ -204,7 +233,7 @@ func addECDSAKey(
) error {
logrus.Debugf("Attempting to add key to yubikey with ID: %s", privKey.ID())
err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SO_USER_PIN)
err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SOUserPin)
if err != nil {
return err
}
@ -317,7 +346,7 @@ func getECDSAKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byt
// Sign returns a signature for a given signature request
func sign(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever passphrase.Retriever, payload []byte) ([]byte, error) {
err := login(ctx, session, passRetriever, pkcs11.CKU_USER, USER_PIN)
err := login(ctx, session, passRetriever, pkcs11.CKU_USER, UserPin)
if err != nil {
return nil, fmt.Errorf("error logging in: %v", err)
}
@ -376,7 +405,7 @@ func sign(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, pass
}
func yubiRemoveKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever passphrase.Retriever, keyID string) error {
err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SO_USER_PIN)
err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SOUserPin)
if err != nil {
return err
}
@ -584,20 +613,20 @@ func getNextEmptySlot(ctx IPKCS11Ctx, session pkcs11.SessionHandle) ([]byte, err
return nil, errors.New("Yubikey has no available slots.")
}
// YubiKeyStore is a KeyStore for private keys inside a Yubikey
type YubiKeyStore struct {
// YubiStore is a KeyStore for private keys inside a Yubikey
type YubiStore struct {
passRetriever passphrase.Retriever
keys map[string]yubiSlot
backupStore trustmanager.KeyStore
libLoader pkcs11LibLoader
}
// NewYubiKeyStore returns a YubiKeyStore, given a backup key store to write any
// NewYubiStore returns a YubiStore, given a backup key store to write any
// generated keys to (usually a KeyFileStore)
func NewYubiKeyStore(backupStore trustmanager.KeyStore, passphraseRetriever passphrase.Retriever) (
*YubiKeyStore, error) {
func NewYubiStore(backupStore trustmanager.KeyStore, passphraseRetriever passphrase.Retriever) (
*YubiStore, error) {
s := &YubiKeyStore{
s := &YubiStore{
passRetriever: passphraseRetriever,
keys: make(map[string]yubiSlot),
backupStore: backupStore,
@ -609,15 +638,16 @@ func NewYubiKeyStore(backupStore trustmanager.KeyStore, passphraseRetriever pass
// Name returns a user friendly name for the location this store
// keeps its data
func (s YubiKeyStore) Name() string {
func (s YubiStore) Name() string {
return "yubikey"
}
func (s *YubiKeyStore) setLibLoader(loader pkcs11LibLoader) {
func (s *YubiStore) setLibLoader(loader pkcs11LibLoader) {
s.libLoader = loader
}
func (s *YubiKeyStore) ListKeys() map[string]trustmanager.KeyInfo {
// ListKeys returns a list of keys in the yubikey store
func (s *YubiStore) ListKeys() map[string]trustmanager.KeyInfo {
if len(s.keys) > 0 {
return buildKeyMap(s.keys)
}
@ -639,7 +669,7 @@ func (s *YubiKeyStore) ListKeys() map[string]trustmanager.KeyInfo {
}
// AddKey puts a key inside the Yubikey, as well as writing it to the backup store
func (s *YubiKeyStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.PrivateKey) error {
func (s *YubiStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.PrivateKey) error {
added, err := s.addKey(privKey.ID(), keyInfo.Role, privKey)
if err != nil {
return err
@ -656,7 +686,7 @@ func (s *YubiKeyStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.Private
// Only add if we haven't seen the key already. Return whether the key was
// added.
func (s *YubiKeyStore) addKey(keyID, role string, privKey data.PrivateKey) (
func (s *YubiStore) addKey(keyID, role string, privKey data.PrivateKey) (
bool, error) {
// We only allow adding root keys for now
@ -702,17 +732,20 @@ func (s *YubiKeyStore) addKey(keyID, role string, privKey data.PrivateKey) (
// GetKey retrieves a key from the Yubikey only (it does not look inside the
// backup store)
func (s *YubiKeyStore) GetKey(keyID string) (data.PrivateKey, string, error) {
func (s *YubiStore) GetKey(keyID string) (data.PrivateKey, string, error) {
ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader)
if err != nil {
logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error())
if _, ok := err.(errHSMNotPresent); ok {
err = trustmanager.ErrKeyNotFound{KeyID: keyID}
}
return nil, "", err
}
defer cleanup(ctx, session)
key, ok := s.keys[keyID]
if !ok {
return nil, "", errors.New("no matching keys found inside of yubikey")
return nil, "", trustmanager.ErrKeyNotFound{KeyID: keyID}
}
pubKey, alias, err := getECDSAKey(ctx, session, key.slotID)
@ -734,7 +767,7 @@ func (s *YubiKeyStore) GetKey(keyID string) (data.PrivateKey, string, error) {
// RemoveKey deletes a key from the Yubikey only (it does not remove it from the
// backup store)
func (s *YubiKeyStore) RemoveKey(keyID string) error {
func (s *YubiStore) RemoveKey(keyID string) error {
ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader)
if err != nil {
logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error())
@ -757,13 +790,13 @@ func (s *YubiKeyStore) RemoveKey(keyID string) error {
}
// ExportKey doesn't work, because you can't export data from a Yubikey
func (s *YubiKeyStore) ExportKey(keyID string) ([]byte, error) {
logrus.Debugf("Attempting to export: %s key inside of YubiKeyStore", keyID)
func (s *YubiStore) ExportKey(keyID string) ([]byte, error) {
logrus.Debugf("Attempting to export: %s key inside of YubiStore", keyID)
return nil, errors.New("Keys cannot be exported from a Yubikey.")
}
// Not yet implemented
func (s *YubiKeyStore) GetKeyInfo(keyID string) (trustmanager.KeyInfo, error) {
// GetKeyInfo is not yet implemented
func (s *YubiStore) GetKeyInfo(keyID string) (trustmanager.KeyInfo, error) {
return trustmanager.KeyInfo{}, fmt.Errorf("Not yet implemented")
}
@ -788,7 +821,7 @@ func SetupHSMEnv(libraryPath string, libLoader pkcs11LibLoader) (
IPKCS11Ctx, pkcs11.SessionHandle, error) {
if libraryPath == "" {
return nil, 0, fmt.Errorf("no library found.")
return nil, 0, errHSMNotPresent{err: "no library found"}
}
p := libLoader(libraryPath)
@ -798,8 +831,7 @@ func SetupHSMEnv(libraryPath string, libLoader pkcs11LibLoader) (
if err := p.Initialize(); err != nil {
defer finalizeAndDestroy(p)
return nil, 0, fmt.Errorf(
"found library %s, but initialize error %s", libraryPath, err.Error())
return nil, 0, fmt.Errorf("found library %s, but initialize error %s", libraryPath, err.Error())
}
slots, err := p.GetSlotList(true)
@ -829,8 +861,8 @@ func SetupHSMEnv(libraryPath string, libLoader pkcs11LibLoader) (
return p, session, nil
}
// YubikeyAccessible returns true if a Yubikey can be accessed
func YubikeyAccessible() bool {
// IsAccessible returns true if a Yubikey can be accessed
func IsAccessible() bool {
if pkcs11Lib == "" {
return false
}

View file

@ -1,9 +1,10 @@
package certs
package trustpinning
import (
"crypto/x509"
"errors"
"fmt"
"strings"
"time"
"github.com/Sirupsen/logrus"
@ -36,6 +37,18 @@ func (err ErrRootRotationFail) Error() string {
return fmt.Sprintf("could not rotate trust to a new trusted root: %s", err.Reason)
}
func prettyFormatCertIDs(certs []*x509.Certificate) string {
ids := make([]string, 0, len(certs))
for _, cert := range certs {
id, err := trustmanager.FingerprintCert(cert)
if err != nil {
id = fmt.Sprintf("[Error %s]", err)
}
ids = append(ids, id)
}
return strings.Join(ids, ", ")
}
/*
ValidateRoot receives a new root, validates its correctness and attempts to
do root key rotation if needed.
@ -45,40 +58,56 @@ that list is non-empty means that we've already seen this repository before, and
have a list of trusted certificates for it. In this case, we use this list of
certificates to attempt to validate this root file.
If the previous validation succeeds, or in the case where we found no trusted
certificates for this particular GUN, we check the integrity of the root by
If the previous validation succeeds, we check the integrity of the root by
making sure that it is validated by itself. This means that we will attempt to
validate the root data with the certificates that are included in the root keys
themselves.
If this last steps succeeds, we attempt to do root rotation, by ensuring that
we only trust the certificates that are present in the new root.
However, if we do not have any current trusted certificates for this GUN, we
check if there are any pinned certificates specified in the trust_pinning section
of the notary client config. If this section specifies a Certs section with this
GUN, we attempt to validate that the certificates present in the downloaded root
file match the pinned ID.
This mechanism of operation is essentially Trust On First Use (TOFU): if we
have never seen a certificate for a particular CN, we trust it. If later we see
a different certificate for that certificate, we return an ErrValidationFailed error.
If the Certs section is empty for this GUN, we check if the trust_pinning
section specifies a CA section specified in the config for this GUN. If so, we check
that the specified CA is valid and has signed a certificate included in the downloaded
root file. The specified CA can be a prefix for this GUN.
If both the Certs and CA configs do not match this GUN, we fall back to the TOFU
section in the config: if true, we trust certificates specified in the root for
this GUN. If later we see a different certificate for that certificate, we return
an ErrValidationFailed error.
Note that since we only allow trust data to be downloaded over an HTTPS channel
we are using the current public PKI to validate the first download of the certificate
adding an extra layer of security over the normal (SSH style) trust model.
We shall call this: TOFUS.
Validation failure at any step will result in an ErrValidationFailed error.
*/
func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun string) error {
func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun string, trustPinning TrustPinConfig) error {
logrus.Debugf("entered ValidateRoot with dns: %s", gun)
signedRoot, err := data.RootFromSigned(root)
if err != nil {
return err
}
// Retrieve all the leaf certificates in root for which the CN matches the GUN
allValidCerts, err := validRootLeafCerts(signedRoot, gun)
rootRole, err := signedRoot.BuildBaseRole(data.CanonicalRootRole)
if err != nil {
return err
}
// Retrieve all the leaf and intermediate certificates in root for which the CN matches the GUN
allLeafCerts, allIntCerts := parseAllCerts(signedRoot)
certsFromRoot, err := validRootLeafCerts(allLeafCerts, gun)
if err != nil {
logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", gun, err)
return &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}
}
// Retrieve all the trusted certificates that match this gun
certsForCN, err := certStore.GetCertificatesByCN(gun)
trustedCerts, err := certStore.GetCertificatesByCN(gun)
if err != nil {
// If the error that we get back is different than ErrNoCertificatesFound
// we couldn't check if there are any certificates with this CN already
@ -88,35 +117,60 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin
return &ErrValidationFail{Reason: "unable to retrieve trusted certificates"}
}
}
// If we have certificates that match this specific GUN, let's make sure to
// use them first to validate that this new root is valid.
if len(certsForCN) != 0 {
logrus.Debugf("found %d valid root certificates for %s", len(certsForCN), gun)
err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(certsForCN))
if len(trustedCerts) != 0 {
logrus.Debugf("found %d valid root certificates for %s: %s", len(trustedCerts), gun,
prettyFormatCertIDs(trustedCerts))
err = signed.VerifySignatures(
root, data.BaseRole{Keys: trustmanager.CertsToKeys(trustedCerts, allIntCerts), Threshold: 1})
if err != nil {
logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err)
return &ErrValidationFail{Reason: "failed to validate data with current trusted certificates"}
}
} else {
logrus.Debugf("found no currently valid root certificates for %s", gun)
logrus.Debugf("found no currently valid root certificates for %s, using trust_pinning config to bootstrap trust", gun)
trustPinCheckFunc, err := NewTrustPinChecker(trustPinning, gun)
if err != nil {
return &ErrValidationFail{Reason: err.Error()}
}
validPinnedCerts := []*x509.Certificate{}
for _, cert := range certsFromRoot {
certID, err := trustmanager.FingerprintCert(cert)
if err != nil {
continue
}
if ok := trustPinCheckFunc(cert, allIntCerts[certID]); !ok {
continue
}
validPinnedCerts = append(validPinnedCerts, cert)
}
if len(validPinnedCerts) == 0 {
return &ErrValidationFail{Reason: "unable to match any certificates to trust_pinning config"}
}
certsFromRoot = validPinnedCerts
}
// Validate the integrity of the new root (does it have valid signatures)
err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(allValidCerts))
// Note that certsFromRoot is guaranteed to be unchanged only if we had prior cert data for this GUN or enabled TOFUS
// If we attempted to pin a certain certificate or CA, certsFromRoot could have been pruned accordingly
err = signed.VerifySignatures(root, data.BaseRole{
Keys: trustmanager.CertsToKeys(certsFromRoot, allIntCerts), Threshold: rootRole.Threshold})
if err != nil {
logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err)
return &ErrValidationFail{Reason: "failed to validate integrity of roots"}
}
// Getting here means A) we had trusted certificates and both the
// old and new validated this root; or B) we had no trusted certificates but
// the new set of certificates has integrity (self-signed)
// Getting here means:
// A) we had trusted certificates and both the old and new validated this root.
// or
// B) we had no trusted certificates but the new set of certificates has integrity (self-signed).
logrus.Debugf("entering root certificate rotation for: %s", gun)
// Do root certificate rotation: we trust only the certs present in the new root
// First we add all the new certificates (even if they already exist)
for _, cert := range allValidCerts {
for _, cert := range certsFromRoot {
err := certStore.AddCert(cert)
if err != nil {
// If the error is already exists we don't fail the rotation
@ -129,7 +183,12 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin
}
// Now we delete old certificates that aren't present in the new root
for certID, cert := range certsToRemove(certsForCN, allValidCerts) {
oldCertsToRemove, err := certsToRemove(trustedCerts, certsFromRoot)
if err != nil {
logrus.Debugf("inconsistency when removing old certificates: %v", err)
return err
}
for certID, cert := range oldCertsToRemove {
logrus.Debugf("removing certificate with certID: %s", certID)
err = certStore.RemoveCert(cert)
if err != nil {
@ -142,11 +201,10 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin
return nil
}
// validRootLeafCerts returns a list of non-exipired, non-sha1 certificates whose
// Common-Names match the provided GUN
func validRootLeafCerts(root *data.SignedRoot, gun string) ([]*x509.Certificate, error) {
// Get a list of all of the leaf certificates present in root
allLeafCerts, _ := parseAllCerts(root)
// validRootLeafCerts returns a list of non-expired, non-sha1 certificates
// found in root whose Common-Names match the provided GUN. Note that this
// "validity" alone does not imply any measure of trust.
func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string) ([]*x509.Certificate, error) {
var validLeafCerts []*x509.Certificate
// Go through every leaf certificate and check that the CN matches the gun
@ -180,7 +238,8 @@ func validRootLeafCerts(root *data.SignedRoot, gun string) ([]*x509.Certificate,
return nil, errors.New("no valid leaf certificates found in any of the root keys")
}
logrus.Debugf("found %d valid leaf certificates for %s", len(validLeafCerts), gun)
logrus.Debugf("found %d valid leaf certificates for %s: %s", len(validLeafCerts), gun,
prettyFormatCertIDs(validLeafCerts))
return validLeafCerts, nil
}
@ -223,6 +282,11 @@ func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, m
logrus.Debugf("invalid chain due to leaf certificate missing or too many leaf certificates for keyID: %s", keyID)
continue
}
// If we found a leaf certificate, assert that the cert bundle started with a leaf
if decodedCerts[0].IsCA {
logrus.Debugf("invalid chain due to leaf certificate not being first certificate for keyID: %s", keyID)
continue
}
// Get the ID of the leaf certificate
leafCert := leafCertList[0]
@ -243,16 +307,12 @@ func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, m
return leafCerts, intCerts
}
// certsToRemove returns all the certifificates from oldCerts that aren't present
// in newCerts
func certsToRemove(oldCerts, newCerts []*x509.Certificate) map[string]*x509.Certificate {
// certsToRemove returns all the certificates from oldCerts that aren't present
// in newCerts. Note that newCerts should never be empty, else this function will error.
// We expect newCerts to come from validateRootLeafCerts, which does not return empty sets.
func certsToRemove(oldCerts, newCerts []*x509.Certificate) (map[string]*x509.Certificate, error) {
certsToRemove := make(map[string]*x509.Certificate)
// If no newCerts were provided
if len(newCerts) == 0 {
return certsToRemove
}
// Populate a map with all the IDs from newCert
var newCertMap = make(map[string]struct{})
for _, cert := range newCerts {
@ -264,6 +324,14 @@ func certsToRemove(oldCerts, newCerts []*x509.Certificate) map[string]*x509.Cert
newCertMap[certID] = struct{}{}
}
// We don't want to "rotate" certificates to an empty set, nor keep old certificates if the
// new root does not trust them. newCerts should come from validRootLeafCerts, which refuses
// to return an empty set, and they should all be fingerprintable, so this should never happen
// - fail just to be sure.
if len(newCertMap) == 0 {
return nil, &ErrRootRotationFail{Reason: "internal error, got no certificates to rotate to"}
}
// Iterate over all the old certificates and check to see if we should remove them
for _, cert := range oldCerts {
certID, err := trustmanager.FingerprintCert(cert)
@ -276,5 +344,5 @@ func certsToRemove(oldCerts, newCerts []*x509.Certificate) map[string]*x509.Cert
}
}
return certsToRemove
return certsToRemove, nil
}

View file

@ -0,0 +1,118 @@
package trustpinning
import (
"crypto/x509"
"fmt"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/tuf/utils"
"strings"
)
// TrustPinConfig represents the configuration under the trust_pinning section of the config file
// This struct represents the preferred way to bootstrap trust for this repository
type TrustPinConfig struct {
CA map[string]string
Certs map[string][]string
DisableTOFU bool
}
type trustPinChecker struct {
gun string
config TrustPinConfig
pinnedCAPool *x509.CertPool
pinnedCertIDs []string
}
// CertChecker is a function type that will be used to check leaf certs against pinned trust
type CertChecker func(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool
// NewTrustPinChecker returns a new certChecker function from a TrustPinConfig for a GUN
func NewTrustPinChecker(trustPinConfig TrustPinConfig, gun string) (CertChecker, error) {
t := trustPinChecker{gun: gun, config: trustPinConfig}
// Determine the mode, and if it's even valid
if pinnedCerts, ok := trustPinConfig.Certs[gun]; ok {
t.pinnedCertIDs = pinnedCerts
return t.certsCheck, nil
}
if caFilepath, err := getPinnedCAFilepathByPrefix(gun, trustPinConfig); err == nil {
// Try to add the CA certs from its bundle file to our certificate store,
// and use it to validate certs in the root.json later
caCerts, err := trustmanager.LoadCertBundleFromFile(caFilepath)
if err != nil {
return nil, fmt.Errorf("could not load root cert from CA path")
}
// Now only consider certificates that are direct children from this CA cert chain
caRootPool := x509.NewCertPool()
for _, caCert := range caCerts {
if err = trustmanager.ValidateCertificate(caCert); err != nil {
continue
}
caRootPool.AddCert(caCert)
}
// If we didn't have any valid CA certs, error out
if len(caRootPool.Subjects()) == 0 {
return nil, fmt.Errorf("invalid CA certs provided")
}
t.pinnedCAPool = caRootPool
return t.caCheck, nil
}
if !trustPinConfig.DisableTOFU {
return t.tofusCheck, nil
}
return nil, fmt.Errorf("invalid trust pinning specified")
}
func (t trustPinChecker) certsCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool {
// reconstruct the leaf + intermediate cert chain, which is bundled as {leaf, intermediates...},
// in order to get the matching id in the root file
leafCertID, err := trustmanager.FingerprintCert(leafCert)
if err != nil {
return false
}
rootKeys := trustmanager.CertsToKeys([]*x509.Certificate{leafCert}, map[string][]*x509.Certificate{leafCertID: intCerts})
for keyID := range rootKeys {
if utils.StrSliceContains(t.pinnedCertIDs, keyID) {
return true
}
}
return false
}
func (t trustPinChecker) caCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool {
// Use intermediate certificates included in the root TUF metadata for our validation
caIntPool := x509.NewCertPool()
for _, intCert := range intCerts {
caIntPool.AddCert(intCert)
}
// Attempt to find a valid certificate chain from the leaf cert to CA root
// Use this certificate if such a valid chain exists (possibly using intermediates)
if _, err := leafCert.Verify(x509.VerifyOptions{Roots: t.pinnedCAPool, Intermediates: caIntPool}); err == nil {
return true
}
return false
}
func (t trustPinChecker) tofusCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool {
return true
}
// Will return the CA filepath corresponding to the most specific (longest) entry in the map that is still a prefix
// of the provided gun. Returns an error if no entry matches this GUN as a prefix.
func getPinnedCAFilepathByPrefix(gun string, t TrustPinConfig) (string, error) {
specificGUN := ""
specificCAFilepath := ""
foundCA := false
for gunPrefix, caFilepath := range t.CA {
if strings.HasPrefix(gun, gunPrefix) && len(gunPrefix) >= len(specificGUN) {
specificGUN = gunPrefix
specificCAFilepath = caFilepath
foundCA = true
}
}
if !foundCA {
return "", fmt.Errorf("could not find pinned CA for GUN: %s\n", gun)
}
return specificCAFilepath, nil
}

View file

@ -75,7 +75,7 @@ func (c *Client) update() error {
return err
}
// will always need top level targets at a minimum
err = c.downloadTargets("targets")
err = c.downloadTargets(data.CanonicalTargetsRole)
if err != nil {
logrus.Debugf("Client Update (Targets): %s", err.Error())
return err
@ -93,7 +93,7 @@ func (c Client) checkRoot() error {
expectedHashes := c.local.Snapshot.Signed.Meta[role].Hashes
raw, err := c.cache.GetMeta("root", size)
raw, err := c.cache.GetMeta(data.CanonicalRootRole, size)
if err != nil {
return err
}
@ -331,7 +331,7 @@ func (c *Client) downloadSnapshot() error {
size := c.local.Timestamp.Signed.Meta[role].Length
expectedHashes := c.local.Timestamp.Signed.Meta[role].Hashes
if len(expectedHashes) == 0 {
return data.ErrMissingMeta{Role: "snapshot"}
return data.ErrMissingMeta{Role: data.CanonicalSnapshotRole}
}
var download bool

View file

@ -20,3 +20,21 @@ type ErrMissingMeta struct {
func (e ErrMissingMeta) Error() string {
return fmt.Sprintf("tuf: sha256 checksum required for %s", e.Role)
}
// ErrInvalidChecksum is the error to be returned when checksum is invalid
type ErrInvalidChecksum struct {
alg string
}
func (e ErrInvalidChecksum) Error() string {
return fmt.Sprintf("%s checksum invalid", e.alg)
}
// ErrMismatchedChecksum is the error to be returned when checksum is mismatched
type ErrMismatchedChecksum struct {
alg string
}
func (e ErrMismatchedChecksum) Error() string {
return fmt.Sprintf("%s checksum mismatched", e.alg)
}

View file

@ -116,6 +116,22 @@ func (b BaseRole) ListKeyIDs() []string {
return listKeyIDs(b.Keys)
}
// Equals returns whether this BaseRole equals another BaseRole
func (b BaseRole) Equals(o BaseRole) bool {
if b.Threshold != o.Threshold || b.Name != o.Name || len(b.Keys) != len(o.Keys) {
return false
}
for keyID, key := range b.Keys {
oKey, ok := o.Keys[keyID]
if !ok || key.ID() != oKey.ID() {
return false
}
}
return true
}
// DelegationRole is an internal representation of a delegation role, with its public keys included
type DelegationRole struct {
BaseRole

View file

@ -2,7 +2,6 @@ package data
import (
"fmt"
"time"
"github.com/docker/go/canonical/json"
)
@ -16,9 +15,7 @@ type SignedRoot struct {
// Root is the Signed component of a root.json
type Root struct {
Type string `json:"_type"`
Version int `json:"version"`
Expires time.Time `json:"expires"`
SignedCommon
Keys Keys `json:"keys"`
Roles map[string]*RootRole `json:"roles"`
ConsistentSnapshot bool `json:"consistent_snapshot"`
@ -34,6 +31,11 @@ func isValidRootStructure(r Root) error {
role: CanonicalRootRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, r.Type)}
}
if r.Version < 0 {
return ErrInvalidMetadata{
role: CanonicalRootRole, msg: "version cannot be negative"}
}
// all the base roles MUST appear in the root.json - other roles are allowed,
// but other than the mirror role (not currently supported) are out of spec
for _, roleName := range BaseRoles {
@ -72,9 +74,11 @@ func NewRoot(keys map[string]PublicKey, roles map[string]*RootRole, consistent b
signedRoot := &SignedRoot{
Signatures: make([]Signature, 0),
Signed: Root{
Type: TUFTypes[CanonicalRootRole],
Version: 0,
Expires: DefaultExpires(CanonicalRootRole),
SignedCommon: SignedCommon{
Type: TUFTypes[CanonicalRootRole],
Version: 0,
Expires: DefaultExpires(CanonicalRootRole),
},
Keys: keys,
Roles: roles,
ConsistentSnapshot: consistent,
@ -146,6 +150,12 @@ func (r SignedRoot) MarshalJSON() ([]byte, error) {
// that it is a valid SignedRoot
func RootFromSigned(s *Signed) (*SignedRoot, error) {
r := Root{}
if s.Signed == nil {
return nil, ErrInvalidMetadata{
role: CanonicalRootRole,
msg: "root file contained an empty payload",
}
}
if err := defaultSerializer.Unmarshal(*s.Signed, &r); err != nil {
return nil, err
}

View file

@ -3,7 +3,6 @@ package data
import (
"bytes"
"fmt"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/go/canonical/json"
@ -19,10 +18,8 @@ type SignedSnapshot struct {
// Snapshot is the Signed component of a snapshot.json
type Snapshot struct {
Type string `json:"_type"`
Version int `json:"version"`
Expires time.Time `json:"expires"`
Meta Files `json:"meta"`
SignedCommon
Meta Files `json:"meta"`
}
// isValidSnapshotStructure returns an error, or nil, depending on whether the content of the
@ -35,6 +32,11 @@ func isValidSnapshotStructure(s Snapshot) error {
role: CanonicalSnapshotRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, s.Type)}
}
if s.Version < 0 {
return ErrInvalidMetadata{
role: CanonicalSnapshotRole, msg: "version cannot be negative"}
}
for _, role := range []string{CanonicalRootRole, CanonicalTargetsRole} {
// Meta is a map of FileMeta, so if the role isn't in the map it returns
// an empty FileMeta, which has an empty map, and you can check on keys
@ -82,9 +84,11 @@ func NewSnapshot(root *Signed, targets *Signed) (*SignedSnapshot, error) {
return &SignedSnapshot{
Signatures: make([]Signature, 0),
Signed: Snapshot{
Type: TUFTypes["snapshot"],
Version: 0,
Expires: DefaultExpires("snapshot"),
SignedCommon: SignedCommon{
Type: TUFTypes[CanonicalSnapshotRole],
Version: 0,
Expires: DefaultExpires(CanonicalSnapshotRole),
},
Meta: Files{
CanonicalRootRole: rootMeta,
CanonicalTargetsRole: targetsMeta,

View file

@ -38,6 +38,10 @@ func isValidTargetsStructure(t Targets, roleName string) error {
role: roleName, msg: fmt.Sprintf("expected type %s, not %s", expectedType, t.Type)}
}
if t.Version < 0 {
return ErrInvalidMetadata{role: roleName, msg: "version cannot be negative"}
}
for _, roleObj := range t.Delegations.Roles {
if !IsDelegation(roleObj.Name) || path.Dir(roleObj.Name) != roleName {
return ErrInvalidMetadata{

View file

@ -3,7 +3,6 @@ package data
import (
"bytes"
"fmt"
"time"
"github.com/docker/go/canonical/json"
"github.com/docker/notary"
@ -18,10 +17,8 @@ type SignedTimestamp struct {
// Timestamp is the Signed component of a timestamp.json
type Timestamp struct {
Type string `json:"_type"`
Version int `json:"version"`
Expires time.Time `json:"expires"`
Meta Files `json:"meta"`
SignedCommon
Meta Files `json:"meta"`
}
// isValidTimestampStructure returns an error, or nil, depending on whether the content of the struct
@ -34,6 +31,11 @@ func isValidTimestampStructure(t Timestamp) error {
role: CanonicalTimestampRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, t.Type)}
}
if t.Version < 0 {
return ErrInvalidMetadata{
role: CanonicalTimestampRole, msg: "version cannot be negative"}
}
// Meta is a map of FileMeta, so if the role isn't in the map it returns
// an empty FileMeta, which has an empty map, and you can check on keys
// from an empty map.
@ -64,9 +66,11 @@ func NewTimestamp(snapshot *Signed) (*SignedTimestamp, error) {
return &SignedTimestamp{
Signatures: make([]Signature, 0),
Signed: Timestamp{
Type: TUFTypes["timestamp"],
Version: 0,
Expires: DefaultExpires("timestamp"),
SignedCommon: SignedCommon{
Type: TUFTypes[CanonicalTimestampRole],
Version: 0,
Expires: DefaultExpires(CanonicalTimestampRole),
},
Meta: Files{
CanonicalSnapshotRole: snapshotMeta,
},

View file

@ -141,13 +141,13 @@ func CheckHashes(payload []byte, hashes Hashes) error {
case notary.SHA256:
checksum := sha256.Sum256(payload)
if subtle.ConstantTimeCompare(checksum[:], v) == 0 {
return fmt.Errorf("%s checksum mismatched", k)
return ErrMismatchedChecksum{alg: notary.SHA256}
}
cnt++
case notary.SHA512:
checksum := sha512.Sum512(payload)
if subtle.ConstantTimeCompare(checksum[:], v) == 0 {
return fmt.Errorf("%s checksum mismatched", k)
return ErrMismatchedChecksum{alg: notary.SHA512}
}
cnt++
}
@ -169,12 +169,12 @@ func CheckValidHashStructures(hashes Hashes) error {
switch k {
case notary.SHA256:
if len(v) != sha256.Size {
return fmt.Errorf("invalid %s checksum", notary.SHA256)
return ErrInvalidChecksum{alg: notary.SHA256}
}
cnt++
case notary.SHA512:
if len(v) != sha512.Size {
return fmt.Errorf("invalid %s checksum", notary.SHA512)
return ErrInvalidChecksum{alg: notary.SHA512}
}
cnt++
}

View file

@ -5,14 +5,21 @@ import (
"strings"
)
// ErrInsufficientSignatures - do not have enough signatures on a piece of
// ErrInsufficientSignatures - can not create enough signatures on a piece of
// metadata
type ErrInsufficientSignatures struct {
Name string
FoundKeys int
NeededKeys int
MissingKeyIDs []string
}
func (e ErrInsufficientSignatures) Error() string {
return fmt.Sprintf("tuf: insufficient signatures: %s", e.Name)
candidates := strings.Join(e.MissingKeyIDs, ", ")
if e.FoundKeys == 0 {
return fmt.Sprintf("signing keys not available, need %d keys out of: %s", e.NeededKeys, candidates)
}
return fmt.Sprintf("not enough signing keys: got %d of %d needed keys, other candidates: %s",
e.FoundKeys, e.NeededKeys, candidates)
}
// ErrExpired indicates a piece of metadata has expired
@ -51,6 +58,13 @@ func (e ErrInvalidKeyType) Error() string {
return "key type is not valid for signature"
}
// ErrInvalidKeyID indicates the specified key ID was incorrect for its associated data
type ErrInvalidKeyID struct{}
func (e ErrInvalidKeyID) Error() string {
return "key ID is not valid for key content"
}
// ErrInvalidKeyLength indicates that while we may support the cipher, the provided
// key length is not specifically supported, i.e. we support RSA, but not 1024 bit keys
type ErrInvalidKeyLength struct {

View file

@ -13,54 +13,73 @@ package signed
import (
"crypto/rand"
"fmt"
"github.com/Sirupsen/logrus"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/utils"
)
// Sign takes a data.Signed and a key, calculated and adds the signature
// to the data.Signed
// N.B. All public keys for a role should be passed so that this function
// can correctly clean up signatures that are no longer valid.
func Sign(service CryptoService, s *data.Signed, keys ...data.PublicKey) error {
logrus.Debugf("sign called with %d keys", len(keys))
// Sign takes a data.Signed and a cryptoservice containing private keys,
// calculates and adds at least minSignature signatures using signingKeys the
// data.Signed. It will also clean up any signatures that are not in produced
// by either a signingKey or an otherWhitelistedKey.
// Note that in most cases, otherWhitelistedKeys should probably be null. They
// are for keys you don't want to sign with, but you also don't want to remove
// existing signatures by those keys. For instance, if you want to call Sign
// multiple times with different sets of signing keys without undoing removing
// signatures produced by the previous call to Sign.
func Sign(service CryptoService, s *data.Signed, signingKeys []data.PublicKey,
minSignatures int, otherWhitelistedKeys []data.PublicKey) error {
logrus.Debugf("sign called with %d/%d required keys", minSignatures, len(signingKeys))
signatures := make([]data.Signature, 0, len(s.Signatures)+1)
signingKeyIDs := make(map[string]struct{})
tufIDs := make(map[string]data.PublicKey)
ids := make([]string, 0, len(keys))
privKeys := make(map[string]data.PrivateKey)
// Get all the private key objects related to the public keys
for _, key := range keys {
missingKeyIDs := []string{}
for _, key := range signingKeys {
canonicalID, err := utils.CanonicalKeyID(key)
ids = append(ids, canonicalID)
tufIDs[key.ID()] = key
if err != nil {
continue
return err
}
k, _, err := service.GetPrivateKey(canonicalID)
if err != nil {
continue
if _, ok := err.(trustmanager.ErrKeyNotFound); ok {
missingKeyIDs = append(missingKeyIDs, canonicalID)
continue
}
return err
}
privKeys[key.ID()] = k
}
// Check to ensure we have at least one signing key
if len(privKeys) == 0 {
return ErrNoKeys{KeyIDs: ids}
// include the list of otherWhitelistedKeys
for _, key := range otherWhitelistedKeys {
if _, ok := tufIDs[key.ID()]; !ok {
tufIDs[key.ID()] = key
}
}
// Check to ensure we have enough signing keys
if len(privKeys) < minSignatures {
return ErrInsufficientSignatures{FoundKeys: len(privKeys),
NeededKeys: minSignatures, MissingKeyIDs: missingKeyIDs}
}
emptyStruct := struct{}{}
// Do signing and generate list of signatures
for keyID, pk := range privKeys {
sig, err := pk.Sign(rand.Reader, *s.Signed, nil)
if err != nil {
logrus.Debugf("Failed to sign with key: %s. Reason: %v", keyID, err)
continue
return err
}
signingKeyIDs[keyID] = struct{}{}
signingKeyIDs[keyID] = emptyStruct
signatures = append(signatures, data.Signature{
KeyID: keyID,
Method: pk.SignatureAlgorithm(),
@ -68,15 +87,6 @@ func Sign(service CryptoService, s *data.Signed, keys ...data.PublicKey) error {
})
}
// Check we produced at least on signature
if len(signatures) < 1 {
return ErrInsufficientSignatures{
Name: fmt.Sprintf(
"cryptoservice failed to produce any signatures for keys with IDs: %v",
ids),
}
}
for _, sig := range s.Signatures {
if _, ok := signingKeyIDs[sig.KeyID]; ok {
// key is in the set of key IDs for which a signature has been created

View file

@ -21,47 +21,6 @@ var (
ErrWrongType = errors.New("tuf: meta file has wrong type")
)
// VerifyRoot checks if a given root file is valid against a known set of keys.
// Threshold is always assumed to be 1
func VerifyRoot(s *data.Signed, minVersion int, keys map[string]data.PublicKey) error {
if len(s.Signatures) == 0 {
return ErrNoSignatures
}
var decoded map[string]interface{}
if err := json.Unmarshal(*s.Signed, &decoded); err != nil {
return err
}
msg, err := json.MarshalCanonical(decoded)
if err != nil {
return err
}
for _, sig := range s.Signatures {
// method lookup is consistent due to Unmarshal JSON doing lower case for us.
method := sig.Method
verifier, ok := Verifiers[method]
if !ok {
logrus.Debugf("continuing b/c signing method is not supported for verify root: %s\n", sig.Method)
continue
}
key, ok := keys[sig.KeyID]
if !ok {
logrus.Debugf("continuing b/c signing key isn't present in keys: %s\n", sig.KeyID)
continue
}
if err := verifier.Verify(key, sig.Signature, msg); err != nil {
logrus.Debugf("continuing b/c signature was invalid\n")
continue
}
// threshold of 1 so return on first success
return verifyMeta(s, data.CanonicalRootRole, minVersion)
}
return ErrRoleThreshold{}
}
// Verify checks the signatures and metadata (expiry, version) for the signed role
// data
func Verify(s *data.Signed, role data.BaseRole, minVersion int) error {
@ -125,6 +84,10 @@ func VerifySignatures(s *data.Signed, roleData data.BaseRole) error {
logrus.Debugf("continuing b/c keyid lookup was nil: %s\n", sig.KeyID)
continue
}
// Check that the signature key ID actually matches the content ID of the key
if key.ID() != sig.KeyID {
return ErrInvalidKeyID{}
}
if err := VerifySignature(msg, sig, key); err != nil {
logrus.Debugf("continuing b/c %s", err.Error())
continue

View file

@ -39,7 +39,8 @@ func (f *FilesystemStore) getPath(name string) string {
}
// GetMeta returns the meta for the given name (a role) up to size bytes
// If size is -1, this corresponds to "infinite," but we cut off at 100MB
// If size is -1, this corresponds to "infinite," but we cut off at the
// predefined threshold "notary.MaxDownloadSize".
func (f *FilesystemStore) GetMeta(name string, size int64) ([]byte, error) {
meta, err := ioutil.ReadFile(f.getPath(name))
if err != nil {

View file

@ -6,6 +6,8 @@ import (
"encoding/json"
"fmt"
"path"
"sort"
"strconv"
"strings"
"time"
@ -62,6 +64,13 @@ type Repo struct {
Snapshot *data.SignedSnapshot
Timestamp *data.SignedTimestamp
cryptoService signed.CryptoService
// Because Repo is a mutable structure, these keep track of what the root
// role was when a root is set on the repo (as opposed to what it might be
// after things like AddBaseKeys and RemoveBaseKeys have been called on it).
// If we know what the original was, we'll if and how to handle root
// rotations.
originalRootRole data.BaseRole
}
// NewRepo initializes a Repo instance with a CryptoService.
@ -90,7 +99,7 @@ func (tr *Repo) AddBaseKeys(role string, keys ...data.PublicKey) error {
tr.Root.Dirty = true
// also, whichever role was switched out needs to be re-signed
// root has already been marked dirty
// root has already been marked dirty.
switch role {
case data.CanonicalSnapshotRole:
if tr.Snapshot != nil {
@ -128,17 +137,38 @@ func (tr *Repo) RemoveBaseKeys(role string, keyIDs ...string) error {
}
var keep []string
toDelete := make(map[string]struct{})
emptyStruct := struct{}{}
// remove keys from specified role
for _, k := range keyIDs {
toDelete[k] = struct{}{}
for _, rk := range tr.Root.Signed.Roles[role].KeyIDs {
if k != rk {
keep = append(keep, rk)
}
toDelete[k] = emptyStruct
}
oldKeyIDs := tr.Root.Signed.Roles[role].KeyIDs
for _, rk := range oldKeyIDs {
if _, ok := toDelete[rk]; !ok {
keep = append(keep, rk)
}
}
tr.Root.Signed.Roles[role].KeyIDs = keep
// also, whichever role had keys removed needs to be re-signed
// root has already been marked dirty.
switch role {
case data.CanonicalSnapshotRole:
if tr.Snapshot != nil {
tr.Snapshot.Dirty = true
}
case data.CanonicalTargetsRole:
if target, ok := tr.Targets[data.CanonicalTargetsRole]; ok {
target.Dirty = true
}
case data.CanonicalTimestampRole:
if tr.Timestamp != nil {
tr.Timestamp.Dirty = true
}
}
// determine which keys are no longer in use by any roles
for roleName, r := range tr.Root.Signed.Roles {
if roleName == role {
@ -151,13 +181,16 @@ func (tr *Repo) RemoveBaseKeys(role string, keyIDs ...string) error {
}
}
// remove keys no longer in use by any roles
for k := range toDelete {
delete(tr.Root.Signed.Keys, k)
// remove the signing key from the cryptoservice if it
// isn't a root key. Root keys must be kept for rotation
// signing
if role != data.CanonicalRootRole {
// Remove keys no longer in use by any roles, except for root keys.
// Root private keys must be kept in tr.cryptoService to be able to sign
// for rotation, and root certificates must be kept in tr.Root.SignedKeys
// because we are not necessarily storing them elsewhere (tuf.Repo does not
// depend on certs.Manager, that is an upper layer), and without storing
// the certificates in their x509 form we are not able to do the
// util.CanonicalKeyID conversion.
if role != data.CanonicalRootRole {
for k := range toDelete {
delete(tr.Root.Signed.Keys, k)
tr.cryptoService.RemoveKey(k)
}
}
@ -459,6 +492,7 @@ func (tr *Repo) InitRoot(root, timestamp, snapshot, targets data.BaseRole, consi
return err
}
tr.Root = r
tr.originalRootRole = root
return nil
}
@ -518,7 +552,11 @@ func (tr *Repo) InitTimestamp() error {
// SetRoot sets the Repo.Root field to the SignedRoot object.
func (tr *Repo) SetRoot(s *data.SignedRoot) error {
tr.Root = s
return nil
var err error
// originalRootRole is the root role prior to any mutations that might
// occur on tr.Root.
tr.originalRootRole, err = tr.Root.BuildBaseRole(data.CanonicalRootRole)
return err
}
// SetTimestamp parses the Signed object into a SignedTimestamp object
@ -781,32 +819,161 @@ func (tr *Repo) UpdateTimestamp(s *data.Signed) error {
if err != nil {
return err
}
tr.Timestamp.Signed.Meta["snapshot"] = meta
tr.Timestamp.Signed.Meta[data.CanonicalSnapshotRole] = meta
tr.Timestamp.Dirty = true
return nil
}
// SignRoot signs the root
type versionedRootRole struct {
data.BaseRole
version int
}
type versionedRootRoles []versionedRootRole
func (v versionedRootRoles) Len() int { return len(v) }
func (v versionedRootRoles) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
func (v versionedRootRoles) Less(i, j int) bool { return v[i].version < v[j].version }
// SignRoot signs the root, using all keys from the "root" role (i.e. currently trusted)
// as well as available keys used to sign the previous version, if the public part is
// carried in tr.Root.Keys and the private key is available (i.e. probably previously
// trusted keys, to allow rollover). If there are any errors, attempt to put root
// back to the way it was (so version won't be incremented, for instance).
func (tr *Repo) SignRoot(expires time.Time) (*data.Signed, error) {
logrus.Debug("signing root...")
tr.Root.Signed.Expires = expires
tr.Root.Signed.Version++
root, err := tr.GetBaseRole(data.CanonicalRootRole)
// duplicate root and attempt to modify it rather than the existing root
rootBytes, err := tr.Root.MarshalJSON()
if err != nil {
return nil, err
}
signed, err := tr.Root.ToSigned()
tempRoot := data.SignedRoot{}
if err := json.Unmarshal(rootBytes, &tempRoot); err != nil {
return nil, err
}
currRoot, err := tr.GetBaseRole(data.CanonicalRootRole)
if err != nil {
return nil, err
}
signed, err = tr.sign(signed, root)
oldRootRoles := tr.getOldRootRoles()
var latestSavedRole data.BaseRole
rolesToSignWith := make([]data.BaseRole, 0, len(oldRootRoles))
if len(oldRootRoles) > 0 {
sort.Sort(oldRootRoles)
for _, vRole := range oldRootRoles {
rolesToSignWith = append(rolesToSignWith, vRole.BaseRole)
}
latest := rolesToSignWith[len(rolesToSignWith)-1]
latestSavedRole = data.BaseRole{
Name: data.CanonicalRootRole,
Threshold: latest.Threshold,
Keys: latest.Keys,
}
}
// if the root role has changed and original role had not been saved as a previous role, save it now
if !tr.originalRootRole.Equals(currRoot) && !tr.originalRootRole.Equals(latestSavedRole) {
rolesToSignWith = append(rolesToSignWith, tr.originalRootRole)
latestSavedRole = tr.originalRootRole
versionName := oldRootVersionName(tempRoot.Signed.Version)
tempRoot.Signed.Roles[versionName] = &data.RootRole{
KeyIDs: latestSavedRole.ListKeyIDs(), Threshold: latestSavedRole.Threshold}
}
tempRoot.Signed.Expires = expires
tempRoot.Signed.Version++
// if the current role doesn't match with the latest saved role, save it
if !currRoot.Equals(latestSavedRole) {
rolesToSignWith = append(rolesToSignWith, currRoot)
versionName := oldRootVersionName(tempRoot.Signed.Version)
tempRoot.Signed.Roles[versionName] = &data.RootRole{
KeyIDs: currRoot.ListKeyIDs(), Threshold: currRoot.Threshold}
}
signed, err := tempRoot.ToSigned()
if err != nil {
return nil, err
}
signed, err = tr.sign(signed, rolesToSignWith, tr.getOptionalRootKeys(rolesToSignWith))
if err != nil {
return nil, err
}
tr.Root = &tempRoot
tr.Root.Signatures = signed.Signatures
tr.originalRootRole = currRoot
return signed, nil
}
// get all the saved previous roles <= the current root version
func (tr *Repo) getOldRootRoles() versionedRootRoles {
oldRootRoles := make(versionedRootRoles, 0, len(tr.Root.Signed.Roles))
// now go through the old roles
for roleName := range tr.Root.Signed.Roles {
// ensure that the rolename matches our format and that the version is
// not too high
if data.ValidRole(roleName) {
continue
}
nameTokens := strings.Split(roleName, ".")
if len(nameTokens) != 2 || nameTokens[0] != data.CanonicalRootRole {
continue
}
version, err := strconv.Atoi(nameTokens[1])
if err != nil || version > tr.Root.Signed.Version {
continue
}
// ignore invalid roles, which shouldn't happen
oldRole, err := tr.Root.BuildBaseRole(roleName)
if err != nil {
continue
}
oldRootRoles = append(oldRootRoles, versionedRootRole{BaseRole: oldRole, version: version})
}
return oldRootRoles
}
// gets any extra optional root keys from the existing root.json signatures
// (because older repositories that have already done root rotation may not
// necessarily have older root roles)
func (tr *Repo) getOptionalRootKeys(signingRoles []data.BaseRole) []data.PublicKey {
oldKeysMap := make(map[string]data.PublicKey)
for _, oldSig := range tr.Root.Signatures {
if k, ok := tr.Root.Signed.Keys[oldSig.KeyID]; ok {
oldKeysMap[k.ID()] = k
}
}
for _, role := range signingRoles {
for keyID := range role.Keys {
delete(oldKeysMap, keyID)
}
}
oldKeys := make([]data.PublicKey, 0, len(oldKeysMap))
for _, key := range oldKeysMap {
oldKeys = append(oldKeys, key)
}
return oldKeys
}
func oldRootVersionName(version int) string {
return fmt.Sprintf("%s.%v", data.CanonicalRootRole, version)
}
// SignTargets signs the targets file for the given top level or delegated targets role
func (tr *Repo) SignTargets(role string, expires time.Time) (*data.Signed, error) {
logrus.Debugf("sign targets called for role %s", role)
@ -838,7 +1005,7 @@ func (tr *Repo) SignTargets(role string, expires time.Time) (*data.Signed, error
return nil, err
}
signed, err = tr.sign(signed, targets)
signed, err = tr.sign(signed, []data.BaseRole{targets}, nil)
if err != nil {
logrus.Debug("errored signing ", role)
return nil, err
@ -854,7 +1021,7 @@ func (tr *Repo) SignSnapshot(expires time.Time) (*data.Signed, error) {
if err != nil {
return nil, err
}
err = tr.UpdateSnapshot("root", signedRoot)
err = tr.UpdateSnapshot(data.CanonicalRootRole, signedRoot)
if err != nil {
return nil, err
}
@ -880,7 +1047,7 @@ func (tr *Repo) SignSnapshot(expires time.Time) (*data.Signed, error) {
if err != nil {
return nil, err
}
signed, err = tr.sign(signed, snapshot)
signed, err = tr.sign(signed, []data.BaseRole{snapshot}, nil)
if err != nil {
return nil, err
}
@ -909,7 +1076,7 @@ func (tr *Repo) SignTimestamp(expires time.Time) (*data.Signed, error) {
if err != nil {
return nil, err
}
signed, err = tr.sign(signed, timestamp)
signed, err = tr.sign(signed, []data.BaseRole{timestamp}, nil)
if err != nil {
return nil, err
}
@ -918,9 +1085,17 @@ func (tr *Repo) SignTimestamp(expires time.Time) (*data.Signed, error) {
return signed, nil
}
func (tr Repo) sign(signedData *data.Signed, role data.BaseRole) (*data.Signed, error) {
if err := signed.Sign(tr.cryptoService, signedData, role.ListKeys()...); err != nil {
return nil, err
func (tr Repo) sign(signedData *data.Signed, roles []data.BaseRole, optionalKeys []data.PublicKey) (*data.Signed, error) {
validKeys := optionalKeys
for _, r := range roles {
roleKeys := r.ListKeys()
validKeys = append(roleKeys, validKeys...)
if err := signed.Sign(tr.cryptoService, signedData, roleKeys, r.Threshold, validKeys); err != nil {
return nil, err
}
}
// Attempt to sign with the optional keys, but ignore any errors, because these keys are optional
signed.Sign(tr.cryptoService, signedData, optionalKeys, 0, validKeys)
return signedData, nil
}

View file

@ -98,7 +98,7 @@ func HashedPaths(path string, hashes data.Hashes) []string {
// CanonicalKeyID returns the ID of the public bytes version of a TUF key.
// On regular RSA/ECDSA TUF keys, this is just the key ID. On X509 RSA/ECDSA
// TUF keys, this is the key ID of the public key part of the key.
// TUF keys, this is the key ID of the public key part of the key in the leaf cert
func CanonicalKeyID(k data.PublicKey) (string, error) {
switch k.Algorithm() {
case data.ECDSAx509Key, data.RSAx509Key: