mirror of https://github.com/docker/cli.git
999 lines
32 KiB
Go
999 lines
32 KiB
Go
//Package client implements everything required for interacting with a Notary repository.
|
|
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
canonicaljson "github.com/docker/go/canonical/json"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/theupdateframework/notary"
|
|
"github.com/theupdateframework/notary/client/changelist"
|
|
"github.com/theupdateframework/notary/cryptoservice"
|
|
store "github.com/theupdateframework/notary/storage"
|
|
"github.com/theupdateframework/notary/trustpinning"
|
|
"github.com/theupdateframework/notary/tuf"
|
|
"github.com/theupdateframework/notary/tuf/data"
|
|
"github.com/theupdateframework/notary/tuf/signed"
|
|
"github.com/theupdateframework/notary/tuf/utils"
|
|
)
|
|
|
|
const (
|
|
tufDir = "tuf"
|
|
|
|
// SignWithAllOldVersions is a sentinel constant for LegacyVersions flag
|
|
SignWithAllOldVersions = -1
|
|
)
|
|
|
|
func init() {
|
|
data.SetDefaultExpiryTimes(data.NotaryDefaultExpiries)
|
|
}
|
|
|
|
// repository stores all the information needed to operate on a notary repository.
|
|
type repository struct {
|
|
gun data.GUN
|
|
baseURL string
|
|
changelist changelist.Changelist
|
|
cache store.MetadataStore
|
|
remoteStore store.RemoteStore
|
|
cryptoService signed.CryptoService
|
|
tufRepo *tuf.Repo
|
|
invalid *tuf.Repo // known data that was parsable but deemed invalid
|
|
roundTrip http.RoundTripper
|
|
trustPinning trustpinning.TrustPinConfig
|
|
LegacyVersions int // number of versions back to fetch roots to sign with
|
|
}
|
|
|
|
// NewFileCachedRepository is a wrapper for NewRepository that initializes
|
|
// a file cache from the provided repository, local config information and a crypto service.
|
|
// It also retrieves the remote store associated to the base directory under where all the
|
|
// trust files will be stored (This is normally defaults to "~/.notary" or "~/.docker/trust"
|
|
// when enabling Docker content trust) and the specified GUN.
|
|
//
|
|
// In case of a nil RoundTripper, a default offline store is used instead.
|
|
func NewFileCachedRepository(baseDir string, gun data.GUN, baseURL string, rt http.RoundTripper,
|
|
retriever notary.PassRetriever, trustPinning trustpinning.TrustPinConfig) (Repository, error) {
|
|
|
|
cache, err := store.NewFileStore(
|
|
filepath.Join(baseDir, tufDir, filepath.FromSlash(gun.String()), "metadata"),
|
|
"json",
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keyStores, err := getKeyStores(baseDir, retriever)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cryptoService := cryptoservice.NewCryptoService(keyStores...)
|
|
|
|
remoteStore, err := getRemoteStore(baseURL, gun, rt)
|
|
if err != nil {
|
|
// baseURL is syntactically invalid
|
|
return nil, err
|
|
}
|
|
|
|
cl, err := changelist.NewFileChangelist(filepath.Join(
|
|
filepath.Join(baseDir, tufDir, filepath.FromSlash(gun.String()), "changelist"),
|
|
))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return NewRepository(gun, baseURL, remoteStore, cache, trustPinning, cryptoService, cl)
|
|
}
|
|
|
|
// NewRepository is the base method that returns a new notary repository.
|
|
// It expects an initialized cache. In case of a nil remote store, a default
|
|
// offline store is used.
|
|
func NewRepository(gun data.GUN, baseURL string, remoteStore store.RemoteStore, cache store.MetadataStore,
|
|
trustPinning trustpinning.TrustPinConfig, cryptoService signed.CryptoService, cl changelist.Changelist) (Repository, error) {
|
|
|
|
// Repo's remote store is either a valid remote store or an OfflineStore
|
|
if remoteStore == nil {
|
|
remoteStore = store.OfflineStore{}
|
|
}
|
|
|
|
if cache == nil {
|
|
return nil, fmt.Errorf("got an invalid cache (nil metadata store)")
|
|
}
|
|
|
|
nRepo := &repository{
|
|
gun: gun,
|
|
baseURL: baseURL,
|
|
changelist: cl,
|
|
cache: cache,
|
|
remoteStore: remoteStore,
|
|
cryptoService: cryptoService,
|
|
trustPinning: trustPinning,
|
|
LegacyVersions: 0, // By default, don't sign with legacy roles
|
|
}
|
|
|
|
return nRepo, nil
|
|
}
|
|
|
|
// GetGUN is a getter for the GUN object from a Repository
|
|
func (r *repository) GetGUN() data.GUN {
|
|
return r.gun
|
|
}
|
|
|
|
func (r *repository) updateTUF(forWrite bool) error {
|
|
repo, invalid, err := LoadTUFRepo(TUFLoadOptions{
|
|
GUN: r.gun,
|
|
TrustPinning: r.trustPinning,
|
|
CryptoService: r.cryptoService,
|
|
Cache: r.cache,
|
|
RemoteStore: r.remoteStore,
|
|
AlwaysCheckInitialized: forWrite,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.tufRepo = repo
|
|
r.invalid = invalid
|
|
return nil
|
|
}
|
|
|
|
// ListTargets calls update first before listing targets
|
|
func (r *repository) ListTargets(roles ...data.RoleName) ([]*TargetWithRole, error) {
|
|
if err := r.updateTUF(false); err != nil {
|
|
return nil, err
|
|
}
|
|
return NewReadOnly(r.tufRepo).ListTargets(roles...)
|
|
}
|
|
|
|
// GetTargetByName calls update first before getting target by name
|
|
func (r *repository) GetTargetByName(name string, roles ...data.RoleName) (*TargetWithRole, error) {
|
|
if err := r.updateTUF(false); err != nil {
|
|
return nil, err
|
|
}
|
|
return NewReadOnly(r.tufRepo).GetTargetByName(name, roles...)
|
|
}
|
|
|
|
// GetAllTargetMetadataByName calls update first before getting targets by name
|
|
func (r *repository) GetAllTargetMetadataByName(name string) ([]TargetSignedStruct, error) {
|
|
if err := r.updateTUF(false); err != nil {
|
|
return nil, err
|
|
}
|
|
return NewReadOnly(r.tufRepo).GetAllTargetMetadataByName(name)
|
|
|
|
}
|
|
|
|
// ListRoles calls update first before getting roles
|
|
func (r *repository) ListRoles() ([]RoleWithSignatures, error) {
|
|
if err := r.updateTUF(false); err != nil {
|
|
return nil, err
|
|
}
|
|
return NewReadOnly(r.tufRepo).ListRoles()
|
|
}
|
|
|
|
// GetDelegationRoles calls update first before getting all delegation roles
|
|
func (r *repository) GetDelegationRoles() ([]data.Role, error) {
|
|
if err := r.updateTUF(false); err != nil {
|
|
return nil, err
|
|
}
|
|
return NewReadOnly(r.tufRepo).GetDelegationRoles()
|
|
}
|
|
|
|
// NewTarget is a helper method that returns a Target
|
|
func NewTarget(targetName, targetPath string, targetCustom *canonicaljson.RawMessage) (*Target, error) {
|
|
b, err := ioutil.ReadFile(targetPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
meta, err := data.NewFileMeta(bytes.NewBuffer(b), data.NotaryDefaultHashes...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Target{Name: targetName, Hashes: meta.Hashes, Length: meta.Length, Custom: targetCustom}, nil
|
|
}
|
|
|
|
// rootCertKey generates the corresponding certificate for the private key given the privKey and repo's GUN
|
|
func rootCertKey(gun data.GUN, privKey data.PrivateKey) (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, err
|
|
}
|
|
|
|
x509PublicKey := utils.CertToKey(cert)
|
|
if x509PublicKey == nil {
|
|
return nil, fmt.Errorf("cannot generate public key from private key with id: %v and algorithm: %v", privKey.ID(), privKey.Algorithm())
|
|
}
|
|
|
|
return x509PublicKey, nil
|
|
}
|
|
|
|
// GetCryptoService is the getter for the repository's CryptoService
|
|
func (r *repository) GetCryptoService() signed.CryptoService {
|
|
return r.cryptoService
|
|
}
|
|
|
|
// initialize initializes the notary repository with a set of rootkeys, root certificates and roles.
|
|
func (r *repository) initialize(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error {
|
|
|
|
// currently we only support server managing timestamps and snapshots, and
|
|
// nothing else - timestamps are always managed by the server, and implicit
|
|
// (do not have to be passed in as part of `serverManagedRoles`, so that
|
|
// the API of Initialize doesn't change).
|
|
var serverManagesSnapshot bool
|
|
locallyManagedKeys := []data.RoleName{
|
|
data.CanonicalTargetsRole,
|
|
data.CanonicalSnapshotRole,
|
|
// root is also locally managed, but that should have been created
|
|
// already
|
|
}
|
|
remotelyManagedKeys := []data.RoleName{data.CanonicalTimestampRole}
|
|
for _, role := range serverManagedRoles {
|
|
switch role {
|
|
case data.CanonicalTimestampRole:
|
|
continue // timestamp is already in the right place
|
|
case data.CanonicalSnapshotRole:
|
|
// because we put Snapshot last
|
|
locallyManagedKeys = []data.RoleName{data.CanonicalTargetsRole}
|
|
remotelyManagedKeys = append(
|
|
remotelyManagedKeys, data.CanonicalSnapshotRole)
|
|
serverManagesSnapshot = true
|
|
default:
|
|
return ErrInvalidRemoteRole{Role: role}
|
|
}
|
|
}
|
|
|
|
// gets valid public keys corresponding to the rootKeyIDs or generate if necessary
|
|
var publicKeys []data.PublicKey
|
|
var err error
|
|
if len(rootCerts) == 0 {
|
|
publicKeys, err = r.createNewPublicKeyFromKeyIDs(rootKeyIDs)
|
|
} else {
|
|
publicKeys, err = r.publicKeysOfKeyIDs(rootKeyIDs, rootCerts)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
//initialize repo with public keys
|
|
rootRole, targetsRole, snapshotRole, timestampRole, err := r.initializeRoles(
|
|
publicKeys,
|
|
locallyManagedKeys,
|
|
remotelyManagedKeys,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.tufRepo = tuf.NewRepo(r.GetCryptoService())
|
|
|
|
if err := r.tufRepo.InitRoot(
|
|
rootRole,
|
|
timestampRole,
|
|
snapshotRole,
|
|
targetsRole,
|
|
false,
|
|
); err != nil {
|
|
logrus.Debug("Error on InitRoot: ", err.Error())
|
|
return err
|
|
}
|
|
if _, err := r.tufRepo.InitTargets(data.CanonicalTargetsRole); err != nil {
|
|
logrus.Debug("Error on InitTargets: ", err.Error())
|
|
return err
|
|
}
|
|
if err := r.tufRepo.InitSnapshot(); err != nil {
|
|
logrus.Debug("Error on InitSnapshot: ", err.Error())
|
|
return err
|
|
}
|
|
|
|
return r.saveMetadata(serverManagesSnapshot)
|
|
}
|
|
|
|
// createNewPublicKeyFromKeyIDs generates a set of public keys corresponding to the given list of
|
|
// key IDs existing in the repository's CryptoService.
|
|
// the public keys returned are ordered to correspond to the keyIDs
|
|
func (r *repository) createNewPublicKeyFromKeyIDs(keyIDs []string) ([]data.PublicKey, error) {
|
|
publicKeys := []data.PublicKey{}
|
|
|
|
privKeys, err := getAllPrivKeys(keyIDs, r.GetCryptoService())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, privKey := range privKeys {
|
|
rootKey, err := rootCertKey(r.gun, privKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
publicKeys = append(publicKeys, rootKey)
|
|
}
|
|
return publicKeys, nil
|
|
}
|
|
|
|
// publicKeysOfKeyIDs confirms that the public key and private keys (by Key IDs) forms valid, strictly ordered key pairs
|
|
// (eg. keyIDs[0] must match pubKeys[0] and keyIDs[1] must match certs[1] and so on).
|
|
// Or throw error when they mismatch.
|
|
func (r *repository) publicKeysOfKeyIDs(keyIDs []string, pubKeys []data.PublicKey) ([]data.PublicKey, error) {
|
|
if len(keyIDs) != len(pubKeys) {
|
|
err := fmt.Errorf("require matching number of keyIDs and public keys but got %d IDs and %d public keys", len(keyIDs), len(pubKeys))
|
|
return nil, err
|
|
}
|
|
|
|
if err := matchKeyIdsWithPubKeys(r, keyIDs, pubKeys); err != nil {
|
|
return nil, fmt.Errorf("could not obtain public key from IDs: %v", err)
|
|
}
|
|
return pubKeys, nil
|
|
}
|
|
|
|
// matchKeyIdsWithPubKeys validates that the private keys (represented by their IDs) and the public keys
|
|
// forms matching key pairs
|
|
func matchKeyIdsWithPubKeys(r *repository, ids []string, pubKeys []data.PublicKey) error {
|
|
for i := 0; i < len(ids); i++ {
|
|
privKey, _, err := r.GetCryptoService().GetPrivateKey(ids[i])
|
|
if err != nil {
|
|
return fmt.Errorf("could not get the private key matching id %v: %v", ids[i], err)
|
|
}
|
|
|
|
pubKey := pubKeys[i]
|
|
err = signed.VerifyPublicKeyMatchesPrivateKey(privKey, pubKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Initialize creates a new repository by using rootKey as the root Key for the
|
|
// 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 *repository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error {
|
|
return r.initialize(rootKeyIDs, nil, serverManagedRoles...)
|
|
}
|
|
|
|
type errKeyNotFound struct{}
|
|
|
|
func (errKeyNotFound) Error() string {
|
|
return fmt.Sprintf("cannot find matching private key id")
|
|
}
|
|
|
|
// keyExistsInList returns the id of the private key in ids that matches the public key
|
|
// otherwise return empty string
|
|
func keyExistsInList(cert data.PublicKey, ids map[string]bool) error {
|
|
pubKeyID, err := utils.CanonicalKeyID(cert)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to obtain the public key id from the given certificate: %v", err)
|
|
}
|
|
if _, ok := ids[pubKeyID]; ok {
|
|
return nil
|
|
}
|
|
return errKeyNotFound{}
|
|
}
|
|
|
|
// InitializeWithCertificate initializes the repository with root keys and their corresponding certificates
|
|
func (r *repository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey,
|
|
serverManagedRoles ...data.RoleName) error {
|
|
|
|
// If we explicitly pass in certificate(s) but not key, then look keys up using certificate
|
|
if len(rootKeyIDs) == 0 && len(rootCerts) != 0 {
|
|
rootKeyIDs = []string{}
|
|
availableRootKeyIDs := make(map[string]bool)
|
|
for _, k := range r.GetCryptoService().ListKeys(data.CanonicalRootRole) {
|
|
availableRootKeyIDs[k] = true
|
|
}
|
|
|
|
for _, cert := range rootCerts {
|
|
if err := keyExistsInList(cert, availableRootKeyIDs); err != nil {
|
|
return fmt.Errorf("error initializing repository with certificate: %v", err)
|
|
}
|
|
keyID, _ := utils.CanonicalKeyID(cert)
|
|
rootKeyIDs = append(rootKeyIDs, keyID)
|
|
}
|
|
}
|
|
return r.initialize(rootKeyIDs, rootCerts, serverManagedRoles...)
|
|
}
|
|
|
|
func (r *repository) initializeRoles(rootKeys []data.PublicKey, localRoles, remoteRoles []data.RoleName) (
|
|
root, targets, snapshot, timestamp data.BaseRole, err error) {
|
|
root = data.NewBaseRole(
|
|
data.CanonicalRootRole,
|
|
notary.MinThreshold,
|
|
rootKeys...,
|
|
)
|
|
|
|
// we want to create all the local keys first so we don't have to
|
|
// make unnecessary network calls
|
|
for _, role := range localRoles {
|
|
// This is currently hardcoding the keys to ECDSA.
|
|
var key data.PublicKey
|
|
key, err = r.GetCryptoService().Create(role, r.gun, data.ECDSAKey)
|
|
if err != nil {
|
|
return
|
|
}
|
|
switch role {
|
|
case data.CanonicalSnapshotRole:
|
|
snapshot = data.NewBaseRole(
|
|
role,
|
|
notary.MinThreshold,
|
|
key,
|
|
)
|
|
case data.CanonicalTargetsRole:
|
|
targets = data.NewBaseRole(
|
|
role,
|
|
notary.MinThreshold,
|
|
key,
|
|
)
|
|
}
|
|
}
|
|
|
|
remote := r.getRemoteStore()
|
|
|
|
for _, role := range remoteRoles {
|
|
// This key is generated by the remote server.
|
|
var key data.PublicKey
|
|
key, err = getRemoteKey(role, remote)
|
|
if err != nil {
|
|
return
|
|
}
|
|
logrus.Debugf("got remote %s %s key with keyID: %s",
|
|
role, key.Algorithm(), key.ID())
|
|
switch role {
|
|
case data.CanonicalSnapshotRole:
|
|
snapshot = data.NewBaseRole(
|
|
role,
|
|
notary.MinThreshold,
|
|
key,
|
|
)
|
|
case data.CanonicalTimestampRole:
|
|
timestamp = data.NewBaseRole(
|
|
role,
|
|
notary.MinThreshold,
|
|
key,
|
|
)
|
|
}
|
|
}
|
|
return root, targets, snapshot, timestamp, nil
|
|
}
|
|
|
|
// adds a TUF Change template to the given roles
|
|
func addChange(cl changelist.Changelist, c changelist.Change, roles ...data.RoleName) error {
|
|
if len(roles) == 0 {
|
|
roles = []data.RoleName{data.CanonicalTargetsRole}
|
|
}
|
|
|
|
var changes []changelist.Change
|
|
for _, role := range roles {
|
|
// Ensure we can only add targets to the CanonicalTargetsRole,
|
|
// or a Delegation role (which is <CanonicalTargetsRole>/something else)
|
|
if role != data.CanonicalTargetsRole && !data.IsDelegation(role) && !data.IsWildDelegation(role) {
|
|
return data.ErrInvalidRole{
|
|
Role: role,
|
|
Reason: "cannot add targets to this role",
|
|
}
|
|
}
|
|
|
|
changes = append(changes, changelist.NewTUFChange(
|
|
c.Action(),
|
|
role,
|
|
c.Type(),
|
|
c.Path(),
|
|
c.Content(),
|
|
))
|
|
}
|
|
|
|
for _, c := range changes {
|
|
if err := cl.Add(c); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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"
|
|
func (r *repository) AddTarget(target *Target, roles ...data.RoleName) error {
|
|
if len(target.Hashes) == 0 {
|
|
return fmt.Errorf("no hashes specified for target \"%s\"", target.Name)
|
|
}
|
|
logrus.Debugf("Adding target \"%s\" with sha256 \"%x\" and size %d bytes.\n", target.Name, target.Hashes["sha256"], target.Length)
|
|
|
|
meta := data.FileMeta{Length: target.Length, Hashes: target.Hashes, Custom: target.Custom}
|
|
metaJSON, err := json.Marshal(meta)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
template := changelist.NewTUFChange(
|
|
changelist.ActionCreate, "", changelist.TypeTargetsTarget,
|
|
target.Name, metaJSON)
|
|
return addChange(r.changelist, template, roles...)
|
|
}
|
|
|
|
// RemoveTarget creates new changelist entries to remove a target from the given
|
|
// roles in the repository when the changelist gets applied at publish time.
|
|
// If roles are unspecified, the default role is "target".
|
|
func (r *repository) RemoveTarget(targetName string, roles ...data.RoleName) error {
|
|
logrus.Debugf("Removing target \"%s\"", targetName)
|
|
template := changelist.NewTUFChange(changelist.ActionDelete, "",
|
|
changelist.TypeTargetsTarget, targetName, nil)
|
|
return addChange(r.changelist, template, roles...)
|
|
}
|
|
|
|
// GetChangelist returns the list of the repository's unpublished changes
|
|
func (r *repository) GetChangelist() (changelist.Changelist, error) {
|
|
return r.changelist, nil
|
|
}
|
|
|
|
// getRemoteStore returns the remoteStore of a repository if valid or
|
|
// or an OfflineStore otherwise
|
|
func (r *repository) getRemoteStore() store.RemoteStore {
|
|
if r.remoteStore != nil {
|
|
return r.remoteStore
|
|
}
|
|
|
|
r.remoteStore = &store.OfflineStore{}
|
|
|
|
return r.remoteStore
|
|
}
|
|
|
|
// Publish pushes the local changes in signed material to the remote notary-server
|
|
// Conceptually it performs an operation similar to a `git rebase`
|
|
func (r *repository) Publish() error {
|
|
if err := r.publish(r.changelist); err != nil {
|
|
return err
|
|
}
|
|
if err := r.changelist.Clear(""); err != nil {
|
|
// This is not a critical problem when only a single host is pushing
|
|
// but will cause weird behaviour if changelist cleanup is failing
|
|
// and there are multiple hosts writing to the repo.
|
|
logrus.Warn("Unable to clear changelist. You may want to manually delete the folder ", r.changelist.Location())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// publish pushes the changes in the given changelist to the remote notary-server
|
|
// Conceptually it performs an operation similar to a `git rebase`
|
|
func (r *repository) publish(cl changelist.Changelist) error {
|
|
var initialPublish bool
|
|
// update first before publishing
|
|
if err := r.updateTUF(true); err != nil {
|
|
// If the remote is not aware of the repo, then this is being published
|
|
// for the first time. Try to initialize the repository before publishing.
|
|
if _, ok := err.(ErrRepositoryNotExist); ok {
|
|
err := r.bootstrapRepo()
|
|
if _, ok := err.(store.ErrMetaNotFound); ok {
|
|
logrus.Infof("No TUF data found locally or remotely - initializing repository %s for the first time", r.gun.String())
|
|
err = r.Initialize(nil)
|
|
}
|
|
|
|
if err != nil {
|
|
logrus.WithError(err).Debugf("Unable to load or initialize repository during first publish: %s", err.Error())
|
|
return err
|
|
}
|
|
|
|
// Ensure we will push the initial root and targets file. Either or
|
|
// both of the root and targets may not be marked as Dirty, since
|
|
// there may not be any changes that update them, so use a
|
|
// different boolean.
|
|
initialPublish = true
|
|
} else {
|
|
// We could not update, so we cannot publish.
|
|
logrus.Error("Could not publish Repository since we could not update: ", err.Error())
|
|
return err
|
|
}
|
|
}
|
|
// apply the changelist to the repo
|
|
if err := applyChangelist(r.tufRepo, r.invalid, cl); err != nil {
|
|
logrus.Debug("Error applying changelist")
|
|
return err
|
|
}
|
|
|
|
// these are the TUF files we will need to update, serialized as JSON before
|
|
// we send anything to remote
|
|
updatedFiles := make(map[data.RoleName][]byte)
|
|
|
|
// Fetch old keys to support old clients
|
|
legacyKeys, err := r.oldKeysForLegacyClientSupport(r.LegacyVersions, initialPublish)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// check if our root file is nearing expiry or dirty. Resign if it is. If
|
|
// root is not dirty but we are publishing for the first time, then just
|
|
// publish the existing root we have.
|
|
if err := signRootIfNecessary(updatedFiles, r.tufRepo, legacyKeys, initialPublish); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := signTargets(updatedFiles, r.tufRepo, initialPublish); err != nil {
|
|
return err
|
|
}
|
|
|
|
// if we initialized the repo while designating the server as the snapshot
|
|
// signer, then there won't be a snapshots file. However, we might now
|
|
// have a local key (if there was a rotation), so initialize one.
|
|
if r.tufRepo.Snapshot == nil {
|
|
if err := r.tufRepo.InitSnapshot(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if snapshotJSON, err := serializeCanonicalRole(
|
|
r.tufRepo, data.CanonicalSnapshotRole, nil); err == nil {
|
|
// Only update the snapshot if we've successfully signed it.
|
|
updatedFiles[data.CanonicalSnapshotRole] = snapshotJSON
|
|
} 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.
|
|
logrus.Debugf("Client does not have the key to sign snapshot. " +
|
|
"Assuming that server should sign the snapshot.")
|
|
} else {
|
|
logrus.Debugf("Client was unable to sign the snapshot: %s", err.Error())
|
|
return err
|
|
}
|
|
|
|
remote := r.getRemoteStore()
|
|
|
|
return remote.SetMulti(data.MetadataRoleMapToStringMap(updatedFiles))
|
|
}
|
|
|
|
func signRootIfNecessary(updates map[data.RoleName][]byte, repo *tuf.Repo, extraSigningKeys data.KeyList, initialPublish bool) error {
|
|
if len(extraSigningKeys) > 0 {
|
|
repo.Root.Dirty = true
|
|
}
|
|
if nearExpiry(repo.Root.Signed.SignedCommon) || repo.Root.Dirty {
|
|
rootJSON, err := serializeCanonicalRole(repo, data.CanonicalRootRole, extraSigningKeys)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
updates[data.CanonicalRootRole] = rootJSON
|
|
} else if initialPublish {
|
|
rootJSON, err := repo.Root.MarshalJSON()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
updates[data.CanonicalRootRole] = rootJSON
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Fetch back a `legacyVersions` number of roots files, collect the root public keys
|
|
// This includes old `root` roles as well as legacy versioned root roles, e.g. `1.root`
|
|
func (r *repository) oldKeysForLegacyClientSupport(legacyVersions int, initialPublish bool) (data.KeyList, error) {
|
|
if initialPublish {
|
|
return nil, nil
|
|
}
|
|
|
|
var oldestVersion int
|
|
prevVersion := r.tufRepo.Root.Signed.Version
|
|
|
|
if legacyVersions == SignWithAllOldVersions {
|
|
oldestVersion = 1
|
|
} else {
|
|
oldestVersion = r.tufRepo.Root.Signed.Version - legacyVersions
|
|
}
|
|
|
|
if oldestVersion < 1 {
|
|
oldestVersion = 1
|
|
}
|
|
|
|
if prevVersion <= 1 || oldestVersion == prevVersion {
|
|
return nil, nil
|
|
}
|
|
oldKeys := make(map[string]data.PublicKey)
|
|
|
|
c, err := bootstrapClient(TUFLoadOptions{
|
|
GUN: r.gun,
|
|
TrustPinning: r.trustPinning,
|
|
CryptoService: r.cryptoService,
|
|
Cache: r.cache,
|
|
RemoteStore: r.remoteStore,
|
|
AlwaysCheckInitialized: true,
|
|
})
|
|
// require a server connection to fetch old roots
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for v := prevVersion; v >= oldestVersion; v-- {
|
|
logrus.Debugf("fetching old keys from version %d", v)
|
|
// fetch old root version
|
|
versionedRole := fmt.Sprintf("%d.%s", v, data.CanonicalRootRole.String())
|
|
|
|
raw, err := c.remote.GetSized(versionedRole, -1)
|
|
if err != nil {
|
|
logrus.Debugf("error downloading %s: %s", versionedRole, err)
|
|
continue
|
|
}
|
|
|
|
signedOldRoot := &data.Signed{}
|
|
if err := json.Unmarshal(raw, signedOldRoot); err != nil {
|
|
return nil, err
|
|
}
|
|
oldRootVersion, err := data.RootFromSigned(signedOldRoot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// extract legacy versioned root keys
|
|
oldRootVersionKeys := getOldRootPublicKeys(oldRootVersion)
|
|
for _, oldKey := range oldRootVersionKeys {
|
|
oldKeys[oldKey.ID()] = oldKey
|
|
}
|
|
}
|
|
oldKeyList := make(data.KeyList, 0, len(oldKeys))
|
|
for _, key := range oldKeys {
|
|
oldKeyList = append(oldKeyList, key)
|
|
}
|
|
return oldKeyList, nil
|
|
}
|
|
|
|
// get all the saved previous roles keys < the current root version
|
|
func getOldRootPublicKeys(root *data.SignedRoot) data.KeyList {
|
|
rootRole, err := root.BuildBaseRole(data.CanonicalRootRole)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return rootRole.ListKeys()
|
|
}
|
|
|
|
func signTargets(updates map[data.RoleName][]byte, repo *tuf.Repo, initialPublish bool) error {
|
|
// iterate through all the targets files - if they are dirty, sign and update
|
|
for roleName, roleObj := range repo.Targets {
|
|
if roleObj.Dirty || (roleName == data.CanonicalTargetsRole && initialPublish) {
|
|
targetsJSON, err := serializeCanonicalRole(repo, roleName, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
updates[roleName] = targetsJSON
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 assumes that bootstrapRepo is only used by Publish() or RotateKey()
|
|
func (r *repository) bootstrapRepo() error {
|
|
b := tuf.NewRepoBuilder(r.gun, r.GetCryptoService(), r.trustPinning)
|
|
|
|
logrus.Debugf("Loading trusted collection.")
|
|
|
|
for _, role := range data.BaseRoles {
|
|
jsonBytes, err := r.cache.GetSized(role.String(), store.NoSizeLimit)
|
|
if err != nil {
|
|
if _, ok := err.(store.ErrMetaNotFound); ok &&
|
|
// server snapshots are supported, and server timestamp management
|
|
// is required, so if either of these fail to load that's ok - especially
|
|
// if the repo is new
|
|
role == data.CanonicalSnapshotRole || role == data.CanonicalTimestampRole {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
if err := b.Load(role, jsonBytes, 1, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
tufRepo, _, err := b.Finish()
|
|
if err == nil {
|
|
r.tufRepo = tufRepo
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// saveMetadata saves contents of r.tufRepo onto the local disk, creating
|
|
// signatures as necessary, possibly prompting for passphrases.
|
|
func (r *repository) saveMetadata(ignoreSnapshot bool) error {
|
|
logrus.Debugf("Saving changes to Trusted Collection.")
|
|
|
|
rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = r.cache.Set(data.CanonicalRootRole.String(), rootJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
targetsToSave := make(map[data.RoleName][]byte)
|
|
for t := range r.tufRepo.Targets {
|
|
signedTargets, err := r.tufRepo.SignTargets(t, data.DefaultExpires(data.CanonicalTargetsRole))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
targetsJSON, err := json.Marshal(signedTargets)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
targetsToSave[t] = targetsJSON
|
|
}
|
|
|
|
for role, blob := range targetsToSave {
|
|
// If the parent directory does not exist, the cache.Set will create it
|
|
r.cache.Set(role.String(), blob)
|
|
}
|
|
|
|
if ignoreSnapshot {
|
|
return nil
|
|
}
|
|
|
|
snapshotJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalSnapshotRole, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return r.cache.Set(data.CanonicalSnapshotRole.String(), snapshotJSON)
|
|
}
|
|
|
|
// RotateKey removes all existing keys associated with the role. If no keys are
|
|
// specified in keyList, then this creates and adds one new key or delegates
|
|
// managing the key to the server. If key(s) are specified by keyList, then they are
|
|
// used for signing the role.
|
|
// These changes are staged in a changelist until publish is called.
|
|
func (r *repository) RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error {
|
|
if err := checkRotationInput(role, serverManagesKey); err != nil {
|
|
return err
|
|
}
|
|
|
|
pubKeyList, err := r.pubKeyListForRotation(role, serverManagesKey, keyList)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cl := changelist.NewMemChangelist()
|
|
if err := r.rootFileKeyChange(cl, role, changelist.ActionCreate, pubKeyList); err != nil {
|
|
return err
|
|
}
|
|
return r.publish(cl)
|
|
}
|
|
|
|
// Given a set of new keys to rotate to and a set of keys to drop, returns the list of current keys to use
|
|
func (r *repository) pubKeyListForRotation(role data.RoleName, serverManaged bool, newKeys []string) (pubKeyList data.KeyList, err error) {
|
|
var pubKey data.PublicKey
|
|
|
|
// If server manages the key being rotated, request a rotation and return the new key
|
|
if serverManaged {
|
|
remote := r.getRemoteStore()
|
|
pubKey, err = rotateRemoteKey(role, remote)
|
|
pubKeyList = make(data.KeyList, 0, 1)
|
|
pubKeyList = append(pubKeyList, pubKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to rotate remote key: %s", err)
|
|
}
|
|
return pubKeyList, nil
|
|
}
|
|
|
|
// If no new keys are passed in, we generate one
|
|
if len(newKeys) == 0 {
|
|
pubKeyList = make(data.KeyList, 0, 1)
|
|
pubKey, err = r.GetCryptoService().Create(role, r.gun, data.ECDSAKey)
|
|
pubKeyList = append(pubKeyList, pubKey)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to generate key: %s", err)
|
|
}
|
|
|
|
// If a list of keys to rotate to are provided, we add those
|
|
if len(newKeys) > 0 {
|
|
pubKeyList = make(data.KeyList, 0, len(newKeys))
|
|
for _, keyID := range newKeys {
|
|
pubKey = r.GetCryptoService().GetKey(keyID)
|
|
if pubKey == nil {
|
|
return nil, fmt.Errorf("unable to find key: %s", keyID)
|
|
}
|
|
pubKeyList = append(pubKeyList, pubKey)
|
|
}
|
|
}
|
|
|
|
// Convert to certs (for root keys)
|
|
if pubKeyList, err = r.pubKeysToCerts(role, pubKeyList); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return pubKeyList, nil
|
|
}
|
|
|
|
func (r *repository) pubKeysToCerts(role data.RoleName, pubKeyList data.KeyList) (data.KeyList, error) {
|
|
// only generate certs for root keys
|
|
if role != data.CanonicalRootRole {
|
|
return pubKeyList, nil
|
|
}
|
|
|
|
for i, pubKey := range pubKeyList {
|
|
privKey, loadedRole, err := r.GetCryptoService().GetPrivateKey(pubKey.ID())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if loadedRole != role {
|
|
return nil, fmt.Errorf("attempted to load root key but given %s key instead", loadedRole)
|
|
}
|
|
pubKey, err = rootCertKey(r.gun, privKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pubKeyList[i] = pubKey
|
|
}
|
|
return pubKeyList, nil
|
|
}
|
|
|
|
func checkRotationInput(role data.RoleName, serverManaged 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 {
|
|
case !data.ValidRole(role) || data.IsDelegation(role):
|
|
return fmt.Errorf("notary does not currently permit rotating the %s key", role)
|
|
case serverManaged && !canBeRemoteKey:
|
|
return ErrInvalidRemoteRole{Role: role}
|
|
case !serverManaged && !canBeLocalKey:
|
|
return ErrInvalidLocalRole{Role: role}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) rootFileKeyChange(cl changelist.Changelist, role data.RoleName, action string, keyList []data.PublicKey) error {
|
|
meta := changelist.TUFRootData{
|
|
RoleName: role,
|
|
Keys: keyList,
|
|
}
|
|
metaJSON, err := json.Marshal(meta)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c := changelist.NewTUFChange(
|
|
action,
|
|
changelist.ScopeRoot,
|
|
changelist.TypeBaseRole,
|
|
role.String(),
|
|
metaJSON,
|
|
)
|
|
return cl.Add(c)
|
|
}
|
|
|
|
// DeleteTrustData removes the trust data stored for this repo in the TUF cache on the client side
|
|
// Note that we will not delete any private key material from local storage
|
|
func DeleteTrustData(baseDir string, gun data.GUN, URL string, rt http.RoundTripper, deleteRemote bool) error {
|
|
localRepo := filepath.Join(baseDir, tufDir, filepath.FromSlash(gun.String()))
|
|
// Remove the tufRepoPath directory, which includes local TUF metadata files and changelist information
|
|
if err := os.RemoveAll(localRepo); err != nil {
|
|
return fmt.Errorf("error clearing TUF repo data: %v", err)
|
|
}
|
|
// Note that this will require admin permission for the gun in the roundtripper
|
|
if deleteRemote {
|
|
remote, err := getRemoteStore(URL, gun, rt)
|
|
if err != nil {
|
|
logrus.Errorf("unable to instantiate a remote store: %v", err)
|
|
return err
|
|
}
|
|
if err := remote.RemoveAll(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetLegacyVersions allows the number of legacy versions of the root
|
|
// to be inspected for old signing keys to be configured.
|
|
func (r *repository) SetLegacyVersions(n int) {
|
|
r.LegacyVersions = n
|
|
}
|