vendor: update notary and deps for new trust commands

Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
This commit is contained in:
Riyaz Faizullabhoy
2017-09-26 11:33:02 -07:00
parent 3352c0e137
commit f47b1a3c6d
99 changed files with 17406 additions and 0 deletions

View File

@ -0,0 +1,202 @@
// Package auth defines a standard interface for request access controllers.
//
// An access controller has a simple interface with a single `Authorized`
// method which checks that a given request is authorized to perform one or
// more actions on one or more resources. This method should return a non-nil
// error if the request is not authorized.
//
// An implementation registers its access controller by name with a constructor
// which accepts an options map for configuring the access controller.
//
// options := map[string]interface{}{"sillySecret": "whysosilly?"}
// accessController, _ := auth.GetAccessController("silly", options)
//
// This `accessController` can then be used in a request handler like so:
//
// func updateOrder(w http.ResponseWriter, r *http.Request) {
// orderNumber := r.FormValue("orderNumber")
// resource := auth.Resource{Type: "customerOrder", Name: orderNumber}
// access := auth.Access{Resource: resource, Action: "update"}
//
// if ctx, err := accessController.Authorized(ctx, access); err != nil {
// if challenge, ok := err.(auth.Challenge) {
// // Let the challenge write the response.
// challenge.SetHeaders(w)
// w.WriteHeader(http.StatusUnauthorized)
// return
// } else {
// // Some other error.
// }
// }
// }
//
package auth
import (
"errors"
"fmt"
"net/http"
"github.com/docker/distribution/context"
)
const (
// UserKey is used to get the user object from
// a user context
UserKey = "auth.user"
// UserNameKey is used to get the user name from
// a user context
UserNameKey = "auth.user.name"
)
var (
// ErrInvalidCredential is returned when the auth token does not authenticate correctly.
ErrInvalidCredential = errors.New("invalid authorization credential")
// ErrAuthenticationFailure returned when authentication fails.
ErrAuthenticationFailure = errors.New("authentication failure")
)
// UserInfo carries information about
// an autenticated/authorized client.
type UserInfo struct {
Name string
}
// Resource describes a resource by type and name.
type Resource struct {
Type string
Class string
Name string
}
// Access describes a specific action that is
// requested or allowed for a given resource.
type Access struct {
Resource
Action string
}
// Challenge is a special error type which is used for HTTP 401 Unauthorized
// responses and is able to write the response with WWW-Authenticate challenge
// header values based on the error.
type Challenge interface {
error
// SetHeaders prepares the request to conduct a challenge response by
// adding the an HTTP challenge header on the response message. Callers
// are expected to set the appropriate HTTP status code (e.g. 401)
// themselves.
SetHeaders(w http.ResponseWriter)
}
// AccessController controls access to registry resources based on a request
// and required access levels for a request. Implementations can support both
// complete denial and http authorization challenges.
type AccessController interface {
// Authorized returns a non-nil error if the context is granted access and
// returns a new authorized context. If one or more Access structs are
// provided, the requested access will be compared with what is available
// to the context. The given context will contain a "http.request" key with
// a `*http.Request` value. If the error is non-nil, access should always
// be denied. The error may be of type Challenge, in which case the caller
// may have the Challenge handle the request or choose what action to take
// based on the Challenge header or response status. The returned context
// object should have a "auth.user" value set to a UserInfo struct.
Authorized(ctx context.Context, access ...Access) (context.Context, error)
}
// CredentialAuthenticator is an object which is able to authenticate credentials
type CredentialAuthenticator interface {
AuthenticateUser(username, password string) error
}
// WithUser returns a context with the authorized user info.
func WithUser(ctx context.Context, user UserInfo) context.Context {
return userInfoContext{
Context: ctx,
user: user,
}
}
type userInfoContext struct {
context.Context
user UserInfo
}
func (uic userInfoContext) Value(key interface{}) interface{} {
switch key {
case UserKey:
return uic.user
case UserNameKey:
return uic.user.Name
}
return uic.Context.Value(key)
}
// WithResources returns a context with the authorized resources.
func WithResources(ctx context.Context, resources []Resource) context.Context {
return resourceContext{
Context: ctx,
resources: resources,
}
}
type resourceContext struct {
context.Context
resources []Resource
}
type resourceKey struct{}
func (rc resourceContext) Value(key interface{}) interface{} {
if key == (resourceKey{}) {
return rc.resources
}
return rc.Context.Value(key)
}
// AuthorizedResources returns the list of resources which have
// been authorized for this request.
func AuthorizedResources(ctx context.Context) []Resource {
if resources, ok := ctx.Value(resourceKey{}).([]Resource); ok {
return resources
}
return nil
}
// InitFunc is the type of an AccessController factory function and is used
// to register the constructor for different AccesController backends.
type InitFunc func(options map[string]interface{}) (AccessController, error)
var accessControllers map[string]InitFunc
func init() {
accessControllers = make(map[string]InitFunc)
}
// Register is used to register an InitFunc for
// an AccessController backend with the given name.
func Register(name string, initFunc InitFunc) error {
if _, exists := accessControllers[name]; exists {
return fmt.Errorf("name already registered: %s", name)
}
accessControllers[name] = initFunc
return nil
}
// GetAccessController constructs an AccessController
// with the given options using the named backend.
func GetAccessController(name string, options map[string]interface{}) (AccessController, error) {
if initFunc, exists := accessControllers[name]; exists {
return initFunc(options)
}
return nil, fmt.Errorf("no access controller registered with name: %s", name)
}

277
vendor/github.com/docker/notary/utils/configuration.go generated vendored Normal file
View File

@ -0,0 +1,277 @@
// Common configuration elements that may be resused
package utils
import (
"crypto/tls"
"fmt"
"os"
"os/signal"
"path/filepath"
"strings"
bugsnag_hook "github.com/Shopify/logrus-bugsnag"
"github.com/bugsnag/bugsnag-go"
"github.com/docker/go-connections/tlsconfig"
"github.com/go-sql-driver/mysql"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/docker/notary"
)
// Storage is a configuration about what storage backend a server should use
type Storage struct {
Backend string
Source string
}
// RethinkDBStorage is configuration about a RethinkDB backend service
type RethinkDBStorage struct {
Storage
CA string
Cert string
DBName string
Key string
Username string
Password string
}
// GetPathRelativeToConfig gets a configuration key which is a path, and if
// it is not empty or an absolute path, returns the absolute path relative
// to the configuration file
func GetPathRelativeToConfig(configuration *viper.Viper, key string) string {
configFile := configuration.ConfigFileUsed()
p := configuration.GetString(key)
if p == "" || filepath.IsAbs(p) {
return p
}
return filepath.Clean(filepath.Join(filepath.Dir(configFile), p))
}
// ParseServerTLS tries to parse out valid server TLS options from a Viper.
// The cert/key files are relative to the config file used to populate the instance
// of viper.
func ParseServerTLS(configuration *viper.Viper, tlsRequired bool) (*tls.Config, error) {
// unmarshalling into objects does not seem to pick up env vars
tlsOpts := tlsconfig.Options{
CertFile: GetPathRelativeToConfig(configuration, "server.tls_cert_file"),
KeyFile: GetPathRelativeToConfig(configuration, "server.tls_key_file"),
CAFile: GetPathRelativeToConfig(configuration, "server.client_ca_file"),
ExclusiveRootPools: true,
}
if tlsOpts.CAFile != "" {
tlsOpts.ClientAuth = tls.RequireAndVerifyClientCert
}
if !tlsRequired {
cert, key, ca := tlsOpts.CertFile, tlsOpts.KeyFile, tlsOpts.CAFile
if cert == "" && key == "" && ca == "" {
return nil, nil
}
if (cert == "" && key != "") || (cert != "" && key == "") || (cert == "" && key == "" && ca != "") {
return nil, fmt.Errorf(
"either include both a cert and key file, or no TLS information at all to disable TLS")
}
}
return tlsconfig.Server(tlsOpts)
}
// ParseLogLevel tries to parse out a log level from a Viper. If there is no
// configuration, defaults to the provided error level
func ParseLogLevel(configuration *viper.Viper, defaultLevel logrus.Level) (
logrus.Level, error) {
logStr := configuration.GetString("logging.level")
if logStr == "" {
return defaultLevel, nil
}
return logrus.ParseLevel(logStr)
}
// ParseSQLStorage tries to parse out Storage from a Viper. If backend and
// URL are not provided, returns a nil pointer. Storage is required (if
// a backend is not provided, an error will be returned.)
func ParseSQLStorage(configuration *viper.Viper) (*Storage, error) {
store := Storage{
Backend: configuration.GetString("storage.backend"),
Source: configuration.GetString("storage.db_url"),
}
switch {
case store.Backend != notary.MySQLBackend && store.Backend != notary.SQLiteBackend && store.Backend != notary.PostgresBackend:
return nil, fmt.Errorf(
"%s is not a supported SQL backend driver",
store.Backend,
)
case store.Source == "":
return nil, fmt.Errorf(
"must provide a non-empty database source for %s",
store.Backend,
)
case store.Backend == notary.MySQLBackend:
urlConfig, err := mysql.ParseDSN(store.Source)
if err != nil {
return nil, fmt.Errorf("failed to parse the database source for %s",
store.Backend,
)
}
urlConfig.ParseTime = true
store.Source = urlConfig.FormatDSN()
}
return &store, nil
}
// ParseRethinkDBStorage tries to parse out Storage from a Viper. If backend and
// URL are not provided, returns a nil pointer. Storage is required (if
// a backend is not provided, an error will be returned.)
func ParseRethinkDBStorage(configuration *viper.Viper) (*RethinkDBStorage, error) {
store := RethinkDBStorage{
Storage: Storage{
Backend: configuration.GetString("storage.backend"),
Source: configuration.GetString("storage.db_url"),
},
CA: GetPathRelativeToConfig(configuration, "storage.tls_ca_file"),
Cert: GetPathRelativeToConfig(configuration, "storage.client_cert_file"),
Key: GetPathRelativeToConfig(configuration, "storage.client_key_file"),
DBName: configuration.GetString("storage.database"),
Username: configuration.GetString("storage.username"),
Password: configuration.GetString("storage.password"),
}
switch {
case store.Backend != notary.RethinkDBBackend:
return nil, fmt.Errorf(
"%s is not a supported RethinkDB backend driver",
store.Backend,
)
case store.Source == "":
return nil, fmt.Errorf(
"must provide a non-empty host:port for %s",
store.Backend,
)
case store.CA == "":
return nil, fmt.Errorf(
"cowardly refusal to connect to %s without a CA cert",
store.Backend,
)
case store.Cert == "" || store.Key == "":
return nil, fmt.Errorf(
"cowardly refusal to connect to %s without a client cert and key",
store.Backend,
)
case store.DBName == "":
return nil, fmt.Errorf(
"%s requires a specific database to connect to",
store.Backend,
)
case store.Username == "":
return nil, fmt.Errorf(
"%s requires a username to connect to the db",
store.Backend,
)
}
return &store, nil
}
// ParseBugsnag tries to parse out a Bugsnag Configuration from a Viper.
// If no values are provided, returns a nil pointer.
func ParseBugsnag(configuration *viper.Viper) (*bugsnag.Configuration, error) {
// can't unmarshal because we can't add tags to the bugsnag.Configuration
// struct
bugconf := bugsnag.Configuration{
APIKey: configuration.GetString("reporting.bugsnag.api_key"),
ReleaseStage: configuration.GetString("reporting.bugsnag.release_stage"),
Endpoint: configuration.GetString("reporting.bugsnag.endpoint"),
}
if bugconf.APIKey == "" && bugconf.ReleaseStage == "" && bugconf.Endpoint == "" {
return nil, nil
}
if bugconf.APIKey == "" {
return nil, fmt.Errorf("must provide an API key for bugsnag")
}
return &bugconf, nil
}
// utilities for setting up/acting on common configurations
// SetupViper sets up an instance of viper to also look at environment
// variables
func SetupViper(v *viper.Viper, envPrefix string) {
v.SetEnvPrefix(envPrefix)
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
}
// SetUpBugsnag configures bugsnag and sets up a logrus hook
func SetUpBugsnag(config *bugsnag.Configuration) error {
if config != nil {
bugsnag.Configure(*config)
hook, err := bugsnag_hook.NewBugsnagHook()
if err != nil {
return err
}
logrus.AddHook(hook)
logrus.Debug("Adding logrus hook for Bugsnag")
}
return nil
}
// ParseViper tries to parse out a Viper from a configuration file.
func ParseViper(v *viper.Viper, configFile string) error {
filename := filepath.Base(configFile)
ext := filepath.Ext(configFile)
configPath := filepath.Dir(configFile)
v.SetConfigType(strings.TrimPrefix(ext, "."))
v.SetConfigName(strings.TrimSuffix(filename, ext))
v.AddConfigPath(configPath)
if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("Could not read config at :%s, viper error: %v", configFile, err)
}
return nil
}
// AdjustLogLevel increases/decreases the log level, return error if the operation is invaild.
func AdjustLogLevel(increment bool) error {
lvl := logrus.GetLevel()
// The log level seems not possible, in the foreseeable future,
// out of range [Panic, Debug]
if increment {
if lvl == logrus.DebugLevel {
return fmt.Errorf("log level can not be set higher than %s", "Debug")
}
lvl++
} else {
if lvl == logrus.PanicLevel {
return fmt.Errorf("log level can not be set lower than %s", "Panic")
}
lvl--
}
logrus.SetLevel(lvl)
return nil
}
// SetupSignalTrap is a utility to trap supported signals hand handle them (currently by increasing logging)
func SetupSignalTrap(handler func(os.Signal)) chan os.Signal {
if len(notary.NotarySupportedSignals) == 0 {
return nil
}
c := make(chan os.Signal, 1)
signal.Notify(c, notary.NotarySupportedSignals...)
go func() {
for {
handler(<-c)
}
}()
return c
}

View File

@ -0,0 +1,29 @@
// +build !windows
package utils
import (
"fmt"
"os"
"syscall"
"github.com/sirupsen/logrus"
)
// LogLevelSignalHandle will increase/decrease the logging level via the signal we get.
func LogLevelSignalHandle(sig os.Signal) {
switch sig {
case syscall.SIGUSR1:
if err := AdjustLogLevel(true); err != nil {
fmt.Printf("Attempt to increase log level failed, will remain at %s level, error: %s\n", logrus.GetLevel(), err)
return
}
case syscall.SIGUSR2:
if err := AdjustLogLevel(false); err != nil {
fmt.Printf("Attempt to decrease log level failed, will remain at %s level, error: %s\n", logrus.GetLevel(), err)
return
}
}
fmt.Println("Successfully setting log level to", logrus.GetLevel())
}

View File

@ -0,0 +1,9 @@
// +build windows
package utils
import "os"
// LogLevelSignalHandle will do nothing, because we aren't currently supporting signal handling in windows
func LogLevelSignalHandle(sig os.Signal) {
}

252
vendor/github.com/docker/notary/utils/http.go generated vendored Normal file
View File

@ -0,0 +1,252 @@
package utils
import (
"fmt"
"net/http"
"time"
ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/auth"
"github.com/gorilla/mux"
"golang.org/x/net/context"
"github.com/docker/notary"
"github.com/docker/notary/tuf/signed"
)
// ContextHandler defines an alternate HTTP handler interface which takes in
// a context for authorization and returns an HTTP application error.
type ContextHandler func(ctx context.Context, w http.ResponseWriter, r *http.Request) error
// rootHandler is an implementation of an HTTP request handler which handles
// authorization and calling out to the defined alternate http handler.
type rootHandler struct {
handler ContextHandler
auth auth.AccessController
actions []string
context context.Context
trust signed.CryptoService
}
// AuthWrapper wraps a Handler with and Auth requirement
type AuthWrapper func(ContextHandler, ...string) *rootHandler
// RootHandlerFactory creates a new rootHandler factory using the given
// Context creator and authorizer. The returned factory allows creating
// new rootHandlers from the alternate http handler contextHandler and
// a scope.
func RootHandlerFactory(ctx context.Context, auth auth.AccessController, trust signed.CryptoService) func(ContextHandler, ...string) *rootHandler {
return func(handler ContextHandler, actions ...string) *rootHandler {
return &rootHandler{
handler: handler,
auth: auth,
actions: actions,
context: ctx,
trust: trust,
}
}
}
// ServeHTTP serves an HTTP request and implements the http.Handler interface.
func (root *rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
err error
ctx = ctxu.WithRequest(root.context, r)
log = ctxu.GetRequestLogger(ctx)
vars = mux.Vars(r)
)
ctx, w = ctxu.WithResponseWriter(ctx, w)
ctx = ctxu.WithLogger(ctx, log)
ctx = context.WithValue(ctx, notary.CtxKeyCryptoSvc, root.trust)
defer func(ctx context.Context) {
ctxu.GetResponseLogger(ctx).Info("response completed")
}(ctx)
if root.auth != nil {
ctx = context.WithValue(ctx, notary.CtxKeyRepo, vars["gun"])
if ctx, err = root.doAuth(ctx, vars["gun"], w); err != nil {
// errors have already been logged/output to w inside doAuth
// just return
return
}
}
if err := root.handler(ctx, w, r); err != nil {
serveError(log, w, err)
}
}
func serveError(log ctxu.Logger, w http.ResponseWriter, err error) {
if httpErr, ok := err.(errcode.Error); ok {
// info level logging for non-5XX http errors
httpErrCode := httpErr.ErrorCode().Descriptor().HTTPStatusCode
if httpErrCode >= http.StatusInternalServerError {
// error level logging for 5XX http errors
log.Errorf("%s: %s: %v", httpErr.ErrorCode().Error(), httpErr.Message, httpErr.Detail)
} else {
log.Infof("%s: %s: %v", httpErr.ErrorCode().Error(), httpErr.Message, httpErr.Detail)
}
}
e := errcode.ServeJSON(w, err)
if e != nil {
log.Error(e)
}
return
}
func (root *rootHandler) doAuth(ctx context.Context, gun string, w http.ResponseWriter) (context.Context, error) {
var access []auth.Access
if gun == "" {
access = buildCatalogRecord(root.actions...)
} else {
access = buildAccessRecords(gun, root.actions...)
}
log := ctxu.GetRequestLogger(ctx)
var authCtx context.Context
var err error
if authCtx, err = root.auth.Authorized(ctx, access...); err != nil {
if challenge, ok := err.(auth.Challenge); ok {
// Let the challenge write the response.
challenge.SetHeaders(w)
if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized.WithDetail(access)); err != nil {
log.Errorf("failed to serve challenge response: %s", err.Error())
return nil, err
}
return nil, err
}
errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized)
return nil, err
}
return authCtx, nil
}
func buildAccessRecords(repo string, actions ...string) []auth.Access {
requiredAccess := make([]auth.Access, 0, len(actions))
for _, action := range actions {
requiredAccess = append(requiredAccess, auth.Access{
Resource: auth.Resource{
Type: "repository",
Name: repo,
},
Action: action,
})
}
return requiredAccess
}
// buildCatalogRecord returns the only valid format for the catalog
// resource. Only admins can get this access level from the token
// server.
func buildCatalogRecord(actions ...string) []auth.Access {
requiredAccess := []auth.Access{{
Resource: auth.Resource{
Type: "registry",
Name: "catalog",
},
Action: "*",
}}
return requiredAccess
}
// CacheControlConfig is an interface for something that knows how to set cache
// control headers
type CacheControlConfig interface {
// SetHeaders will actually set the cache control headers on a Headers object
SetHeaders(headers http.Header)
}
// NewCacheControlConfig returns CacheControlConfig interface for either setting
// cache control or disabling cache control entirely
func NewCacheControlConfig(maxAgeInSeconds int, mustRevalidate bool) CacheControlConfig {
if maxAgeInSeconds > 0 {
return PublicCacheControl{MustReValidate: mustRevalidate, MaxAgeInSeconds: maxAgeInSeconds}
}
return NoCacheControl{}
}
// PublicCacheControl is a set of options that we will set to enable cache control
type PublicCacheControl struct {
MustReValidate bool
MaxAgeInSeconds int
}
// SetHeaders sets the public headers with an optional must-revalidate header
func (p PublicCacheControl) SetHeaders(headers http.Header) {
cacheControlValue := fmt.Sprintf("public, max-age=%v, s-maxage=%v",
p.MaxAgeInSeconds, p.MaxAgeInSeconds)
if p.MustReValidate {
cacheControlValue = fmt.Sprintf("%s, must-revalidate", cacheControlValue)
}
headers.Set("Cache-Control", cacheControlValue)
// delete the Pragma directive, because the only valid value in HTTP is
// "no-cache"
headers.Del("Pragma")
if headers.Get("Last-Modified") == "" {
SetLastModifiedHeader(headers, time.Time{})
}
}
// NoCacheControl is an object which represents a directive to cache nothing
type NoCacheControl struct{}
// SetHeaders sets the public headers cache-control headers and pragma to no-cache
func (n NoCacheControl) SetHeaders(headers http.Header) {
headers.Set("Cache-Control", "max-age=0, no-cache, no-store")
headers.Set("Pragma", "no-cache")
}
// cacheControlResponseWriter wraps an existing response writer, and if Write is
// called, will try to set the cache control headers if it can
type cacheControlResponseWriter struct {
http.ResponseWriter
config CacheControlConfig
statusCode int
}
// WriteHeader stores the header before writing it, so we can tell if it's been set
// to a non-200 status code
func (c *cacheControlResponseWriter) WriteHeader(statusCode int) {
c.statusCode = statusCode
c.ResponseWriter.WriteHeader(statusCode)
}
// Write will set the cache headers if they haven't already been set and if the status
// code has either not been set or set to 200
func (c *cacheControlResponseWriter) Write(data []byte) (int, error) {
if c.statusCode == http.StatusOK || c.statusCode == 0 {
headers := c.ResponseWriter.Header()
if headers.Get("Cache-Control") == "" {
c.config.SetHeaders(headers)
}
}
return c.ResponseWriter.Write(data)
}
type cacheControlHandler struct {
http.Handler
config CacheControlConfig
}
func (c cacheControlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.Handler.ServeHTTP(&cacheControlResponseWriter{ResponseWriter: w, config: c.config}, r)
}
// WrapWithCacheHandler wraps another handler in one that can add cache control headers
// given a 200 response
func WrapWithCacheHandler(ccc CacheControlConfig, handler http.Handler) http.Handler {
if ccc != nil {
return cacheControlHandler{Handler: handler, config: ccc}
}
return handler
}
// SetLastModifiedHeader takes a time and uses it to set the LastModified header using
// the right date format
func SetLastModifiedHeader(headers http.Header, lmt time.Time) {
headers.Set("Last-Modified", lmt.Format(time.RFC1123))
}

240
vendor/github.com/docker/notary/utils/keys.go generated vendored Normal file
View File

@ -0,0 +1,240 @@
package utils
import (
"encoding/pem"
"errors"
"io"
"io/ioutil"
"path/filepath"
"sort"
"strings"
"github.com/docker/notary"
tufdata "github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/utils"
"github.com/sirupsen/logrus"
)
// Exporter is a simple interface for the two functions we need from the Storage interface
type Exporter interface {
Get(string) ([]byte, error)
ListFiles() []string
}
// Importer is a simple interface for the one function we need from the Storage interface
type Importer interface {
Set(string, []byte) error
}
// ExportKeysByGUN exports all keys filtered to a GUN
func ExportKeysByGUN(to io.Writer, s Exporter, gun string) error {
keys := s.ListFiles()
sort.Strings(keys) // ensure consistency. ListFiles has no order guarantee
for _, loc := range keys {
keyFile, err := s.Get(loc)
if err != nil {
logrus.Warn("Could not parse key file at ", loc)
continue
}
block, _ := pem.Decode(keyFile)
keyGun := block.Headers["gun"]
if keyGun == gun { // must be full GUN match
if err := ExportKeys(to, s, loc); err != nil {
return err
}
}
}
return nil
}
// ExportKeysByID exports all keys matching the given ID
func ExportKeysByID(to io.Writer, s Exporter, ids []string) error {
want := make(map[string]struct{})
for _, id := range ids {
want[id] = struct{}{}
}
keys := s.ListFiles()
for _, k := range keys {
id := filepath.Base(k)
if _, ok := want[id]; ok {
if err := ExportKeys(to, s, k); err != nil {
return err
}
}
}
return nil
}
// ExportKeys copies a key from the store to the io.Writer
func ExportKeys(to io.Writer, s Exporter, from string) error {
// get PEM block
k, err := s.Get(from)
if err != nil {
return err
}
// parse PEM blocks if there are more than one
for block, rest := pem.Decode(k); block != nil; block, rest = pem.Decode(rest) {
// add from path in a header for later import
block.Headers["path"] = from
// write serialized PEM
err = pem.Encode(to, block)
if err != nil {
return err
}
}
return nil
}
// ImportKeys expects an io.Reader containing one or more PEM blocks.
// It reads PEM blocks one at a time until pem.Decode returns a nil
// block.
// Each block is written to the subpath indicated in the "path" PEM
// header. If the file already exists, the file is truncated. Multiple
// adjacent PEMs with the same "path" header are appended together.
func ImportKeys(from io.Reader, to []Importer, fallbackRole string, fallbackGUN string, passRet notary.PassRetriever) error {
// importLogic.md contains a small flowchart I made to clear up my understand while writing the cases in this function
// it is very rough, but it may help while reading this piece of code
data, err := ioutil.ReadAll(from)
if err != nil {
return err
}
var (
writeTo string
toWrite []byte
)
for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) {
handleLegacyPath(block)
setFallbacks(block, fallbackGUN, fallbackRole)
loc, err := checkValidity(block)
if err != nil {
// already logged in checkValidity
continue
}
// the path header is not of any use once we've imported the key so strip it away
delete(block.Headers, "path")
// we are now all set for import but let's first encrypt the key
blockBytes := pem.EncodeToMemory(block)
// check if key is encrypted, note: if it is encrypted at this point, it will have had a path header
if privKey, err := utils.ParsePEMPrivateKey(blockBytes, ""); err == nil {
// Key is not encrypted- ask for a passphrase and encrypt this key
var chosenPassphrase string
for attempts := 0; ; attempts++ {
var giveup bool
chosenPassphrase, giveup, err = passRet(loc, block.Headers["role"], true, attempts)
if err == nil {
break
}
if giveup || attempts > 10 {
return errors.New("maximum number of passphrase attempts exceeded")
}
}
blockBytes, err = utils.ConvertPrivateKeyToPKCS8(privKey, tufdata.RoleName(block.Headers["role"]), tufdata.GUN(block.Headers["gun"]), chosenPassphrase)
if err != nil {
return errors.New("failed to encrypt key with given passphrase")
}
}
if loc != writeTo {
// next location is different from previous one. We've finished aggregating
// data for the previous file. If we have data, write the previous file,
// clear toWrite and set writeTo to the next path we're going to write
if toWrite != nil {
if err = importToStores(to, writeTo, toWrite); err != nil {
return err
}
}
// set up for aggregating next file's data
toWrite = nil
writeTo = loc
}
toWrite = append(toWrite, blockBytes...)
}
if toWrite != nil { // close out final iteration if there's data left
return importToStores(to, writeTo, toWrite)
}
return nil
}
func handleLegacyPath(block *pem.Block) {
// if there is a legacy path then we set the gun header from this path
// this is the case when a user attempts to import a key bundle generated by an older client
if rawPath := block.Headers["path"]; rawPath != "" && rawPath != filepath.Base(rawPath) {
// this is a legacy filepath and we should try to deduce the gun name from it
pathWOFileName := filepath.Dir(rawPath)
if strings.HasPrefix(pathWOFileName, notary.NonRootKeysSubdir) {
// remove the notary keystore-specific segment of the path, and any potential leading or trailing slashes
gunName := strings.Trim(strings.TrimPrefix(pathWOFileName, notary.NonRootKeysSubdir), "/")
if gunName != "" {
block.Headers["gun"] = gunName
}
}
block.Headers["path"] = filepath.Base(rawPath)
}
}
func setFallbacks(block *pem.Block, fallbackGUN, fallbackRole string) {
if block.Headers["gun"] == "" {
if fallbackGUN != "" {
block.Headers["gun"] = fallbackGUN
}
}
if block.Headers["role"] == "" {
if fallbackRole == "" {
block.Headers["role"] = notary.DefaultImportRole
} else {
block.Headers["role"] = fallbackRole
}
}
}
// checkValidity ensures the fields in the pem headers are valid and parses out the location.
// While importing a collection of keys, errors from this function should result in only the
// current pem block being skipped.
func checkValidity(block *pem.Block) (string, error) {
// A root key or a delegations key should not have a gun
// Note that a key that is not any of the canonical roles (except root) is a delegations key and should not have a gun
switch block.Headers["role"] {
case tufdata.CanonicalSnapshotRole.String(), tufdata.CanonicalTargetsRole.String(), tufdata.CanonicalTimestampRole.String():
// check if the key is missing a gun header or has an empty gun and error out since we don't know what gun it belongs to
if block.Headers["gun"] == "" {
logrus.Warnf("failed to import key (%s) to store: Cannot have canonical role key without a gun, don't know what gun it belongs to", block.Headers["path"])
return "", errors.New("invalid key pem block")
}
default:
delete(block.Headers, "gun")
}
loc, ok := block.Headers["path"]
// only if the path isn't specified do we get into this parsing path logic
if !ok || loc == "" {
// if the path isn't specified, we will try to infer the path rel to trust dir from the role (and then gun)
// parse key for the keyID which we will save it by.
// if the key is encrypted at this point, we will generate an error and continue since we don't know the ID to save it by
decodedKey, err := utils.ParsePEMPrivateKey(pem.EncodeToMemory(block), "")
if err != nil {
logrus.Warn("failed to import key to store: Invalid key generated, key may be encrypted and does not contain path header")
return "", errors.New("invalid key pem block")
}
loc = decodedKey.ID()
}
return loc, nil
}
func importToStores(to []Importer, path string, bytes []byte) error {
var err error
for _, i := range to {
if err = i.Set(path, bytes); err != nil {
logrus.Errorf("failed to import key to store: %s", err.Error())
continue
}
break
}
return err
}