blob: b383c94dcafdb209adc63bdd8a2e1db4fea93264 [file] [log] [blame]
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
"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/tuf"
tufclient "github.com/docker/notary/tuf/client"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/keys"
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/store"
)
const (
maxSize = 5 << 20
)
func init() {
data.SetDefaultExpiryTimes(
map[string]int{
"root": 3650,
"targets": 1095,
"snapshot": 1095,
},
)
}
// ErrRepoNotInitialized is returned when trying to publish an uninitialized
// notary repository
type ErrRepoNotInitialized struct{}
func (err ErrRepoNotInitialized) Error() string {
return "repository has not been initialized"
}
// ErrInvalidRemoteRole is returned when the server is requested to manage
// an unsupported key type
type ErrInvalidRemoteRole struct {
Role string
}
func (err ErrInvalidRemoteRole) Error() string {
return fmt.Sprintf(
"notary does not support the server managing the %s key", err.Role)
}
// ErrRepositoryNotExist is returned when an action is taken on a remote
// repository that doesn't exist
type ErrRepositoryNotExist struct {
remote string
gun string
}
func (err ErrRepositoryNotExist) Error() string {
return fmt.Sprintf("%s does not have trust data for %s", err.remote, err.gun)
}
const (
tufDir = "tuf"
)
// NotaryRepository stores all the information needed to operate on a notary
// repository.
type NotaryRepository struct {
baseDir string
gun string
baseURL string
tufRepoPath string
fileStore store.MetadataStore
CryptoService signed.CryptoService
tufRepo *tuf.Repo
roundTrip http.RoundTripper
CertStore trustmanager.X509Store
}
// 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) {
certPath := filepath.Join(baseDir, notary.TrustedCertsDir)
certStore, err := trustmanager.NewX509FilteredFileStore(
certPath,
trustmanager.FilterCertsExpiredSha1,
)
if err != nil {
return nil, err
}
cryptoService := cryptoservice.NewCryptoService(gun, keyStores...)
nRepo := &NotaryRepository{
gun: gun,
baseDir: baseDir,
baseURL: baseURL,
tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)),
CryptoService: cryptoService,
roundTrip: rt,
CertStore: certStore,
}
fileStore, err := store.NewFilesystemStore(
nRepo.tufRepoPath,
"metadata",
"json",
"",
)
if err != nil {
return nil, err
}
nRepo.fileStore = fileStore
return nRepo, nil
}
// Target represents a simplified version of the data TUF operates on, so external
// applications don't have to depend on tuf data types.
type Target struct {
Name string // the name of the target
Hashes data.Hashes // the hash of the target
Length int64 // the size in bytes of the target
}
// TargetWithRole represents a Target that exists in a particular role - this is
// produced by ListTargets and GetTargetByName
type TargetWithRole struct {
Target
Role string
}
// NewTarget is a helper method that returns a Target
func NewTarget(targetName string, targetPath string) (*Target, error) {
b, err := ioutil.ReadFile(targetPath)
if err != nil {
return nil, err
}
meta, err := data.NewFileMeta(bytes.NewBuffer(b))
if err != nil {
return nil, err
}
return &Target{Name: targetName, Hashes: meta.Hashes, Length: meta.Length}, nil
}
// Initialize creates a new repository by using rootKey as the root Key for the
// TUF repository.
func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...string) error {
privKey, _, err := r.CryptoService.GetPrivateKey(rootKeyID)
if err != nil {
return err
}
// 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 := []string{
data.CanonicalTargetsRole,
data.CanonicalSnapshotRole,
// root is also locally managed, but that should have been created
// already
}
remotelyManagedKeys := []string{data.CanonicalTimestampRole}
for _, role := range serverManagedRoles {
switch role {
case data.CanonicalTimestampRole:
continue // timestamp is already in the right place
case data.CanonicalSnapshotRole:
// because we put Snapshot last
locallyManagedKeys = []string{data.CanonicalTargetsRole}
remotelyManagedKeys = append(
remotelyManagedKeys, data.CanonicalSnapshotRole)
serverManagesSnapshot = true
default:
return ErrInvalidRemoteRole{Role: role}
}
}
// Hard-coded policy: the generated certificate expires in 10 years.
startTime := time.Now()
rootCert, err := cryptoservice.GenerateCertificate(
privKey, r.gun, startTime, startTime.AddDate(10, 0, 0))
if err != nil {
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())
}
kdb := keys.NewDB()
err = addKeyForRole(kdb, data.CanonicalRootRole, rootKey)
if err != nil {
return err
}
// we want to create all the local keys first so we don't have to
// make unnecessary network calls
for _, role := range locallyManagedKeys {
// This is currently hardcoding the keys to ECDSA.
key, err := r.CryptoService.Create(role, data.ECDSAKey)
if err != nil {
return err
}
if err := addKeyForRole(kdb, role, key); err != nil {
return err
}
}
for _, role := range remotelyManagedKeys {
// This key is generated by the remote server.
key, err := getRemoteKey(r.baseURL, r.gun, role, r.roundTrip)
if err != nil {
return err
}
logrus.Debugf("got remote %s %s key with keyID: %s",
role, key.Algorithm(), key.ID())
if err := addKeyForRole(kdb, role, key); err != nil {
return err
}
}
r.tufRepo = tuf.NewRepo(kdb, r.CryptoService)
err = r.tufRepo.InitRoot(false)
if err != nil {
logrus.Debug("Error on InitRoot: ", err.Error())
return err
}
_, err = r.tufRepo.InitTargets(data.CanonicalTargetsRole)
if err != nil {
logrus.Debug("Error on InitTargets: ", err.Error())
return err
}
err = r.tufRepo.InitSnapshot()
if err != nil {
logrus.Debug("Error on InitSnapshot: ", err.Error())
return err
}
return r.saveMetadata(serverManagesSnapshot)
}
// adds a TUF Change template to the given roles
func addChange(cl *changelist.FileChangelist, c changelist.Change, roles ...string) error {
if len(roles) == 0 {
roles = []string{data.CanonicalTargetsRole}
}
var changes []changelist.Change
for _, role := range roles {
// Ensure we can only add targets to the CanonicalTargetsRole,
// or a Delegation role (which is <CanonicalTargetsRole>/something else)
if role != data.CanonicalTargetsRole && !data.IsDelegation(role) {
return data.ErrInvalidRole{
Role: role,
Reason: "cannot add targets to this role",
}
}
changes = append(changes, changelist.NewTufChange(
c.Action(),
role,
c.Type(),
c.Path(),
c.Content(),
))
}
for _, c := range changes {
if err := cl.Add(c); err != nil {
return err
}
}
return nil
}
// AddDelegation creates a new changelist entry to add a delegation to the repository
// when the changelist gets applied at publish time. This does not do any validation
// other than checking the name of the delegation to add - all that will happen
// at publish time.
func (r *NotaryRepository) AddDelegation(name string, threshold int,
delegationKeys []data.PublicKey, paths []string) error {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Adding delegation "%s" with threshold %d, and %d keys\n`,
name, threshold, len(delegationKeys))
tdJSON, err := json.Marshal(&changelist.TufDelegation{
NewThreshold: threshold,
AddKeys: data.KeyList(delegationKeys),
AddPaths: paths,
})
if err != nil {
return err
}
template := changelist.NewTufChange(
changelist.ActionCreate,
name,
changelist.TypeTargetsDelegation,
"", // no path
tdJSON,
)
return addChange(cl, template, name)
}
// RemoveDelegation creates a new changelist entry to remove a delegation from
// the repository when the changelist gets applied at publish time.
// This does not validate that the delegation exists, since one might exist
// after applying all changes.
func (r *NotaryRepository) RemoveDelegation(name string, keyIDs, paths []string, removeAll bool) error {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Removing delegation "%s"\n`, name)
var template *changelist.TufChange
// We use the Delete action only for force removal, Update is used for removing individual keys and paths
if removeAll {
template = changelist.NewTufChange(
changelist.ActionDelete,
name,
changelist.TypeTargetsDelegation,
"", // no path
nil, // deleting role, no data needed
)
} else {
tdJSON, err := json.Marshal(&changelist.TufDelegation{
RemoveKeys: keyIDs,
RemovePaths: paths,
})
if err != nil {
return err
}
template = changelist.NewTufChange(
changelist.ActionUpdate,
name,
changelist.TypeTargetsDelegation,
"", // no path
tdJSON,
)
}
return addChange(cl, template, name)
}
// AddTarget creates new changelist entries to add a target to the given roles
// in the repository when the changelist gets applied at publish time.
// If roles are unspecified, the default role is "targets".
func (r *NotaryRepository) AddTarget(target *Target, roles ...string) error {
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
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}
metaJSON, err := json.Marshal(meta)
if err != nil {
return err
}
template := changelist.NewTufChange(
changelist.ActionCreate, "", changelist.TypeTargetsTarget,
target.Name, metaJSON)
return addChange(cl, 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 *NotaryRepository) RemoveTarget(targetName string, roles ...string) error {
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
logrus.Debugf("Removing target \"%s\"", targetName)
template := changelist.NewTufChange(changelist.ActionDelete, "",
changelist.TypeTargetsTarget, targetName, nil)
return addChange(cl, template, roles...)
}
// ListTargets lists all targets for the current repository. The list of
// roles should be passed in order from highest to lowest priority.
// IMPORTANT: if you pass a set of roles such as [ "targets/a", "targets/x"
// "targets/a/b" ], even though "targets/a/b" is part of the "targets/a" subtree
// its entries will be strictly shadowed by those in other parts of the "targets/a"
// subtree and also the "targets/x" subtree, as we will defer parsing it until
// we explicitly reach it in our iteration of the provided list of roles.
func (r *NotaryRepository) ListTargets(roles ...string) ([]*TargetWithRole, error) {
_, err := r.Update(false)
if err != nil {
return nil, err
}
if len(roles) == 0 {
roles = []string{data.CanonicalTargetsRole}
}
targets := make(map[string]*TargetWithRole)
for _, role := range roles {
// we don't need to do anything special with removing role from
// roles because listSubtree always processes role and only excludes
// descendant delegations that appear in roles.
r.listSubtree(targets, role, roles...)
}
var targetList []*TargetWithRole
for _, v := range targets {
targetList = append(targetList, v)
}
return targetList, nil
}
func (r *NotaryRepository) listSubtree(targets map[string]*TargetWithRole, role string, exclude ...string) {
excl := make(map[string]bool)
for _, r := range exclude {
excl[r] = true
}
roles := []string{role}
for len(roles) > 0 {
role = roles[0]
roles = roles[1:]
tgts, ok := r.tufRepo.Targets[role]
if !ok {
// not every role has to exist
continue
}
for name, meta := range tgts.Signed.Targets {
if _, ok := targets[name]; !ok {
targets[name] = &TargetWithRole{
Target: Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, Role: role}
}
}
for _, d := range tgts.Signed.Delegations.Roles {
if !excl[d.Name] {
roles = append(roles, d.Name)
}
}
}
}
// GetTargetByName returns a target given a name. If no roles are passed
// it uses the targets role and does a search of the entire delegation
// graph, finding the first entry in a breadth first search of the delegations.
// If roles are passed, they should be passed in descending priority and
// the target entry found in the subtree of the highest priority role
// will be returned
// See the IMPORTANT section on ListTargets above. Those roles also apply here.
func (r *NotaryRepository) GetTargetByName(name string, roles ...string) (*TargetWithRole, error) {
c, err := r.Update(false)
if err != nil {
return nil, err
}
if len(roles) == 0 {
roles = append(roles, data.CanonicalTargetsRole)
}
for _, role := range roles {
meta, foundRole := c.TargetMeta(role, name, roles...)
if meta != nil {
return &TargetWithRole{
Target: Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, Role: foundRole}, nil
}
}
return nil, fmt.Errorf("No trust data for %s", name)
}
// GetChangelist returns the list of the repository's unpublished changes
func (r *NotaryRepository) GetChangelist() (changelist.Changelist, error) {
changelistDir := filepath.Join(r.tufRepoPath, "changelist")
cl, err := changelist.NewFileChangelist(changelistDir)
if err != nil {
logrus.Debug("Error initializing changelist")
return nil, err
}
return cl, nil
}
// GetDelegationRoles returns the keys and roles of the repository's delegations
func (r *NotaryRepository) GetDelegationRoles() ([]*data.Role, error) {
// Update state of the repo to latest
if _, err := r.Update(false); err != nil {
return nil, err
}
// All top level delegations (ex: targets/level1) are stored exclusively in targets.json
targets, ok := r.tufRepo.Targets[data.CanonicalTargetsRole]
if !ok {
return nil, store.ErrMetaNotFound{Resource: data.CanonicalTargetsRole}
}
allDelegations := targets.Signed.Delegations.Roles
// make a copy for traversing nested delegations
delegationsList := make([]*data.Role, len(allDelegations))
copy(delegationsList, allDelegations)
// Now traverse to lower level delegations (ex: targets/level1/level2)
for len(delegationsList) > 0 {
// Pop off first delegation to traverse
delegation := delegationsList[0]
delegationsList = delegationsList[1:]
// Get metadata
delegationMeta, ok := r.tufRepo.Targets[delegation.Name]
// If we get an error, don't try to traverse further into this subtree because it doesn't exist or is malformed
if !ok {
continue
}
// Add nested delegations to return list and exploration list
allDelegations = append(allDelegations, delegationMeta.Signed.Delegations.Roles...)
delegationsList = append(delegationsList, delegationMeta.Signed.Delegations.Roles...)
}
return allDelegations, nil
}
// RoleWithSignatures is a Role with its associated signatures
type RoleWithSignatures struct {
Signatures []data.Signature
data.Role
}
// ListRoles returns a list of RoleWithSignatures objects for this repo
// 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 {
return nil, err
}
// Get all role info from our updated keysDB, can be empty
roles := r.tufRepo.GetAllLoadedRoles()
var roleWithSigs []RoleWithSignatures
// Populate RoleWithSignatures with Role from keysDB and signatures from TUF metadata
for _, role := range roles {
roleWithSig := RoleWithSignatures{Role: *role, Signatures: nil}
switch role.Name {
case data.CanonicalRootRole:
roleWithSig.Signatures = r.tufRepo.Root.Signatures
case data.CanonicalTargetsRole:
roleWithSig.Signatures = r.tufRepo.Targets[data.CanonicalTargetsRole].Signatures
case data.CanonicalSnapshotRole:
roleWithSig.Signatures = r.tufRepo.Snapshot.Signatures
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 keyDB state
if !data.IsDelegation(role.Name) {
return nil, data.ErrInvalidRole{Role: role.Name, Reason: "invalid role name"}
}
if _, ok := r.tufRepo.Targets[role.Name]; ok {
// We'll only find a signature if we've published any targets with this delegation
roleWithSig.Signatures = r.tufRepo.Targets[role.Name].Signatures
}
}
roleWithSigs = append(roleWithSigs, roleWithSig)
}
return roleWithSigs, nil
}
// 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 *NotaryRepository) Publish() error {
var initialPublish bool
// update first before publishing
_, err := r.Update(true)
if 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 {
err := r.bootstrapRepo()
if err != nil {
logrus.Debugf("Unable to load repository from local files: %s",
err.Error())
if _, ok := err.(store.ErrMetaNotFound); ok {
return ErrRepoNotInitialized{}
}
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: ", err.Error())
return err
}
}
cl, err := r.GetChangelist()
if err != nil {
return err
}
// apply the changelist to the repo
err = applyChangelist(r.tufRepo, cl)
if 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[string][]byte)
// 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 nearExpiry(r.tufRepo.Root) || r.tufRepo.Root.Dirty {
rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole)
if err != nil {
return err
}
updatedFiles[data.CanonicalRootRole] = rootJSON
} else if initialPublish {
rootJSON, err := r.tufRepo.Root.MarshalJSON()
if err != nil {
return err
}
updatedFiles[data.CanonicalRootRole] = rootJSON
}
// iterate through all the targets files - if they are dirty, sign and update
for roleName, roleObj := range r.tufRepo.Targets {
if roleObj.Dirty || (roleName == data.CanonicalTargetsRole && initialPublish) {
targetsJSON, err := serializeCanonicalRole(r.tufRepo, roleName)
if err != nil {
return err
}
updatedFiles[roleName] = targetsJSON
}
}
// if we initialized the repo while designating the server as the snapshot
// signer, then there won't be a snapshots file. However, we might now
// have a local key (if there was a rotation), so initialize one.
if r.tufRepo.Snapshot == nil {
if err := r.tufRepo.InitSnapshot(); err != nil {
return err
}
}
snapshotJSON, err := serializeCanonicalRole(
r.tufRepo, data.CanonicalSnapshotRole)
if err == nil {
// Only update the snapshot if we've sucessfully signed it.
updatedFiles[data.CanonicalSnapshotRole] = snapshotJSON
} else if _, ok := err.(signed.ErrNoKeys); ok {
// If signing fails due to us not having the snapshot key, then
// assume the server is going to sign, and do not include any snapshot
// data.
logrus.Debugf("Client does not have the key to sign snapshot. " +
"Assuming that server should sign the snapshot.")
} else {
logrus.Debugf("Client was unable to sign the snapshot: %s", err.Error())
return err
}
remote, err := getRemoteStore(r.baseURL, r.gun, r.roundTrip)
if err != nil {
return err
}
err = remote.SetMultiMeta(updatedFiles)
if err != nil {
return err
}
err = cl.Clear("")
if 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 ", filepath.Join(r.tufRepoPath, "changelist"))
}
return nil
}
// bootstrapRepo loads the repository from the local file system. This attempts
// to load metadata for all roles. Since server snapshots are supported,
// if the snapshot metadata fails to load, that's ok.
// This can also be unified with some cache reading tools from tuf/client.
// This assumes that bootstrapRepo is only used by Publish()
func (r *NotaryRepository) bootstrapRepo() error {
kdb := keys.NewDB()
tufRepo := tuf.NewRepo(kdb, r.CryptoService)
logrus.Debugf("Loading trusted collection.")
rootJSON, err := r.fileStore.GetMeta("root", 0)
if err != nil {
return err
}
root := &data.SignedRoot{}
err = json.Unmarshal(rootJSON, root)
if err != nil {
return err
}
err = tufRepo.SetRoot(root)
if err != nil {
return err
}
targetsJSON, err := r.fileStore.GetMeta("targets", 0)
if err != nil {
return err
}
targets := &data.SignedTargets{}
err = json.Unmarshal(targetsJSON, targets)
if err != nil {
return err
}
tufRepo.SetTargets("targets", targets)
snapshotJSON, err := r.fileStore.GetMeta("snapshot", 0)
if err == nil {
snapshot := &data.SignedSnapshot{}
err = json.Unmarshal(snapshotJSON, snapshot)
if err != nil {
return err
}
tufRepo.SetSnapshot(snapshot)
} else if _, ok := err.(store.ErrMetaNotFound); !ok {
return err
}
r.tufRepo = tufRepo
return nil
}
func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error {
logrus.Debugf("Saving changes to Trusted Collection.")
rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole)
if err != nil {
return err
}
err = r.fileStore.SetMeta(data.CanonicalRootRole, rootJSON)
if err != nil {
return err
}
targetsToSave := make(map[string][]byte)
for t := range r.tufRepo.Targets {
signedTargets, err := r.tufRepo.SignTargets(t, data.DefaultExpires("targets"))
if err != nil {
return err
}
targetsJSON, err := json.Marshal(signedTargets)
if err != nil {
return err
}
targetsToSave[t] = targetsJSON
}
for role, blob := range targetsToSave {
parentDir := filepath.Dir(role)
os.MkdirAll(parentDir, 0755)
r.fileStore.SetMeta(role, blob)
}
if ignoreSnapshot {
return nil
}
snapshotJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalSnapshotRole)
if err != nil {
return err
}
return r.fileStore.SetMeta(data.CanonicalSnapshotRole, snapshotJSON)
}
// returns a properly constructed ErrRepositoryNotExist error based on this
// repo's information
func (r *NotaryRepository) errRepositoryNotExist() error {
host := r.baseURL
parsed, err := url.Parse(r.baseURL)
if err == nil {
host = parsed.Host // try to exclude the scheme and any paths
}
return ErrRepositoryNotExist{remote: host, gun: r.gun}
}
// Update bootstraps a trust anchor (root.json) before updating all the
// metadata from the repo.
func (r *NotaryRepository) Update(forWrite bool) (*tufclient.Client, error) {
c, err := r.bootstrapClient(forWrite)
if err != nil {
if _, ok := err.(store.ErrMetaNotFound); ok {
return nil, r.errRepositoryNotExist()
}
return nil, err
}
err = c.Update()
if err != nil {
if notFound, ok := err.(store.ErrMetaNotFound); ok && notFound.Resource == data.CanonicalRootRole {
return nil, r.errRepositoryNotExist()
}
return nil, err
}
return c, nil
}
// bootstrapClient attempts to bootstrap a root.json to be used as the trust
// anchor for a repository. The checkInitialized argument indicates whether
// 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.
func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Client, error) {
var (
rootJSON []byte
err error
signedRoot *data.SignedRoot
)
// 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", maxSize)
if cachedRootErr == nil {
signedRoot, cachedRootErr = r.validateRoot(rootJSON)
}
remote, remoteErr := getRemoteStore(r.baseURL, r.gun, r.roundTrip)
if remoteErr != nil {
logrus.Error(remoteErr)
} else if cachedRootErr != nil || checkInitialized {
// remoteErr was nil and we had a cachedRootErr (or are specifically
// checking for initialization of the repo).
// if remote store successfully set up, try and get root from remote
tmpJSON, err := remote.GetMeta("root", maxSize)
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.
return nil, err
}
if cachedRootErr != nil {
// we always want to use the downloaded root if there was a cache
// error.
signedRoot, err = r.validateRoot(tmpJSON)
if err != nil {
return nil, err
}
err = r.fileStore.SetMeta("root", 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())
}
}
}
kdb := keys.NewDB()
r.tufRepo = tuf.NewRepo(kdb, r.CryptoService)
if signedRoot == nil {
return nil, ErrRepoNotInitialized{}
}
err = r.tufRepo.SetRoot(signedRoot)
if err != nil {
return nil, err
}
return tufclient.NewClient(
r.tufRepo,
remote,
kdb,
r.fileStore,
), nil
}
// validateRoot MUST only be used during bootstrapping. It will only validate
// signatures of the root based on known keys, not expiry or other metadata.
// This is so that an out of date root can be loaded to be used in a rotation
// should the TUF update process detect a problem.
func (r *NotaryRepository) validateRoot(rootJSON []byte) (*data.SignedRoot, error) {
// can't just unmarshal into SignedRoot because validate root
// needs the root.Signed field to still be []byte for signature
// validation
root := &data.Signed{}
err := json.Unmarshal(rootJSON, root)
if err != nil {
return nil, err
}
err = certs.ValidateRoot(r.CertStore, root, r.gun)
if err != nil {
return nil, err
}
return data.RootFromSigned(root)
}
// RotateKey removes all existing keys associated with the role, and either
// 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 {
if role == data.CanonicalRootRole || role == data.CanonicalTimestampRole {
return fmt.Errorf(
"notary does not currently support rotating the %s key", role)
}
if serverManagesKey && role == data.CanonicalTargetsRole {
return ErrInvalidRemoteRole{Role: data.CanonicalTargetsRole}
}
var (
pubKey data.PublicKey
err error
)
if serverManagesKey {
pubKey, err = getRemoteKey(r.baseURL, r.gun, role, r.roundTrip)
} else {
pubKey, err = r.CryptoService.Create(role, data.ECDSAKey)
}
if err != nil {
return err
}
return r.rootFileKeyChange(role, changelist.ActionCreate, pubKey)
}
func (r *NotaryRepository) rootFileKeyChange(role, action string, key data.PublicKey) error {
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
kl := make(data.KeyList, 0, 1)
kl = append(kl, key)
meta := changelist.TufRootData{
RoleName: role,
Keys: kl,
}
metaJSON, err := json.Marshal(meta)
if err != nil {
return err
}
c := changelist.NewTufChange(
action,
changelist.ScopeRoot,
changelist.TypeRootRole,
role,
metaJSON,
)
err = cl.Add(c)
if err != nil {
return err
}
return nil
}
// DeleteTrustData removes the trust data stored for this repo in the TUF cache and certificate store on the client side
func (r *NotaryRepository) DeleteTrustData() error {
// Clear TUF files and cache
if err := r.fileStore.RemoveAll(); err != nil {
return fmt.Errorf("error clearing TUF repo data: %v", err)
}
r.tufRepo = tuf.NewRepo(nil, nil)
// Clear certificates
certificates, err := r.CertStore.GetCertificatesByCN(r.gun)
if err != nil {
// If there were no certificates to delete, we're done
if _, ok := err.(*trustmanager.ErrNoCertificatesFound); ok {
return nil
}
return fmt.Errorf("error retrieving certificates for %s: %v", r.gun, err)
}
for _, cert := range certificates {
if err := r.CertStore.RemoveCert(cert); err != nil {
return fmt.Errorf("error removing certificate: %v: %v", cert, err)
}
}
return nil
}